From 47a91d44ad4e9db8a9d67dfe223395998fea9bb2 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Sun, 29 Sep 2024 13:35:04 +0200 Subject: [PATCH 1/4] buiding with kotlin 2.0.20 --- .idea/kotlinc.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 6d0ee1c..d4b7acc 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From 8983d6d42e957df7dc46678f72fd5f3d9e787ab7 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Fri, 4 Oct 2024 20:58:24 +0200 Subject: [PATCH 2/4] modalsheet: fix handling custom BackHandler in modalsheet --- .../hrach/navigation/demo/screens/Modal1.kt | 20 ++ .../navigation/modalsheet/ModalSheetDialog.kt | 188 ++---------------- 2 files changed, 32 insertions(+), 176 deletions(-) diff --git a/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt b/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt index 20aa530..a8b8131 100644 --- a/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt +++ b/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt @@ -1,22 +1,30 @@ package dev.hrach.navigation.demo.screens import android.annotation.SuppressLint +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.navigation.NavController import dev.hrach.navigation.demo.Destinations import dev.hrach.navigation.results.NavigationResultEffect @@ -42,6 +50,11 @@ private fun Modal1( navigate: (Any) -> Unit, bottomSheetResult: Int, ) { + var disableBackHandling by rememberSaveable { mutableStateOf(false) } + BackHandler(disableBackHandling) { + // no-op + } + Surface( color = MaterialTheme.colorScheme.inverseSurface, ) { @@ -58,6 +71,13 @@ private fun Modal1( Text("BottomSheet") } Text("BottomSheetResult: $bottomSheetResult") + + Spacer(Modifier.height(32.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Disable back handling") + Switch(disableBackHandling, onCheckedChange = { disableBackHandling = it }) + } } } } diff --git a/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetDialog.kt b/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetDialog.kt index 70edf41..582c25d 100644 --- a/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetDialog.kt +++ b/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetDialog.kt @@ -7,16 +7,10 @@ import android.view.View import android.view.ViewOutlineProvider import android.view.Window import android.view.WindowManager -import android.window.BackEvent -import android.window.OnBackAnimationCallback -import android.window.OnBackInvokedCallback -import android.window.OnBackInvokedDispatcher import androidx.activity.BackEventCompat import androidx.activity.ComponentDialog -import androidx.activity.addCallback +import androidx.activity.compose.PredictiveBackHandler import androidx.activity.setViewTreeOnBackPressedDispatcherOwner -import androidx.annotation.DoNotInline -import androidx.annotation.RequiresApi import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box @@ -29,7 +23,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCompositionContext -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -54,16 +47,7 @@ import androidx.lifecycle.setViewTreeViewModelStoreOwner import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import java.util.UUID -import java.util.concurrent.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.BufferOverflow.SUSPEND -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.Channel.Factory.BUFFERED import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.consumeAsFlow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.launch @Composable internal fun ModalSheetDialog( @@ -79,18 +63,16 @@ internal fun ModalSheetDialog( val dialogId = rememberSaveable { UUID.randomUUID() } val darkThemeEnabled = isSystemInDarkTheme() val currentOnPredictiveBack = rememberUpdatedState(onPredictiveBack) - val scope = rememberCoroutineScope() val dialog = remember(view, density) { ModalSheetDialogWrapper( - currentOnPredictiveBack, - view, - scope, - securePolicy, - layoutDirection, - density, - dialogId, - darkThemeEnabled, + onPredictiveBack = currentOnPredictiveBack, + composeView = view, + securePolicy = securePolicy, + layoutDirection = layoutDirection, + density = density, + dialogId = dialogId, + darkThemeEnabled = darkThemeEnabled, ).apply { setContent(composition) { Box( @@ -124,10 +106,8 @@ private class ModalSheetDialogLayout( context: Context, override val window: Window, private val onPredictiveBack: State) -> Unit>, - private val scope: CoroutineScope, ) : AbstractComposeView(context), DialogWindowProvider { private var content: @Composable () -> Unit by mutableStateOf({}) - private var backCallback: Any? = null override var shouldCreateCompositionOnAttachedToWindow: Boolean = false private set @@ -140,111 +120,9 @@ private class ModalSheetDialogLayout( @Composable override fun Content() { + PredictiveBackHandler(onBack = onPredictiveBack.value) content() } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - maybeRegisterBackCallback() - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - maybeUnregisterBackCallback() - } - - private fun maybeRegisterBackCallback() { - if (Build.VERSION.SDK_INT < 33) return - if (backCallback == null) { - backCallback = when { - Build.VERSION.SDK_INT >= 34 -> Api34Impl.createBackCallback(onPredictiveBack, scope) - else -> Api33Impl.createBackCallback(onPredictiveBack, scope) - } - } - Api33Impl.maybeRegisterBackCallback(this, backCallback) - } - - private fun maybeUnregisterBackCallback() { - if (Build.VERSION.SDK_INT >= 33) { - Api33Impl.maybeUnregisterBackCallback(this, backCallback) - } - backCallback = null - } - - @RequiresApi(34) - private object Api34Impl { - @JvmStatic - @DoNotInline - fun createBackCallback( - currentOnBack: State) -> Unit>, - scope: CoroutineScope, - ) = object : OnBackAnimationCallback { - var onBackInstance: OnBackInstance? = null - - override fun onBackStarted(backEvent: BackEvent) { - onBackInstance?.cancel() - onBackInstance = OnBackInstance(scope, true, currentOnBack.value) - } - - override fun onBackProgressed(backEvent: BackEvent) { - onBackInstance?.send(BackEventCompat(backEvent)) - } - - override fun onBackInvoked() { - onBackInstance?.apply { - if (!isPredictiveBack) { - cancel() - onBackInstance = null - } - } - if (onBackInstance == null) { - onBackInstance = OnBackInstance(scope, false, currentOnBack.value) - } - onBackInstance?.close() - onBackInstance?.isPredictiveBack = false - } - - override fun onBackCancelled() { - onBackInstance?.cancel() - onBackInstance = null - onBackInstance?.isPredictiveBack = false - } - } - } - - @RequiresApi(33) - private object Api33Impl { - @JvmStatic - @DoNotInline - fun createBackCallback( - currentOnBack: State) -> Unit>, - scope: CoroutineScope, - ) { - OnBackInvokedCallback { - scope.launch { - currentOnBack.value.invoke(flowOf()) - } - } - } - - @JvmStatic - @DoNotInline - fun maybeRegisterBackCallback(view: View, backCallback: Any?) { - if (backCallback !is OnBackInvokedCallback) return - val dispatcher = view.findOnBackInvokedDispatcher() ?: return - dispatcher.registerOnBackInvokedCallback( - OnBackInvokedDispatcher.PRIORITY_OVERLAY, - backCallback, - ) - } - - @JvmStatic - @DoNotInline - fun maybeUnregisterBackCallback(view: View, backCallback: Any?) { - if (backCallback !is OnBackInvokedCallback) return - view.findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(backCallback) - } - } } // Fork of androidx.compose.ui.window.DialogWrapper. @@ -253,7 +131,6 @@ private class ModalSheetDialogLayout( internal class ModalSheetDialogWrapper( onPredictiveBack: State) -> Unit>, private val composeView: View, - scope: CoroutineScope, securePolicy: SecureFlagPolicy, layoutDirection: LayoutDirection, density: Density, @@ -274,10 +151,9 @@ internal class ModalSheetDialogWrapper( window.setBackgroundDrawableResource(android.R.color.transparent) WindowCompat.setDecorFitsSystemWindows(window, false) dialogLayout = ModalSheetDialogLayout( - context, - window, - onPredictiveBack, - scope, + context = context, + window = window, + onPredictiveBack = onPredictiveBack, ).apply { // Set unique id for AbstractComposeView. This allows state restoration for the state // defined inside the Dialog via rememberSaveable() @@ -310,17 +186,6 @@ internal class ModalSheetDialogWrapper( dialogLayout.setViewTreeOnBackPressedDispatcherOwner(this) // Initial setup updateParameters(securePolicy, layoutDirection, darkThemeEnabled) - - // Due to how the onDismissRequest callback works - // (it enforces a just-in-time decision on whether to update the state to hide the dialog) - // we need to unconditionally add a callback here that is always enabled, - // meaning we'll never get a system UI controlled predictive back animation - // for these dialogs - onBackPressedDispatcher.addCallback(this) { - scope.launch { - onPredictiveBack.value.invoke(flowOf()) - } - } } private fun setLayoutDirection(layoutDirection: LayoutDirection) { @@ -383,35 +248,6 @@ internal class ModalSheetDialogWrapper( } } -private class OnBackInstance( - scope: CoroutineScope, - var isPredictiveBack: Boolean, - onBack: suspend (progress: Flow) -> Unit, -) { - val channel = Channel(capacity = BUFFERED, onBufferOverflow = SUSPEND) - val job = scope.launch { - var completed = false - onBack( - channel.consumeAsFlow().onCompletion { - completed = true - }, - ) - check(completed) { - "You must collect the progress flow" - } - } - - fun send(backEvent: BackEventCompat) = channel.trySend(backEvent) - - // idempotent if invoked more than once - fun close() = channel.close() - - fun cancel() { - channel.cancel(CancellationException("onBack cancelled")) - job.cancel() - } -} - internal fun View.isFlagSecureEnabled(): Boolean { val windowParams = rootView.layoutParams as? WindowManager.LayoutParams if (windowParams != null) { From 86beb9f3172931ff6908fa7e9f71481cb0881ad3 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Fri, 4 Oct 2024 21:44:41 +0200 Subject: [PATCH 3/4] fix modalsheet missing final exit animation --- .../hrach/navigation/demo/screens/Modal1.kt | 7 ++ .../navigation/modalsheet/ModalSheetHost.kt | 100 +++--------------- .../modalsheet/ModalSheetNavigator.kt | 1 + 3 files changed, 21 insertions(+), 87 deletions(-) diff --git a/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt b/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt index a8b8131..456fc98 100644 --- a/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt +++ b/demo/src/main/kotlin/dev/hrach/navigation/demo/screens/Modal1.kt @@ -41,6 +41,7 @@ internal fun Modal1(navController: NavController) { } Modal1( navigate = navController::navigate, + close = navController::popBackStack, bottomSheetResult = bottomSheetResult, ) } @@ -48,6 +49,7 @@ internal fun Modal1(navController: NavController) { @Composable private fun Modal1( navigate: (Any) -> Unit, + close: () -> Unit, bottomSheetResult: Int, ) { var disableBackHandling by rememberSaveable { mutableStateOf(false) } @@ -78,6 +80,11 @@ private fun Modal1( Text("Disable back handling") Switch(disableBackHandling, onCheckedChange = { disableBackHandling = it }) } + + Spacer(Modifier.height(32.dp)) + OutlinedButton(onClick = close) { + Text("Close") + } } } } diff --git a/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetHost.kt b/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetHost.kt index 8ecb759..2c5f69c 100644 --- a/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetHost.kt +++ b/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetHost.kt @@ -21,19 +21,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.window.SecureFlagPolicy -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.compose.LocalOwnersProvider @@ -57,24 +52,14 @@ public fun ModalSheetHost( sizeTransform: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards SizeTransform?)? = null, ) { - val modalBackStack by modalSheetNavigator.backStack.collectAsState(listOf()) - var progress by remember { mutableFloatStateOf(0f) } var inPredictiveBack by remember { mutableStateOf(false) } - val zIndices = remember { mutableMapOf() } val saveableStateHolder = rememberSaveableStateHolder() - val visibleEntries = rememberVisibleList(modalBackStack) - visibleEntries.PopulateVisibleList(modalBackStack) - - val currentBackStack = if (LocalInspectionMode.current) { - modalSheetNavigator.backStack.collectAsState(emptyList()).value - } else { - visibleEntries - } - val backStackEntry: NavBackStackEntry? = currentBackStack.lastOrNull() + val modalBackStack by modalSheetNavigator.backStack.collectAsState(listOf()) + val backStackEntry: NavBackStackEntry? = modalBackStack.lastOrNull() val finalEnter: AnimatedContentTransitionScope.() -> EnterTransition = { val targetDestination = targetState.destination as ModalSheetNavigator.Destination @@ -90,7 +75,6 @@ public fun ModalSheetHost( } val finalExit: AnimatedContentTransitionScope.() -> ExitTransition = { val initialDestination = initialState.destination as ModalSheetNavigator.Destination - if (modalSheetNavigator.isPop.value) { initialDestination.hierarchy.firstNotNullOfOrNull { destination -> null // destination.createPopExitTransition(this) @@ -103,7 +87,6 @@ public fun ModalSheetHost( } val finalSizeTransform: AnimatedContentTransitionScope.() -> SizeTransform? = { val targetDestination = targetState.destination as ModalSheetNavigator.Destination - targetDestination.hierarchy.firstNotNullOfOrNull { destination -> null // destination.createSizeTransform(this) } ?: sizeTransform?.invoke(this) @@ -115,14 +98,16 @@ public fun ModalSheetHost( // scope exposed by the transitions on the NavHost and composable APIs. SeekableTransitionState(backStackEntry) } + val transitionsInProgress = modalSheetNavigator.transitionsInProgress.collectAsState().value val transition = rememberTransition(transitionState, label = "entry") val nothingToShow = transition.currentState == transition.targetState && transition.currentState == null && - backStackEntry == null + backStackEntry == null && + transitionsInProgress.isEmpty() if (inPredictiveBack) { LaunchedEffect(progress) { - val previousEntry = currentBackStack.getOrNull(currentBackStack.size - 2) + val previousEntry = modalBackStack.getOrNull(modalBackStack.size - 2) transitionState.seekTo(progress, previousEntry) } } else { @@ -163,10 +148,12 @@ public fun ModalSheetHost( ?: SecureFlagPolicy.Inherit ModalSheetDialog( - onPredictiveBack = { backEvent -> + onPredictiveBack = onPredictBack@{ backEvent -> progress = 0f - val currentBackStackEntry = modalBackStack.lastOrNull() - modalSheetNavigator.prepareForTransition(currentBackStackEntry!!) + // early return: already animating backstack out, repeated back handling + // probably reproducible only with slowed animations + val currentBackStackEntry = modalBackStack.lastOrNull() ?: return@onPredictBack + modalSheetNavigator.prepareForTransition(currentBackStackEntry) val previousEntry = modalBackStack.getOrNull(modalBackStack.size - 2) if (previousEntry != null) { modalSheetNavigator.prepareForTransition(previousEntry) @@ -186,7 +173,7 @@ public fun ModalSheetHost( ) { transition.AnimatedContent( modifier = modifier - .background(if (transition.targetState == null) Color.Unspecified else containerColor), + .background(if (transition.targetState == null || transition.currentState == null) Color.Transparent else containerColor), contentAlignment = Alignment.TopStart, transitionSpec = block@{ val initialState = initialState ?: return@block ContentTransform( @@ -218,13 +205,7 @@ public fun ModalSheetHost( sizeTransform = finalSizeTransform(this), ) }, - ) { - val currentEntry = if (inPredictiveBack) { - it - } else { - visibleEntries.lastOrNull { entry -> it == entry } - } - + ) { currentEntry -> if (currentEntry == null) { Box(Modifier.fillMaxSize()) {} return@AnimatedContent @@ -251,58 +232,3 @@ public fun ModalSheetHost( } } } - -@Suppress("ComposeUnstableCollections") -@Composable -internal fun MutableList.PopulateVisibleList( - transitionsInProgress: List, -) { - val isInspecting = LocalInspectionMode.current - transitionsInProgress.forEach { entry -> - DisposableEffect(entry.lifecycle) { - val observer = LifecycleEventObserver { _, event -> - // show dialog in preview - if (isInspecting && !contains(entry)) { - add(entry) - } - // ON_START -> add to visibleBackStack, ON_STOP -> remove from visibleBackStack - if (event == Lifecycle.Event.ON_START) { - // We want to treat the visible lists as sets, but we want to keep - // the functionality of mutableStateListOf() so that we recompose in response - // to adds and removes. - if (!contains(entry)) { - add(entry) - } - } - if (event == Lifecycle.Event.ON_STOP) { - remove(entry) - } - } - entry.lifecycle.addObserver(observer) - onDispose { - entry.lifecycle.removeObserver(observer) - } - } - } -} - -@Composable -internal fun rememberVisibleList( - transitionsInProgress: List, -): SnapshotStateList { - // show dialog in preview - val isInspecting = LocalInspectionMode.current - return remember(transitionsInProgress) { - mutableStateListOf().also { - it.addAll( - transitionsInProgress.filter { entry -> - if (isInspecting) { - true - } else { - entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) - } - }, - ) - } - } -} diff --git a/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetNavigator.kt b/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetNavigator.kt index 083faec..774b4b6 100644 --- a/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetNavigator.kt +++ b/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetNavigator.kt @@ -18,6 +18,7 @@ import dev.hrach.navigation.modalsheet.ModalSheetNavigator.Destination @Navigator.Name("ModalSheetNavigator") public class ModalSheetNavigator : Navigator() { internal val backStack get() = state.backStack + internal val transitionsInProgress get() = state.transitionsInProgress internal val isPop = mutableStateOf(false) From a6f87e29b259df958320e49c4f3dbf43a13d31c8 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Fri, 4 Oct 2024 22:14:17 +0200 Subject: [PATCH 4/4] make modalsheet main enter/exit animations configurable --- .../dev/hrach/navigation/demo/NavHost.kt | 72 ++++++++++++++++++- .../navigation/modalsheet/ModalSheetHost.kt | 7 +- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/demo/src/main/kotlin/dev/hrach/navigation/demo/NavHost.kt b/demo/src/main/kotlin/dev/hrach/navigation/demo/NavHost.kt index b3f8217..8cf3494 100644 --- a/demo/src/main/kotlin/dev/hrach/navigation/demo/NavHost.kt +++ b/demo/src/main/kotlin/dev/hrach/navigation/demo/NavHost.kt @@ -1,10 +1,23 @@ package dev.hrach.navigation.demo +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -29,9 +42,14 @@ internal fun NavHost( modalSheetNavigator: ModalSheetNavigator, bottomSheetNavigator: BottomSheetNavigator, ) { + val density = LocalDensity.current NavHost( navController = navController, startDestination = Destinations.Home, + enterTransition = { SharedXAxisEnterTransition(density) }, + exitTransition = { SharedXAxisExitTransition(density) }, + popEnterTransition = { SharedXAxisPopEnterTransition(density) }, + popExitTransition = { SharedXAxisPopExitTransition(density) }, ) { composable { Home(navController) } composable { List() } @@ -40,10 +58,16 @@ internal fun NavHost( modalSheet { Modal2() } bottomSheet { BottomSheet(navController) } } - ModalSheetHost(modalSheetNavigator, containerColor = MaterialTheme.colorScheme.background) + ModalSheetHost( + modalSheetNavigator = modalSheetNavigator, + containerColor = MaterialTheme.colorScheme.background, + enterTransition = { SharedYAxisEnterTransition(density) }, + exitTransition = { SharedYAxisExitTransition(density) }, + ) BottomSheetHost( - bottomSheetNavigator, - shape = RoundedCornerShape( // optional, just an example of bottom sheet custom property + navigator = bottomSheetNavigator, + shape = RoundedCornerShape( + // optional, just an example of bottom sheet custom property topStart = CornerSize(12.dp), topEnd = CornerSize(12.dp), bottomStart = CornerSize(0.dp), @@ -51,3 +75,45 @@ internal fun NavHost( ), ) } + +private val SharedXAxisEnterTransition: (Density) -> EnterTransition = { density -> + fadeIn(animationSpec = tween(durationMillis = 210, delayMillis = 90, easing = LinearOutSlowInEasing)) + + slideInHorizontally(animationSpec = tween(durationMillis = 300)) { + with(density) { 30.dp.roundToPx() } + } +} + +private val SharedXAxisPopEnterTransition: (Density) -> EnterTransition = { density -> + fadeIn(animationSpec = tween(durationMillis = 210, delayMillis = 90, easing = LinearOutSlowInEasing)) + + slideInHorizontally(animationSpec = tween(durationMillis = 300)) { + with(density) { (-30).dp.roundToPx() } + } +} + +private val SharedXAxisExitTransition: (Density) -> ExitTransition = { density -> + fadeOut(animationSpec = tween(durationMillis = 90, easing = FastOutLinearInEasing)) + + slideOutHorizontally(animationSpec = tween(durationMillis = 300)) { + with(density) { (-30).dp.roundToPx() } + } +} + +private val SharedXAxisPopExitTransition: (Density) -> ExitTransition = { density -> + fadeOut(animationSpec = tween(durationMillis = 90, easing = FastOutLinearInEasing)) + + slideOutHorizontally(animationSpec = tween(durationMillis = 300)) { + with(density) { 30.dp.roundToPx() } + } +} + +private val SharedYAxisEnterTransition: (Density) -> EnterTransition = { density -> + fadeIn(animationSpec = tween(durationMillis = 210, delayMillis = 90, easing = LinearOutSlowInEasing)) + + slideInVertically(animationSpec = tween(durationMillis = 300)) { + with(density) { 30.dp.roundToPx() } + } +} + +private val SharedYAxisExitTransition: (Density) -> ExitTransition = { density -> + fadeOut(animationSpec = tween(durationMillis = 210, delayMillis = 90, easing = LinearOutSlowInEasing)) + + slideOutVertically(animationSpec = tween(durationMillis = 300)) { + with(density) { 30.dp.roundToPx() } + } +} diff --git a/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetHost.kt b/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetHost.kt index 2c5f69c..3c0d77a 100644 --- a/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetHost.kt +++ b/modalsheet/src/main/kotlin/dev/hrach/navigation/modalsheet/ModalSheetHost.kt @@ -176,14 +176,17 @@ public fun ModalSheetHost( .background(if (transition.targetState == null || transition.currentState == null) Color.Transparent else containerColor), contentAlignment = Alignment.TopStart, transitionSpec = block@{ + @Suppress("UNCHECKED_CAST") val initialState = initialState ?: return@block ContentTransform( - fadeIn(), + enterTransition(this as AnimatedContentTransitionScope), fadeOut(), // irrelevant 0f, ) + + @Suppress("UNCHECKED_CAST") val targetState = targetState ?: return@block ContentTransform( fadeIn(), // irrelevant - fadeOut(), + exitTransition(this as AnimatedContentTransitionScope), 0f, )