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