Skip to content

Commit 86beb9f

Browse files
committed
fix modalsheet missing final exit animation
1 parent 8983d6d commit 86beb9f

File tree

3 files changed

+21
-87
lines changed

3 files changed

+21
-87
lines changed

demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ internal fun Modal1(navController: NavController) {
4141
}
4242
Modal1(
4343
navigate = navController::navigate,
44+
close = navController::popBackStack,
4445
bottomSheetResult = bottomSheetResult,
4546
)
4647
}
4748

4849
@Composable
4950
private fun Modal1(
5051
navigate: (Any) -> Unit,
52+
close: () -> Unit,
5153
bottomSheetResult: Int,
5254
) {
5355
var disableBackHandling by rememberSaveable { mutableStateOf(false) }
@@ -78,6 +80,11 @@ private fun Modal1(
7880
Text("Disable back handling")
7981
Switch(disableBackHandling, onCheckedChange = { disableBackHandling = it })
8082
}
83+
84+
Spacer(Modifier.height(32.dp))
85+
OutlinedButton(onClick = close) {
86+
Text("Close")
87+
}
8188
}
8289
}
8390
}

modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetHost.kt

Lines changed: 13 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,14 @@ import androidx.compose.runtime.LaunchedEffect
2121
import androidx.compose.runtime.collectAsState
2222
import androidx.compose.runtime.getValue
2323
import androidx.compose.runtime.mutableFloatStateOf
24-
import androidx.compose.runtime.mutableStateListOf
2524
import androidx.compose.runtime.mutableStateOf
2625
import androidx.compose.runtime.remember
2726
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
2827
import androidx.compose.runtime.setValue
29-
import androidx.compose.runtime.snapshots.SnapshotStateList
3028
import androidx.compose.ui.Alignment
3129
import androidx.compose.ui.Modifier
3230
import androidx.compose.ui.graphics.Color
33-
import androidx.compose.ui.platform.LocalInspectionMode
3431
import androidx.compose.ui.window.SecureFlagPolicy
35-
import androidx.lifecycle.Lifecycle
36-
import androidx.lifecycle.LifecycleEventObserver
3732
import androidx.navigation.NavBackStackEntry
3833
import androidx.navigation.NavDestination.Companion.hierarchy
3934
import androidx.navigation.compose.LocalOwnersProvider
@@ -57,24 +52,14 @@ public fun ModalSheetHost(
5752
sizeTransform: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards SizeTransform?)? =
5853
null,
5954
) {
60-
val modalBackStack by modalSheetNavigator.backStack.collectAsState(listOf())
61-
6255
var progress by remember { mutableFloatStateOf(0f) }
6356
var inPredictiveBack by remember { mutableStateOf(false) }
64-
6557
val zIndices = remember { mutableMapOf<String, Float>() }
6658

6759
val saveableStateHolder = rememberSaveableStateHolder()
6860

69-
val visibleEntries = rememberVisibleList(modalBackStack)
70-
visibleEntries.PopulateVisibleList(modalBackStack)
71-
72-
val currentBackStack = if (LocalInspectionMode.current) {
73-
modalSheetNavigator.backStack.collectAsState(emptyList()).value
74-
} else {
75-
visibleEntries
76-
}
77-
val backStackEntry: NavBackStackEntry? = currentBackStack.lastOrNull()
61+
val modalBackStack by modalSheetNavigator.backStack.collectAsState(listOf())
62+
val backStackEntry: NavBackStackEntry? = modalBackStack.lastOrNull()
7863

7964
val finalEnter: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
8065
val targetDestination = targetState.destination as ModalSheetNavigator.Destination
@@ -90,7 +75,6 @@ public fun ModalSheetHost(
9075
}
9176
val finalExit: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
9277
val initialDestination = initialState.destination as ModalSheetNavigator.Destination
93-
9478
if (modalSheetNavigator.isPop.value) {
9579
initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
9680
null // destination.createPopExitTransition(this)
@@ -103,7 +87,6 @@ public fun ModalSheetHost(
10387
}
10488
val finalSizeTransform: AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform? = {
10589
val targetDestination = targetState.destination as ModalSheetNavigator.Destination
106-
10790
targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
10891
null // destination.createSizeTransform(this)
10992
} ?: sizeTransform?.invoke(this)
@@ -115,14 +98,16 @@ public fun ModalSheetHost(
11598
// scope exposed by the transitions on the NavHost and composable APIs.
11699
SeekableTransitionState(backStackEntry)
117100
}
101+
val transitionsInProgress = modalSheetNavigator.transitionsInProgress.collectAsState().value
118102
val transition = rememberTransition(transitionState, label = "entry")
119103
val nothingToShow = transition.currentState == transition.targetState &&
120104
transition.currentState == null &&
121-
backStackEntry == null
105+
backStackEntry == null &&
106+
transitionsInProgress.isEmpty()
122107

123108
if (inPredictiveBack) {
124109
LaunchedEffect(progress) {
125-
val previousEntry = currentBackStack.getOrNull(currentBackStack.size - 2)
110+
val previousEntry = modalBackStack.getOrNull(modalBackStack.size - 2)
126111
transitionState.seekTo(progress, previousEntry)
127112
}
128113
} else {
@@ -163,10 +148,12 @@ public fun ModalSheetHost(
163148
?: SecureFlagPolicy.Inherit
164149

165150
ModalSheetDialog(
166-
onPredictiveBack = { backEvent ->
151+
onPredictiveBack = onPredictBack@{ backEvent ->
167152
progress = 0f
168-
val currentBackStackEntry = modalBackStack.lastOrNull()
169-
modalSheetNavigator.prepareForTransition(currentBackStackEntry!!)
153+
// early return: already animating backstack out, repeated back handling
154+
// probably reproducible only with slowed animations
155+
val currentBackStackEntry = modalBackStack.lastOrNull() ?: return@onPredictBack
156+
modalSheetNavigator.prepareForTransition(currentBackStackEntry)
170157
val previousEntry = modalBackStack.getOrNull(modalBackStack.size - 2)
171158
if (previousEntry != null) {
172159
modalSheetNavigator.prepareForTransition(previousEntry)
@@ -186,7 +173,7 @@ public fun ModalSheetHost(
186173
) {
187174
transition.AnimatedContent(
188175
modifier = modifier
189-
.background(if (transition.targetState == null) Color.Unspecified else containerColor),
176+
.background(if (transition.targetState == null || transition.currentState == null) Color.Transparent else containerColor),
190177
contentAlignment = Alignment.TopStart,
191178
transitionSpec = block@{
192179
val initialState = initialState ?: return@block ContentTransform(
@@ -218,13 +205,7 @@ public fun ModalSheetHost(
218205
sizeTransform = finalSizeTransform(this),
219206
)
220207
},
221-
) {
222-
val currentEntry = if (inPredictiveBack) {
223-
it
224-
} else {
225-
visibleEntries.lastOrNull { entry -> it == entry }
226-
}
227-
208+
) { currentEntry ->
228209
if (currentEntry == null) {
229210
Box(Modifier.fillMaxSize()) {}
230211
return@AnimatedContent
@@ -251,58 +232,3 @@ public fun ModalSheetHost(
251232
}
252233
}
253234
}
254-
255-
@Suppress("ComposeUnstableCollections")
256-
@Composable
257-
internal fun MutableList<NavBackStackEntry>.PopulateVisibleList(
258-
transitionsInProgress: List<NavBackStackEntry>,
259-
) {
260-
val isInspecting = LocalInspectionMode.current
261-
transitionsInProgress.forEach { entry ->
262-
DisposableEffect(entry.lifecycle) {
263-
val observer = LifecycleEventObserver { _, event ->
264-
// show dialog in preview
265-
if (isInspecting && !contains(entry)) {
266-
add(entry)
267-
}
268-
// ON_START -> add to visibleBackStack, ON_STOP -> remove from visibleBackStack
269-
if (event == Lifecycle.Event.ON_START) {
270-
// We want to treat the visible lists as sets, but we want to keep
271-
// the functionality of mutableStateListOf() so that we recompose in response
272-
// to adds and removes.
273-
if (!contains(entry)) {
274-
add(entry)
275-
}
276-
}
277-
if (event == Lifecycle.Event.ON_STOP) {
278-
remove(entry)
279-
}
280-
}
281-
entry.lifecycle.addObserver(observer)
282-
onDispose {
283-
entry.lifecycle.removeObserver(observer)
284-
}
285-
}
286-
}
287-
}
288-
289-
@Composable
290-
internal fun rememberVisibleList(
291-
transitionsInProgress: List<NavBackStackEntry>,
292-
): SnapshotStateList<NavBackStackEntry> {
293-
// show dialog in preview
294-
val isInspecting = LocalInspectionMode.current
295-
return remember(transitionsInProgress) {
296-
mutableStateListOf<NavBackStackEntry>().also {
297-
it.addAll(
298-
transitionsInProgress.filter { entry ->
299-
if (isInspecting) {
300-
true
301-
} else {
302-
entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
303-
}
304-
},
305-
)
306-
}
307-
}
308-
}

modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetNavigator.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import dev.hrach.navigation.modalsheet.ModalSheetNavigator.Destination
1818
@Navigator.Name("ModalSheetNavigator")
1919
public class ModalSheetNavigator : Navigator<Destination>() {
2020
internal val backStack get() = state.backStack
21+
internal val transitionsInProgress get() = state.transitionsInProgress
2122

2223
internal val isPop = mutableStateOf(false)
2324

0 commit comments

Comments
 (0)