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/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/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/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 new file mode 100644 index 00000000..1f4c5baa --- /dev/null +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt @@ -0,0 +1,321 @@ +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.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 + +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 should react to + 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 + // 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 // 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 + // Default Push behaviour. + // Horizontal animation will show from right + // Vertical animation will show from bottom + else sizeDefault to sizeMinus // Case when isInvokerScaleScreen + } + 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 + 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 { + isPush && isInvokerTransitionScreen && isTargetFadeScreen || + isPop && isInvokerFadeScreen && isTargetTransitionScreen -> { + val enter = slide.inHorizontally + fade.In + val exit = slide.outHorizontally + fade.Out + enter togetherWith exit + } + isPush && isInvokerTransitionScreen && isTargetShrinkScreen || + isPop && isInvokerShrinkScreen && isTargetTransitionScreen -> { + val enter = slide.inVertically + val exit = shrink.vertically + enter togetherWith exit + } + isPush && isInvokerTransitionScreen && isTargetScaleScreen -> { + val enter = slide.inVertically + scale.In + val exit = slide.outVertically + fade.Out + scale.Out + enter togetherWith exit + } + isPop && isInvokerScaleScreen && isTargetTransitionScreen -> { + val enter = slide.inHorizontally + fade.In + scale.In + val exit = fade.Out + scale.Out + enter togetherWith exit + } + // Default + else -> { + val slideShort = TransitionSlide( + initialOffset = initialOffset, + targetOffset = targetOffset, + animationSpec = TransitionTween.tweenOffsetShort + ) + slideShort.inHorizontally togetherWith slideShort.outHorizontally + } + } + } + ScreenTransition( + navigator = navigator, + transition = transition, + modifier = modifier, + 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 + } +} + +@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, + ) +} + +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 new file mode 100644 index 00000000..8b6e14f2 --- /dev/null +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt @@ -0,0 +1,155 @@ +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 +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() + .padding(horizontal = 32.dp) + ) { + 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)) + 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 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 + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + 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 + ) + } + } +} 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 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