From df0f5ffdfc2db2a22320736d1d7257b9261b044a Mon Sep 17 00:00:00 2001 From: Velord Date: Thu, 8 Feb 2024 04:09:17 +0200 Subject: [PATCH 1/6] Add: Demo Transition --- samples/android/src/main/AndroidManifest.xml | 1 + .../adriel/voyager/sample/SampleActivity.kt | 2 + .../voyager/sample/transition/FadeScreen.kt | 28 +++ .../voyager/sample/transition/ScaleScreen.kt | 28 +++ .../voyager/sample/transition/ShrinkScreen.kt | 28 +++ .../sample/transition/TransitionActivity.kt | 167 ++++++++++++++++++ .../sample/transition/TransitionScreen.kt | 59 +++++++ .../adriel/voyager/navigator/Navigator.kt | 6 + 8 files changed, 319 insertions(+) create mode 100644 samples/android/src/main/java/cafe/adriel/voyager/sample/transition/FadeScreen.kt create mode 100644 samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ScaleScreen.kt create mode 100644 samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ShrinkScreen.kt create mode 100644 samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt create mode 100644 samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt diff --git a/samples/android/src/main/AndroidManifest.xml b/samples/android/src/main/AndroidManifest.xml index 96782b71..dfa169a5 100644 --- a/samples/android/src/main/AndroidManifest.xml +++ b/samples/android/src/main/AndroidManifest.xml @@ -22,6 +22,7 @@ + diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt index 8d9be558..25e5f342 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt @@ -32,6 +32,7 @@ import cafe.adriel.voyager.sample.rxJavaIntegration.RxJavaIntegrationActivity import cafe.adriel.voyager.sample.screenModel.ScreenModelActivity import cafe.adriel.voyager.sample.stateStack.StateStackActivity import cafe.adriel.voyager.sample.tabNavigation.TabNavigationActivity +import cafe.adriel.voyager.sample.transition.TransitionActivity class SampleActivity : ComponentActivity() { @@ -58,6 +59,7 @@ class SampleActivity : ComponentActivity() { StartSampleButton("Tab Navigation") StartSampleButton("BottomSheet Navigation") StartSampleButton("Nested Navigation") + StartSampleButton("Transition") StartSampleButton("Android ViewModel") StartSampleButton("ScreenModel") StartSampleButton("Koin Integration") diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/FadeScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/FadeScreen.kt new file mode 100644 index 00000000..3c16b45e --- /dev/null +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/FadeScreen.kt @@ -0,0 +1,28 @@ +package cafe.adriel.voyager.sample.transition + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.uniqueScreenKey + +data object FadeScreen : Screen { + override val key = uniqueScreenKey + + @Composable + override fun Content() { + Box(modifier = Modifier.fillMaxSize()) { + Text( + text = "Fade Screen", + modifier = Modifier.align(alignment = Alignment.Center), + color = Color.Red, + fontSize = 30.sp + ) + } + } +} diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ScaleScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ScaleScreen.kt new file mode 100644 index 00000000..e5fabe0b --- /dev/null +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ScaleScreen.kt @@ -0,0 +1,28 @@ +package cafe.adriel.voyager.sample.transition + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.uniqueScreenKey + +data object ScaleScreen : Screen { + override val key = uniqueScreenKey + + @Composable + override fun Content() { + Box(modifier = Modifier.fillMaxSize()) { + Text( + text = "Scale Screen", + modifier = Modifier.align(alignment = Alignment.Center), + color = Color.Red, + fontSize = 30.sp + ) + } + } +} diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ShrinkScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ShrinkScreen.kt new file mode 100644 index 00000000..4e069268 --- /dev/null +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/ShrinkScreen.kt @@ -0,0 +1,28 @@ +package cafe.adriel.voyager.sample.transition + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.uniqueScreenKey + +data object ShrinkScreen : Screen { + override val key = uniqueScreenKey + + @Composable + override fun Content() { + Box(modifier = Modifier.fillMaxSize()) { + Text( + text = "Shrink Screen", + modifier = Modifier.align(alignment = Alignment.Center), + color = Color.Red, + fontSize = 30.sp + ) + } + } +} diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt new file mode 100644 index 00000000..7e16a048 --- /dev/null +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt @@ -0,0 +1,167 @@ +package cafe.adriel.voyager.sample.transition + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.isPopLastEvent +import cafe.adriel.voyager.navigator.isPushLastEvent +import cafe.adriel.voyager.transitions.ScreenTransition +import cafe.adriel.voyager.transitions.ScreenTransitionContent + +class TransitionActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + Navigator(TransitionScreen) { + TransitionDemo(it) + } + } + } +} + +@Composable +fun TransitionDemo( + navigator: Navigator, + modifier: Modifier = Modifier, + content: ScreenTransitionContent = { it.Content() } +) { + val transition: AnimatedContentTransitionScope.() -> ContentTransform = { + // Define any StackEvent you want transition to be + val isPush = navigator.isPushLastEvent() + val isPop = navigator.isPopLastEvent() + // Define any Screen you want transition must be from + val invoker = this.initialState + val isInvokerTransitionScreen = invoker == TransitionScreen + val isInvokerFadeScreen = invoker == FadeScreen + val isInvokerShrinkScreen = invoker == ShrinkScreen + val isInvokerScaleScreen = invoker == ScaleScreen + // Define any Screen you want transition must be to + val target = this.targetState + val isTargetTransitionScreen = target == TransitionScreen + val isTargetFadeScreen = target == FadeScreen + val isTargetShrinkScreen = target == ShrinkScreen + val isTargetScaleScreen = target == ScaleScreen + + val tweenOffset: FiniteAnimationSpec = tween( + durationMillis = 2000, + delayMillis = 100, + easing = LinearEasing + ) + val tweenSize: FiniteAnimationSpec = tween( + durationMillis = 2000, + delayMillis = 100, + easing = LinearEasing + ) + + val sizeDefault = ({ size: Int -> size }) + val sizeMinus = ({ size: Int -> -size }) + val (initialOffset, targetOffset) = when { + isPush && isInvokerTransitionScreen -> { + if (isTargetFadeScreen || isTargetShrinkScreen) sizeMinus to sizeDefault + else sizeDefault to sizeMinus + } + isPop && isInvokerFadeScreen && isTargetTransitionScreen -> sizeDefault to sizeMinus + else -> sizeDefault to sizeMinus + } + + val fadeInFrames = keyframes { + durationMillis = 2000 + 0.1f at 0 with LinearEasing + 0.2f at 1800 with LinearEasing + 1.0f at 2000 with LinearEasing + } + val fadeOutFrames = keyframes { + durationMillis = 2000 + 0.9f at 0 with LinearEasing + 0.8f at 100 with LinearEasing + 0.7f at 200 with LinearEasing + 0.6f at 300 with LinearEasing + 0.5f at 400 with LinearEasing + 0.4f at 500 with LinearEasing + 0.3f at 600 with LinearEasing + 0.2f at 1000 with LinearEasing + 0.1f at 1500 with LinearEasing + 0.0f at 2000 with LinearEasing + } + + val scaleInFrames = keyframes { + durationMillis = 2000 + 0.1f at 0 with LinearEasing + 0.3f at 1500 with LinearEasing + 1.0f at 2000 with LinearEasing + } + val scaleOutFrames = keyframes { + durationMillis = 2000 + 0.9f at 0 with LinearEasing + 0.7f at 500 with LinearEasing + 0.3f at 700 with LinearEasing + 0.0f at 2000 with LinearEasing + } + + when { + // Define any transition you want based on the StackEvent, invoker and target + isPush && isInvokerTransitionScreen && isTargetFadeScreen || + isPop && isInvokerFadeScreen && isTargetTransitionScreen -> { + val enter = slideInHorizontally(tweenOffset, initialOffset) + fadeIn(fadeInFrames) + val exit = slideOutHorizontally(tweenOffset, targetOffset) + fadeOut(fadeOutFrames) + enter togetherWith exit + } + isPush && isInvokerTransitionScreen && isTargetShrinkScreen || + isPop && isInvokerShrinkScreen && isTargetTransitionScreen -> { + val enter = slideInVertically(tweenOffset, initialOffset) + val exit = shrinkVertically(animationSpec = tweenSize, shrinkTowards = Alignment.Top) + enter togetherWith exit + } + isPush && isInvokerTransitionScreen && isTargetScaleScreen -> { + val enter = slideInVertically(tweenOffset, initialOffset) + fadeIn(fadeInFrames) + scaleIn(scaleInFrames) + val exit = slideOutVertically(tweenOffset, targetOffset) + fadeOut(fadeOutFrames) + scaleOut(scaleOutFrames) + enter togetherWith exit + } + isPop && isInvokerScaleScreen && isTargetTransitionScreen -> { + val enter = slideInHorizontally(tweenOffset, initialOffset) + fadeIn(fadeInFrames) + scaleIn(scaleInFrames) + val exit = slideOutHorizontally(tweenOffset, targetOffset) + fadeOut(fadeOutFrames) + scaleOut(scaleOutFrames) + enter togetherWith exit + } + else -> { + val animationSpec: FiniteAnimationSpec = tween( + durationMillis = 500, + delayMillis = 100, + easing = LinearEasing + ) + slideInHorizontally(animationSpec, initialOffset) togetherWith + slideOutHorizontally(animationSpec, targetOffset) + } + } + } + ScreenTransition( + navigator = navigator, + transition = transition, + modifier = modifier, + content = content, + ) +} diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt new file mode 100644 index 00000000..919cc78d --- /dev/null +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt @@ -0,0 +1,59 @@ +package cafe.adriel.voyager.sample.transition + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow + +data object TransitionScreen : Screen { + + override val key = uniqueScreenKey + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + PushButton(text = "Push fade left\nPop fade right") { + navigator.push(FadeScreen) + } + Spacer(modifier = Modifier.height(50.dp)) + PushButton(text = "Push shrink top\nPop shrink bottom") { + navigator.push(ShrinkScreen) + } + Spacer(modifier = Modifier.height(50.dp)) + PushButton(text = "Push fade scale bottom\nPop scale right") { + navigator.push(ScaleScreen) + } + } + } +} + +@Composable +private fun PushButton( + text: String, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier.sizeIn(minWidth = 200.dp, minHeight = 70.dp) + ) { + Text(text = text) + } +} diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/Navigator.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/Navigator.kt index 0aa0e187..2c23f7b2 100644 --- a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/Navigator.kt +++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/Navigator.kt @@ -19,6 +19,7 @@ import cafe.adriel.voyager.core.lifecycle.getNavigatorScreenLifecycleProvider import cafe.adriel.voyager.core.lifecycle.rememberScreenLifecycleOwner import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.stack.Stack +import cafe.adriel.voyager.core.stack.StackEvent import cafe.adriel.voyager.core.stack.toMutableStateStack import cafe.adriel.voyager.navigator.internal.ChildrenNavigationDisposableEffect import cafe.adriel.voyager.navigator.internal.LocalNavigatorStateHolder @@ -190,3 +191,8 @@ public data class NavigatorDisposeBehavior( public fun compositionUniqueId(): String = currentCompositeKeyHash.toString(MaxSupportedRadix) private val MaxSupportedRadix = 36 + +public fun Navigator?.isPushLastEvent(): Boolean = this?.lastEvent == StackEvent.Push +public fun Navigator?.isPopLastEvent(): Boolean = this?.lastEvent == StackEvent.Pop +public fun Navigator?.isReplaceLastEvent(): Boolean = this?.lastEvent == StackEvent.Replace +public fun Navigator?.isIdleLastEvent(): Boolean = this?.lastEvent == StackEvent.Idle From 6527376b31f4302dfc13e54bbcd0630eef90f3c0 Mon Sep 17 00:00:00 2001 From: Velord Date: Thu, 8 Feb 2024 04:50:32 +0200 Subject: [PATCH 2/6] Refactor: make more readable TransitionDemo --- .../sample/transition/TransitionActivity.kt | 169 +++++++++++------- 1 file changed, 106 insertions(+), 63 deletions(-) diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt index 7e16a048..bc12ec14 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt @@ -51,7 +51,7 @@ fun TransitionDemo( content: ScreenTransitionContent = { it.Content() } ) { val transition: AnimatedContentTransitionScope.() -> ContentTransform = { - // Define any StackEvent you want transition to be + // Define any StackEvent you want transition should react to val isPush = navigator.isPushLastEvent() val isPop = navigator.isPopLastEvent() // Define any Screen you want transition must be from @@ -66,18 +66,7 @@ fun TransitionDemo( val isTargetFadeScreen = target == FadeScreen val isTargetShrinkScreen = target == ShrinkScreen val isTargetScaleScreen = target == ScaleScreen - - val tweenOffset: FiniteAnimationSpec = tween( - durationMillis = 2000, - delayMillis = 100, - easing = LinearEasing - ) - val tweenSize: FiniteAnimationSpec = tween( - durationMillis = 2000, - delayMillis = 100, - easing = LinearEasing - ) - + // Define offset based on target and invoker val sizeDefault = ({ size: Int -> size }) val sizeMinus = ({ size: Int -> -size }) val (initialOffset, targetOffset) = when { @@ -88,73 +77,44 @@ fun TransitionDemo( isPop && isInvokerFadeScreen && isTargetTransitionScreen -> sizeDefault to sizeMinus else -> sizeDefault to sizeMinus } - - val fadeInFrames = keyframes { - durationMillis = 2000 - 0.1f at 0 with LinearEasing - 0.2f at 1800 with LinearEasing - 1.0f at 2000 with LinearEasing - } - val fadeOutFrames = keyframes { - durationMillis = 2000 - 0.9f at 0 with LinearEasing - 0.8f at 100 with LinearEasing - 0.7f at 200 with LinearEasing - 0.6f at 300 with LinearEasing - 0.5f at 400 with LinearEasing - 0.4f at 500 with LinearEasing - 0.3f at 600 with LinearEasing - 0.2f at 1000 with LinearEasing - 0.1f at 1500 with LinearEasing - 0.0f at 2000 with LinearEasing - } - - val scaleInFrames = keyframes { - durationMillis = 2000 - 0.1f at 0 with LinearEasing - 0.3f at 1500 with LinearEasing - 1.0f at 2000 with LinearEasing - } - val scaleOutFrames = keyframes { - durationMillis = 2000 - 0.9f at 0 with LinearEasing - 0.7f at 500 with LinearEasing - 0.3f at 700 with LinearEasing - 0.0f at 2000 with LinearEasing - } - + // Create transitions + val slide = TransitionSlide(initialOffset = initialOffset, targetOffset = targetOffset) + val fade = TransitionFade + val shrink = TransitionShrink + val scale = TransitionScale + // Define custom behaviour or use default + // There can be any custom transition you want based on StackEvent, invoker and target when { - // Define any transition you want based on the StackEvent, invoker and target isPush && isInvokerTransitionScreen && isTargetFadeScreen || isPop && isInvokerFadeScreen && isTargetTransitionScreen -> { - val enter = slideInHorizontally(tweenOffset, initialOffset) + fadeIn(fadeInFrames) - val exit = slideOutHorizontally(tweenOffset, targetOffset) + fadeOut(fadeOutFrames) + val enter = slide.inHorizontally + fade.In + val exit = slide.outHorizontally + fade.Out enter togetherWith exit } isPush && isInvokerTransitionScreen && isTargetShrinkScreen || isPop && isInvokerShrinkScreen && isTargetTransitionScreen -> { - val enter = slideInVertically(tweenOffset, initialOffset) - val exit = shrinkVertically(animationSpec = tweenSize, shrinkTowards = Alignment.Top) + val enter = slide.inVertically + val exit = shrink.vertically enter togetherWith exit } isPush && isInvokerTransitionScreen && isTargetScaleScreen -> { - val enter = slideInVertically(tweenOffset, initialOffset) + fadeIn(fadeInFrames) + scaleIn(scaleInFrames) - val exit = slideOutVertically(tweenOffset, targetOffset) + fadeOut(fadeOutFrames) + scaleOut(scaleOutFrames) + val enter = slide.inVertically + fade.In + scale.In + val exit = slide.outVertically + fade.Out + scale.Out enter togetherWith exit } isPop && isInvokerScaleScreen && isTargetTransitionScreen -> { - val enter = slideInHorizontally(tweenOffset, initialOffset) + fadeIn(fadeInFrames) + scaleIn(scaleInFrames) - val exit = slideOutHorizontally(tweenOffset, targetOffset) + fadeOut(fadeOutFrames) + scaleOut(scaleOutFrames) + val enter = slide.inHorizontally + fade.In + scale.In + val exit = fade.Out + scale.Out enter togetherWith exit } + // Default else -> { - val animationSpec: FiniteAnimationSpec = tween( - durationMillis = 500, - delayMillis = 100, - easing = LinearEasing + val slideShort = TransitionSlide( + initialOffset = initialOffset, + targetOffset = targetOffset, + animationSpec = TransitionTween.tweenOffsetShort ) - slideInHorizontally(animationSpec, initialOffset) togetherWith - slideOutHorizontally(animationSpec, targetOffset) + slideShort.inHorizontally togetherWith slideShort.outHorizontally } } } @@ -165,3 +125,86 @@ fun TransitionDemo( content = content, ) } + +private object TransitionFrames { + + val fadeInFrames = keyframes { + durationMillis = 2000 + 0.1f at 0 with LinearEasing + 0.2f at 1800 with LinearEasing + 1.0f at 2000 with LinearEasing + } + + val fadeOutFrames = keyframes { + durationMillis = 2000 + 0.9f at 0 with LinearEasing + 0.8f at 100 with LinearEasing + 0.7f at 200 with LinearEasing + 0.6f at 300 with LinearEasing + 0.5f at 400 with LinearEasing + 0.4f at 500 with LinearEasing + 0.3f at 600 with LinearEasing + 0.2f at 1000 with LinearEasing + 0.1f at 1500 with LinearEasing + 0.0f at 2000 with LinearEasing + } + + val scaleInFrames = keyframes { + durationMillis = 2000 + 0.1f at 0 with LinearEasing + 0.3f at 1500 with LinearEasing + 1.0f at 2000 with LinearEasing + } + + val scaleOutFrames = keyframes { + durationMillis = 2000 + 0.9f at 0 with LinearEasing + 0.7f at 500 with LinearEasing + 0.3f at 700 with LinearEasing + 0.0f at 2000 with LinearEasing + } +} + +private object TransitionTween { + val tweenOffsetShort: FiniteAnimationSpec = tween( + durationMillis = 500, + delayMillis = 100, + easing = LinearEasing + ) + val tweenOffset: FiniteAnimationSpec = tween( + durationMillis = 2000, + delayMillis = 100, + easing = LinearEasing + ) + val tweenSize: FiniteAnimationSpec = tween( + durationMillis = 2000, + delayMillis = 100, + easing = LinearEasing + ) +} + +private class TransitionSlide( + initialOffset: (Int) -> Int, + targetOffset: (Int) -> Int, + animationSpec: FiniteAnimationSpec = TransitionTween.tweenOffset +) { + val inHorizontally = slideInHorizontally(animationSpec, initialOffset) + val outHorizontally = slideOutHorizontally(animationSpec, targetOffset) + val inVertically = slideInVertically(animationSpec, initialOffset) + val outVertically = slideOutVertically(animationSpec, targetOffset) +} + +private object TransitionFade { + val In = fadeIn(TransitionFrames.fadeInFrames) + val Out = fadeOut(TransitionFrames.fadeOutFrames) +} + +private object TransitionShrink { + val vertically = shrinkVertically(animationSpec = TransitionTween.tweenSize, shrinkTowards = Alignment.Top) +} + +private object TransitionScale { + val In = scaleIn(TransitionFrames.scaleInFrames) + val Out = scaleOut(TransitionFrames.scaleOutFrames) +} + From 83c908fa50f2cc304961b052e9f0a593c1d7b9ee Mon Sep 17 00:00:00 2001 From: Velord Date: Thu, 8 Feb 2024 07:47:11 +0200 Subject: [PATCH 3/6] Add: comments that describe initialOffset, targetOffset in details --- .../sample/transition/TransitionActivity.kt | 26 ++++++++++++++++--- .../sample/transition/TransitionScreen.kt | 2 +- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt index bc12ec14..b5b38fc1 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt @@ -67,14 +67,34 @@ fun TransitionDemo( val isTargetShrinkScreen = target == ShrinkScreen val isTargetScaleScreen = target == ScaleScreen // Define offset based on target and invoker + // Offset is important to choose side transition be to or from. Top, Left, Right, Bottom val sizeDefault = ({ size: Int -> size }) val sizeMinus = ({ size: Int -> -size }) val (initialOffset, targetOffset) = when { - isPush && isInvokerTransitionScreen -> { + isPush -> { + isInvokerTransitionScreen // in our example is always true + // This reverts animation side. + // FadeScreen will show from left + // ShrinkScreen will show from top if (isTargetFadeScreen || isTargetShrinkScreen) sizeMinus to sizeDefault - else sizeDefault to sizeMinus + // Default Push behaviour. + // Horizontal animation will show from right + // Vertical animation will show from bottom + else sizeDefault to sizeMinus // Case when isInvokerScaleScreen } - isPop && isInvokerFadeScreen && isTargetTransitionScreen -> sizeDefault to sizeMinus + isPop -> { + isTargetTransitionScreen // in our example is always true + // This reverts animation side. + // TransitionScreen will show from right + if (isInvokerFadeScreen) sizeDefault to sizeMinus + // TransitionScreen will show from bottom + else if (isInvokerShrinkScreen) sizeDefault to sizeMinus + // Default Pop behaviour. + // Horizontal animation will show from left + // Vertical animation will show from top + else sizeMinus to sizeDefault // Case when isInvokerScaleScreen + } + // Always the same side else -> sizeDefault to sizeMinus } // Create transitions diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt index 919cc78d..56d07575 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt @@ -38,7 +38,7 @@ data object TransitionScreen : Screen { navigator.push(ShrinkScreen) } Spacer(modifier = Modifier.height(50.dp)) - PushButton(text = "Push fade scale bottom\nPop scale right") { + PushButton(text = "Push fade scale bottom\nPop scale left") { navigator.push(ScaleScreen) } } From 4abe051645cd82640e5a20fe9e3cd7b8a7b9e026 Mon Sep 17 00:00:00 2001 From: Velord Date: Thu, 8 Feb 2024 06:08:07 +0200 Subject: [PATCH 4/6] Add: TransitionTab --- .../sample/transition/TransitionActivity.kt | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt index b5b38fc1..da972b2c 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt @@ -28,6 +28,10 @@ import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.isPopLastEvent import cafe.adriel.voyager.navigator.isPushLastEvent +import cafe.adriel.voyager.navigator.isReplaceLastEvent +import cafe.adriel.voyager.sample.tabNavigation.tabs.FavoritesTab +import cafe.adriel.voyager.sample.tabNavigation.tabs.HomeTab +import cafe.adriel.voyager.sample.tabNavigation.tabs.ProfileTab import cafe.adriel.voyager.transitions.ScreenTransition import cafe.adriel.voyager.transitions.ScreenTransitionContent @@ -186,6 +190,11 @@ private object TransitionFrames { } private object TransitionTween { + val tweenOffsetShortest: FiniteAnimationSpec = tween( + durationMillis = 200, + delayMillis = 50, + easing = LinearEasing + ) val tweenOffsetShort: FiniteAnimationSpec = tween( durationMillis = 500, delayMillis = 100, @@ -228,3 +237,85 @@ private object TransitionScale { val Out = scaleOut(TransitionFrames.scaleOutFrames) } +@Composable +fun TransitionTab( + navigator: Navigator, + modifier: Modifier = Modifier, + content: ScreenTransitionContent = { it.Content() } +) { + val transition: AnimatedContentTransitionScope.() -> ContentTransform = { + // Define any StackEvent you want transition should react to + val isReplace = navigator.isReplaceLastEvent() // in TabNavigator is always true + // Define any Screen you want transition must be from + val invoker = this.initialState + val isInvokerHomeTab = invoker == HomeTab + val isInvokerFavoritesTab = invoker == FavoritesTab + val isInvokerProfileTab = invoker == ProfileTab + // Define any Screen you want transition must be to + val target = this.targetState + val isTargetHomeTab = target == HomeTab + val isTargetFavoritesTab = target == FavoritesTab + val isTargetProfileTab = target == ProfileTab + // Define offset based on target and invoker + // Offset is important to choose side transition be to or from. Top, Left, Right, Bottom + val sizeDefault = ({ size: Int -> size }) + val sizeMinus = ({ size: Int -> -size }) + val (initialOffset, targetOffset) = when { + isReplace -> { // in TabNavigator is always true + // This reverts animation side. + // Any else tabs will appear from the left + if (isInvokerProfileTab) sizeMinus to sizeDefault + // From center tab to the most left tab + else if (isInvokerFavoritesTab && isTargetHomeTab) sizeMinus to sizeDefault + // Default Push behaviour. + // Horizontal animation will show from right + // Vertical animation will show from bottom + else sizeDefault to sizeMinus // Case when isInvokerScaleScreen + } + // Always the same side + else -> sizeDefault to sizeMinus + } + // Create transitions + val slide = TransitionSlide(initialOffset = initialOffset, targetOffset = targetOffset) + val fade = TransitionFade + val shrink = TransitionShrink + val scale = TransitionScale + // Define custom behaviour or use default + // There can be any custom transition you want based on StackEvent, invoker and target + when { + // From the most left tab to the most right tab + isInvokerHomeTab && isTargetProfileTab -> { + val enter = scale.In + fade.In + val exit = scale.Out + fade.Out + enter togetherWith exit + } + // From the most right tab to the center tab + isInvokerProfileTab && isTargetFavoritesTab -> { + val enter = slide.inVertically + val exit = shrink.vertically + enter togetherWith exit + } + // From the most right tab to the most left tab + isInvokerProfileTab && isTargetHomeTab -> { + val enter = scale.In + fade.In + val exit = slide.outVertically + enter togetherWith exit + } + // Default + else -> { + val slideShort = TransitionSlide( + initialOffset = initialOffset, + targetOffset = targetOffset, + animationSpec = TransitionTween.tweenOffsetShortest + ) + slideShort.inHorizontally togetherWith slideShort.outHorizontally + } + } + } + ScreenTransition( + navigator = navigator, + transition = transition, + modifier = modifier, + content = content, + ) +} From c72f6ecff293869c510b41bdd48fb345068a61bd Mon Sep 17 00:00:00 2001 From: Velord Date: Thu, 8 Feb 2024 06:35:19 +0200 Subject: [PATCH 5/6] Add: TabNavigationScreen which shows how transition might work between tabs --- .../tabNavigation/TabNavigationActivity.kt | 80 ++++++++-------- .../sample/tabNavigation/tabs/TabContent.kt | 2 - .../sample/transition/TabNavigationScreen.kt | 17 ++++ .../sample/transition/TransitionActivity.kt | 96 +++++++++---------- .../sample/transition/TransitionScreen.kt | 9 +- .../voyager/navigator/tab/TabNavigator.kt | 2 +- 6 files changed, 116 insertions(+), 90 deletions(-) create mode 100644 samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TabNavigationScreen.kt diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/TabNavigationActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/TabNavigationActivity.kt index 26e73ad6..ba5b2245 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/TabNavigationActivity.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/TabNavigationActivity.kt @@ -26,49 +26,53 @@ class TabNavigationActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { - Content() - } - } - - @Composable - fun Content() { - TabNavigator( - HomeTab, - tabDisposable = { - TabDisposable( - navigator = it, - tabs = listOf(HomeTab, FavoritesTab, ProfileTab) - ) + TabNavigationContent { + CurrentTab() } - ) { tabNavigator -> - Scaffold( - topBar = { - TopAppBar( - title = { Text(text = tabNavigator.current.options.title) } - ) - }, - content = { - CurrentTab() - }, - bottomBar = { - BottomNavigation { - TabNavigationItem(HomeTab) - TabNavigationItem(FavoritesTab) - TabNavigationItem(ProfileTab) - } - } - ) } } +} - @Composable - private fun RowScope.TabNavigationItem(tab: Tab) { - val tabNavigator = LocalTabNavigator.current +@Composable +private fun RowScope.TabNavigationItem(tab: Tab) { + val tabNavigator = LocalTabNavigator.current - BottomNavigationItem( - selected = tabNavigator.current.key == tab.key, - onClick = { tabNavigator.current = tab }, - icon = { Icon(painter = tab.options.icon!!, contentDescription = tab.options.title) } + BottomNavigationItem( + selected = tabNavigator.current.key == tab.key, + onClick = { tabNavigator.current = tab }, + icon = { Icon(painter = tab.options.icon!!, contentDescription = tab.options.title) } + ) +} + +@Composable +fun TabNavigationContent( + scaffoldContent: @Composable (TabNavigator) -> Unit +) { + TabNavigator( + tab = HomeTab, + tabDisposable = { + TabDisposable( + navigator = it, + tabs = listOf(HomeTab, FavoritesTab, ProfileTab) + ) + } + ) { tabNavigator -> + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = tabNavigator.current.options.title) } + ) + }, + content = { + scaffoldContent(tabNavigator) + }, + bottomBar = { + BottomNavigation { + TabNavigationItem(HomeTab) + TabNavigationItem(FavoritesTab) + TabNavigationItem(ProfileTab) + } + } ) } } diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt index aab8f17b..d8530758 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/tabNavigation/tabs/TabContent.kt @@ -1,7 +1,6 @@ package cafe.adriel.voyager.sample.tabNavigation.tabs import android.util.Log -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope @@ -19,7 +18,6 @@ import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.sample.basicNavigation.BasicNavigationScreen import cafe.adriel.voyager.transitions.SlideTransition -@OptIn(ExperimentalAnimationApi::class) @Composable fun Tab.TabContent() { val tabTitle = options.title diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TabNavigationScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TabNavigationScreen.kt new file mode 100644 index 00000000..5864e68f --- /dev/null +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TabNavigationScreen.kt @@ -0,0 +1,17 @@ +package cafe.adriel.voyager.sample.transition + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.sample.tabNavigation.TabNavigationContent + +data object TabNavigationScreen : Screen { + override val key = uniqueScreenKey + + @Composable + override fun Content() { + TabNavigationContent { + TransitionTab(it.navigator) + } + } +} diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt index da972b2c..2d491d67 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt @@ -189,54 +189,6 @@ private object TransitionFrames { } } -private object TransitionTween { - val tweenOffsetShortest: FiniteAnimationSpec = tween( - durationMillis = 200, - delayMillis = 50, - easing = LinearEasing - ) - val tweenOffsetShort: FiniteAnimationSpec = tween( - durationMillis = 500, - delayMillis = 100, - easing = LinearEasing - ) - val tweenOffset: FiniteAnimationSpec = tween( - durationMillis = 2000, - delayMillis = 100, - easing = LinearEasing - ) - val tweenSize: FiniteAnimationSpec = tween( - durationMillis = 2000, - delayMillis = 100, - easing = LinearEasing - ) -} - -private class TransitionSlide( - initialOffset: (Int) -> Int, - targetOffset: (Int) -> Int, - animationSpec: FiniteAnimationSpec = TransitionTween.tweenOffset -) { - val inHorizontally = slideInHorizontally(animationSpec, initialOffset) - val outHorizontally = slideOutHorizontally(animationSpec, targetOffset) - val inVertically = slideInVertically(animationSpec, initialOffset) - val outVertically = slideOutVertically(animationSpec, targetOffset) -} - -private object TransitionFade { - val In = fadeIn(TransitionFrames.fadeInFrames) - val Out = fadeOut(TransitionFrames.fadeOutFrames) -} - -private object TransitionShrink { - val vertically = shrinkVertically(animationSpec = TransitionTween.tweenSize, shrinkTowards = Alignment.Top) -} - -private object TransitionScale { - val In = scaleIn(TransitionFrames.scaleInFrames) - val Out = scaleOut(TransitionFrames.scaleOutFrames) -} - @Composable fun TransitionTab( navigator: Navigator, @@ -319,3 +271,51 @@ fun TransitionTab( content = content, ) } + +private object TransitionTween { + val tweenOffsetShortest: FiniteAnimationSpec = tween( + durationMillis = 200, + delayMillis = 50, + easing = LinearEasing + ) + val tweenOffsetShort: FiniteAnimationSpec = tween( + durationMillis = 500, + delayMillis = 100, + easing = LinearEasing + ) + val tweenOffset: FiniteAnimationSpec = tween( + durationMillis = 2000, + delayMillis = 100, + easing = LinearEasing + ) + val tweenSize: FiniteAnimationSpec = tween( + durationMillis = 2000, + delayMillis = 100, + easing = LinearEasing + ) +} + +private class TransitionSlide( + initialOffset: (Int) -> Int, + targetOffset: (Int) -> Int, + animationSpec: FiniteAnimationSpec = TransitionTween.tweenOffset +) { + val inHorizontally = slideInHorizontally(animationSpec, initialOffset) + val outHorizontally = slideOutHorizontally(animationSpec, targetOffset) + val inVertically = slideInVertically(animationSpec, initialOffset) + val outVertically = slideOutVertically(animationSpec, targetOffset) +} + +private object TransitionFade { + val In = fadeIn(TransitionFrames.fadeInFrames) + val Out = fadeOut(TransitionFrames.fadeOutFrames) +} + +private object TransitionShrink { + val vertically = shrinkVertically(animationSpec = TransitionTween.tweenSize, shrinkTowards = Alignment.Top) +} + +private object TransitionScale { + val In = scaleIn(TransitionFrames.scaleInFrames) + val Out = scaleOut(TransitionFrames.scaleOutFrames) +} diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt index 56d07575..8f07be54 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn import androidx.compose.material.Button import androidx.compose.material.Text @@ -41,6 +42,10 @@ data object TransitionScreen : Screen { PushButton(text = "Push fade scale bottom\nPop scale left") { navigator.push(ScaleScreen) } + Spacer(modifier = Modifier.height(50.dp)) + PushButton(text = "Tap to see how transition might work inside TabNavigator") { + navigator.push(TabNavigationScreen) + } } } } @@ -52,7 +57,9 @@ private fun PushButton( ) { Button( onClick = onClick, - modifier = Modifier.sizeIn(minWidth = 200.dp, minHeight = 70.dp) + modifier = Modifier + .sizeIn(minWidth = 200.dp, minHeight = 70.dp) + .padding(horizontal = 32.dp) ) { Text(text = text) } diff --git a/voyager-tab-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/tab/TabNavigator.kt b/voyager-tab-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/tab/TabNavigator.kt index 7c93a515..31b0c58b 100644 --- a/voyager-tab-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/tab/TabNavigator.kt +++ b/voyager-tab-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/tab/TabNavigator.kt @@ -56,7 +56,7 @@ public fun TabDisposable(navigator: TabNavigator, tabs: List) { } public class TabNavigator internal constructor( - internal val navigator: Navigator + public val navigator: Navigator ) { public var current: Tab From 0e2d3a5acc3e90bd61b9c7eb1672c72f6f96bafe Mon Sep 17 00:00:00 2001 From: Velord Date: Thu, 8 Feb 2024 07:46:08 +0200 Subject: [PATCH 6/6] Refactor: beautify TransitionScreen --- .../sample/transition/TransitionActivity.kt | 2 +- .../sample/transition/TransitionScreen.kt | 123 +++++++++++++++--- 2 files changed, 107 insertions(+), 18 deletions(-) diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt index 2d491d67..1f4c5baa 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt @@ -122,7 +122,7 @@ fun TransitionDemo( enter togetherWith exit } isPush && isInvokerTransitionScreen && isTargetScaleScreen -> { - val enter = slide.inVertically + fade.In + scale.In + val enter = slide.inVertically + scale.In val exit = slide.outVertically + fade.Out + scale.Out enter togetherWith exit } diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt index 8f07be54..8b6e14f2 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt @@ -1,18 +1,25 @@ package cafe.adriel.voyager.sample.transition +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.navigator.LocalNavigator @@ -29,38 +36,120 @@ data object TransitionScreen : Screen { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 32.dp) ) { - PushButton(text = "Push fade left\nPop fade right") { - navigator.push(FadeScreen) + Frame( + text = "Navigator", + borderColor = Color.Blue + ) { + PushButton( + currentBehaviourMessage = "1) on Push - slide to Right + fade out\n2) on Pop - reverse", + targetBehaviourMessage = "1) on Push slide from Left + fade in\n2) on Pop - reverse" + ) { + navigator.push(FadeScreen) + } + Spacer(modifier = Modifier.height(20.dp)) + PushButton( + currentBehaviourMessage = "Current screen:\n1) on Push - shrink vertically to top\n2) on Pop - slide from bottom", + targetBehaviourMessage = "1) on Push slide from top\n2) on Pop - shrink vertically to top" + ) { + navigator.push(ShrinkScreen) + } + Spacer(modifier = Modifier.height(20.dp)) + PushButton( + currentBehaviourMessage = "1) on Push - slide to top + fade out + scale out\n2) on Pop - slide from left + fade in + scale in", + targetBehaviourMessage = "1) on Push slide from bottom + scale in\n2) on Pop - fade out + scale out" + ) { + navigator.push(ScaleScreen) + } } Spacer(modifier = Modifier.height(50.dp)) - PushButton(text = "Push shrink top\nPop shrink bottom") { - navigator.push(ShrinkScreen) - } - Spacer(modifier = Modifier.height(50.dp)) - PushButton(text = "Push fade scale bottom\nPop scale left") { - navigator.push(ScaleScreen) - } - Spacer(modifier = Modifier.height(50.dp)) - PushButton(text = "Tap to see how transition might work inside TabNavigator") { - navigator.push(TabNavigationScreen) + Frame( + text = "TabNavigator", + borderColor = Color.Magenta + ) { + Button( + onClick = { navigator.push(TabNavigationScreen) }, + modifier = Modifier + .fillMaxWidth() + .sizeIn(minHeight = 70.dp) + .padding(horizontal = 32.dp) + ) { + Text(text = "Tap to see how transition might work inside TabNavigator") + } } } } } @Composable -private fun PushButton( +private fun ColumnScope.Frame( text: String, + borderColor: Color, + content: @Composable () -> Unit +) { + Text( + text = text, + modifier = Modifier.align(Alignment.Start), + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .border( + width = 3.dp, + color = borderColor, + shape = RoundedCornerShape(size = 10.dp) + ) + ) { + Spacer(modifier = Modifier.height(16.dp)) + content() + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +private fun PushButton( + currentBehaviourMessage: String = "", + targetBehaviourMessage: String = "", onClick: () -> Unit ) { Button( onClick = onClick, modifier = Modifier - .sizeIn(minWidth = 200.dp, minHeight = 70.dp) - .padding(horizontal = 32.dp) + .fillMaxWidth() + .padding(horizontal = 16.dp) ) { - Text(text = text) + Column( + horizontalAlignment = Alignment.Start, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Current screen:", + color = Color.Cyan, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + Text( + text = currentBehaviourMessage, + fontSize = 12.sp + ) + Text( + text = "Target screen:", + color = Color.Green, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + Text( + text = targetBehaviourMessage, + fontSize = 12.sp + ) + } } }