diff --git a/samples/android/build.gradle.kts b/samples/android/build.gradle.kts index 3dcaa778..a1c228ea 100644 --- a/samples/android/build.gradle.kts +++ b/samples/android/build.gradle.kts @@ -51,3 +51,7 @@ dependencies { debugImplementation(libs.leakCanary) } + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).all { + kotlinOptions.freeCompilerArgs = listOf("-Xcontext-receivers") +} 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..243fa533 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") @@ -76,7 +78,9 @@ class SampleActivity : ComponentActivity() { Button( onClick = { context.startActivity(Intent(this, T::class.java)) }, - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) ) { Text(text = text) } diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenOne.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenOne.kt index 63bff5e0..4781f603 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenOne.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenOne.kt @@ -18,6 +18,7 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.hilt.getScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.push class LegacyScreenOne : Screen { override val key: ScreenKey = uniqueScreenKey diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenTwo.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenTwo.kt index deceb287..bd637c4c 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenTwo.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidLegacy/LegacyScreenTwo.kt @@ -18,6 +18,7 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.hilt.getScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.pop class LegacyScreenTwo : Screen { override val key: ScreenKey = uniqueScreenKey @@ -47,7 +48,7 @@ class LegacyScreenTwo : Screen { Spacer(modifier = Modifier.height(16.dp)) Button( - onClick = navigator::pop, + onClick = { navigator.pop() }, content = { Text(text = "Go to One") } ) } diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt index 259a10d5..ced20387 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt @@ -5,7 +5,9 @@ import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.pop import cafe.adriel.voyager.sample.DetailsContent import org.koin.androidx.compose.getViewModel import org.koin.core.parameter.parametersOf @@ -20,6 +22,8 @@ data class AndroidDetailsScreen( val navigator = LocalNavigator.currentOrThrow val viewModel = getViewModel { parametersOf(index) } - DetailsContent(viewModel, "Item #${viewModel.index}", navigator::pop) + DetailsContent(viewModel, "Item #${viewModel.index}") { + navigator.pop() + } } } diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt index fb2093c4..3634b8e3 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt @@ -7,6 +7,7 @@ import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.push import cafe.adriel.voyager.sample.ListContent class AndroidListScreen : Screen { @@ -17,6 +18,8 @@ class AndroidListScreen : Screen { val navigator = LocalNavigator.currentOrThrow val viewModel = viewModel() - ListContent(viewModel.items, onClick = { index -> navigator.push(AndroidDetailsScreen(index)) }) + ListContent(viewModel.items, onClick = { + index -> navigator.push(AndroidDetailsScreen(index)) + }) } } diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt index 306cdec5..c1a54918 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/basicNavigation/BasicNavigationScreen.kt @@ -22,6 +22,9 @@ 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 +import cafe.adriel.voyager.navigator.pop +import cafe.adriel.voyager.navigator.push +import cafe.adriel.voyager.navigator.replace data class BasicNavigationScreen( val index: Int, @@ -62,7 +65,7 @@ data class BasicNavigationScreen( ) { Button( enabled = navigator.canPop, - onClick = navigator::pop, + onClick = { navigator.pop() }, modifier = Modifier.weight(.5f) ) { Text(text = "Pop") diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltDetailsScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltDetailsScreen.kt index 83f2559c..0eff6d5f 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltDetailsScreen.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltDetailsScreen.kt @@ -7,6 +7,7 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.hilt.getScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.pop import cafe.adriel.voyager.sample.DetailsContent data class HiltDetailsScreen( @@ -28,6 +29,8 @@ data class HiltDetailsScreen( factory.create(index) } - DetailsContent(viewModel, "Item #${viewModel.index}", navigator::pop) + DetailsContent(viewModel, "Item #${viewModel.index}") { + navigator.pop() + } } } diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltListScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltListScreen.kt index af0a7214..9b330c75 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltListScreen.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/hiltIntegration/HiltListScreen.kt @@ -5,6 +5,7 @@ import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.hilt.getViewModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.push import cafe.adriel.voyager.sample.ListContent class HiltListScreen : Screen { @@ -17,6 +18,6 @@ class HiltListScreen : Screen { // Uncomment version below if you want to use ScreenModel // val viewModel: HiltListScreenModel = getScreenModel() - ListContent(viewModel.items, onClick = { index -> navigator.push(HiltDetailsScreen(index)) }) + ListContent(viewModel.items, onClick = { index -> navigator.push(HiltDetailsScreen(index)) } ) } } diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/nestedNavigation/NestedNavigationActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/nestedNavigation/NestedNavigationActivity.kt index d8f22a22..7f921aa3 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/nestedNavigation/NestedNavigationActivity.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/nestedNavigation/NestedNavigationActivity.kt @@ -42,7 +42,7 @@ class NestedNavigationActivity : ComponentActivity() { NestedNavigation(backgroundColor = Color.White) { navigator -> CurrentScreen() Button( - onClick = { navigator.popUntilRoot() }, + onClick = { navigator.popUntilRoot(navigator.lastItem) }, modifier = Modifier.padding(bottom = 16.dp) ) { Text(text = "Pop Until Root") diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/parcelableScreen/SampleParcelableScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/parcelableScreen/SampleParcelableScreen.kt index 3e94016e..0ef8ce55 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/parcelableScreen/SampleParcelableScreen.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/parcelableScreen/SampleParcelableScreen.kt @@ -23,6 +23,9 @@ 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 +import cafe.adriel.voyager.navigator.pop +import cafe.adriel.voyager.navigator.push +import cafe.adriel.voyager.navigator.replace import kotlinx.parcelize.Parcelize @Parcelize @@ -70,7 +73,7 @@ data class SampleParcelableScreen( ) { Button( enabled = navigator.canPop, - onClick = navigator::pop, + onClick = { navigator.pop() }, modifier = Modifier.weight(.5f) ) { Text(text = "Pop") diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt index 1529a679..010e3910 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt @@ -11,6 +11,7 @@ import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.pop import cafe.adriel.voyager.sample.DetailsContent import cafe.adriel.voyager.sample.LoadingContent @@ -28,7 +29,9 @@ data class DetailsScreen( when (val result = state) { is DetailsScreenModel.State.Loading -> LoadingContent() - is DetailsScreenModel.State.Result -> DetailsContent(screenModel, result.item, navigator::pop) + is DetailsScreenModel.State.Result -> DetailsContent(screenModel, result.item) { + navigator.pop() + } } LaunchedEffect(currentCompositeKeyHash) { diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt index d52789bb..b1008f6e 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt @@ -7,6 +7,7 @@ import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.push import cafe.adriel.voyager.sample.ListContent class ListScreen : Screen { @@ -18,6 +19,8 @@ class ListScreen : Screen { val navigator = LocalNavigator.currentOrThrow val screenModel = rememberScreenModel { ListScreenModel() } - ListContent(screenModel.items, onClick = { index -> navigator.push(DetailsScreen(index)) }) + ListContent(screenModel.items, onClick = { + index -> navigator.push(DetailsScreen(index)) } + ) } } diff --git a/samples/android/src/main/java/cafe/adriel/voyager/sample/stateStack/StateStackActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/stateStack/StateStackActivity.kt index ee4e355b..c3774493 100644 --- a/samples/android/src/main/java/cafe/adriel/voyager/sample/stateStack/StateStackActivity.kt +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/stateStack/StateStackActivity.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.Button import androidx.compose.material.Text @@ -83,7 +82,7 @@ class StateStackActivity : ComponentActivity() { if (stateStack.lastItemOrNull == selectedItem) { selectItem("") } - stateStack.pop() + stateStack.pop("Pop") } ActionButton(text = "Pop Until", enabled = stateStack.canPop) { if (selectedItem.isBlank()) { @@ -91,27 +90,27 @@ class StateStackActivity : ComponentActivity() { return@ActionButton } selectItem("") - stateStack.popUntil { it == selectedItem } + stateStack.popUntil("Pop Until") { it == selectedItem } } ActionButton(text = "Push") { - stateStack.push(randomValue) + stateStack.push("Push", randomValue) } } Row( modifier = Modifier.weight(.1f) ) { ActionButton(text = "Replace") { - stateStack.replace(randomValue) + stateStack.replace("Replace", randomValue) } ActionButton(text = "Replace All") { - stateStack.replaceAll(randomValue) + stateStack.replaceAll("Replace All", randomValue) } } } } @Composable - private fun LazyItemScope.ListItem( + private fun ListItem( index: Int, item: String, isLast: Boolean, 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..016b4a7b 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 @@ -67,7 +67,7 @@ class TabNavigationActivity : ComponentActivity() { BottomNavigationItem( selected = tabNavigator.current.key == tab.key, - onClick = { tabNavigator.current = tab }, + onClick = { tabNavigator.setCurrent(invoker = tabNavigator.current, newTab = tab) }, icon = { Icon(painter = tab.options.icon!!, contentDescription = tab.options.title) } ) } 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..64165a85 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 @@ -34,7 +34,7 @@ fun Tab.TabContent() { Column { InnerTabNavigation() screen.Content() - Log.d("Navigator", "Last Event: ${navigator.lastEvent}") + Log.d("Navigator", "Last Event: ${navigator.lastAction.event}") } } } @@ -65,7 +65,7 @@ private fun RowScope.TabNavigationButton( Button( enabled = tabNavigator.current.key != tab.key, - onClick = { tabNavigator.current = tab }, + onClick = { tabNavigator.setCurrent(invoker = tabNavigator.current, newTab = tab) }, modifier = Modifier.weight(1f) ) { Text(text = tab.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/TransitionActivity.kt b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt new file mode 100644 index 00000000..ff77f6a2 --- /dev/null +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionActivity.kt @@ -0,0 +1,164 @@ +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.core.stack.StackEvent +import cafe.adriel.voyager.navigator.Navigator +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.lastAction.event == StackEvent.Push + val isPop = navigator.lastAction.event == StackEvent.Pop + // Define any Screen you want transition must be from + val isInvokerTransitionScreen = navigator.lastAction.invoker == TransitionScreen + val isInvokerFadeScreen = navigator.lastAction.invoker == FadeScreen + val isInvokerShrinkScreen = navigator.lastAction.invoker == ShrinkScreen + val isInvokerScaleScreen = navigator.lastAction.invoker == ScaleScreen + // Define any Screen you want transition must be to + val isTargetTransitionScreen = navigator.lastItem == TransitionScreen + val isTargetFadeScreen = navigator.lastItem == FadeScreen + val isTargetShrinkScreen = navigator.lastItem == ShrinkScreen + val isTargetScaleScreen = navigator.lastItem == 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..5cad0df5 --- /dev/null +++ b/samples/android/src/main/java/cafe/adriel/voyager/sample/transition/TransitionScreen.kt @@ -0,0 +1,60 @@ +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 +import cafe.adriel.voyager.navigator.push + +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-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt b/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt index 91384caf..590ac4d5 100644 --- a/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt +++ b/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt @@ -26,8 +26,10 @@ import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.annotation.InternalVoyagerApi import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.stack.Stack +import cafe.adriel.voyager.core.stack.WithLastActionStack import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.NavigatorOld import cafe.adriel.voyager.navigator.bottomSheet.internal.BottomSheetNavigatorBackHandler import cafe.adriel.voyager.navigator.compositionUniqueId import kotlinx.coroutines.CoroutineScope @@ -98,19 +100,57 @@ public fun BottomSheetNavigator( } } +@OptIn(ExperimentalMaterialApi::class) +public class BottomSheetNavigatorSimple @InternalVoyagerApi constructor( + private val navigator: NavigatorOld, + private val sheetState: ModalBottomSheetState, + private val coroutineScope: CoroutineScope +) : Stack by navigator { + + public val isVisible: Boolean + get() = sheetState.isVisible + + public fun show(screen: Screen) { + coroutineScope.launch { + navigator.replaceAll(screen) + sheetState.show() + } + } + + public fun hide() { + coroutineScope.launch { + if (isVisible) { + sheetState.hide() + navigator.replaceAll(HiddenBottomSheetScreen) + } else if (sheetState.targetValue == ModalBottomSheetValue.Hidden) { + // Swipe down - sheetState is already hidden here so `isVisible` is false + navigator.replaceAll(HiddenBottomSheetScreen) + } + } + } + + @Composable + public fun saveableState( + key: String, + content: @Composable () -> Unit + ) { + navigator.saveableState(key, content = content) + } +} + @OptIn(ExperimentalMaterialApi::class) public class BottomSheetNavigator @InternalVoyagerApi constructor( private val navigator: Navigator, private val sheetState: ModalBottomSheetState, private val coroutineScope: CoroutineScope -) : Stack by navigator { +) : WithLastActionStack by navigator { public val isVisible: Boolean get() = sheetState.isVisible public fun show(screen: Screen) { coroutineScope.launch { - replaceAll(screen) + navigator.replaceAll(navigator.lastItem, screen) sheetState.show() } } @@ -119,10 +159,10 @@ public class BottomSheetNavigator @InternalVoyagerApi constructor( coroutineScope.launch { if (isVisible) { sheetState.hide() - replaceAll(HiddenBottomSheetScreen) + navigator.replaceAll(navigator.lastItem, HiddenBottomSheetScreen) } else if (sheetState.targetValue == ModalBottomSheetValue.Hidden) { // Swipe down - sheetState is already hidden here so `isVisible` is false - replaceAll(HiddenBottomSheetScreen) + navigator.replaceAll(navigator.lastItem, HiddenBottomSheetScreen) } } } diff --git a/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/internal/BottomSheetNavigatorBackHandler.kt b/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/internal/BottomSheetNavigatorBackHandler.kt index bdb13b89..7858a944 100644 --- a/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/internal/BottomSheetNavigatorBackHandler.kt +++ b/voyager-bottom-sheet-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/bottomSheet/internal/BottomSheetNavigatorBackHandler.kt @@ -16,8 +16,10 @@ internal fun BottomSheetNavigatorBackHandler( hideOnBackPress: Boolean ) { BackHandler(enabled = sheetState.isVisible) { - if (navigator.pop().not() && hideOnBackPress) { - navigator.hide() + navigator.lastItemOrNull?.let { + if (navigator.pop(it).not() && hideOnBackPress) { + navigator.hide() + } } } } diff --git a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStatePropertyHolderStack.kt b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStatePropertyHolderStack.kt new file mode 100644 index 00000000..52f13df3 --- /dev/null +++ b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStatePropertyHolderStack.kt @@ -0,0 +1,44 @@ +package cafe.adriel.voyager.core.stack + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.toMutableStateList + +public open class SnapshotStatePropertyHolderStack( + items: List, + minSize: Int = 0 +) : PropertyHolderStack { + + init { + require(minSize >= 0) { "Min size $minSize is less than zero" } + require(items.size >= minSize) { "Stack size ${items.size} is less than the min size $minSize" } + } + + @PublishedApi + internal val stateStack: SnapshotStateList = items.toMutableStateList() + + public override val items: List by derivedStateOf { + stateStack.toList() + } + + public override val lastItemOrNull: Item? by derivedStateOf { + stateStack.lastOrNull() + } + + public override val size: Int by derivedStateOf { + stateStack.size + } + + public override val isEmpty: Boolean by derivedStateOf { + stateStack.isEmpty() + } + + public override val canPop: Boolean by derivedStateOf { + stateStack.size > minSize + } +} diff --git a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt index d6e7adb8..d6d9330c 100644 --- a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt +++ b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/SnapshotStateStack.kt @@ -1,7 +1,6 @@ package cafe.adriel.voyager.core.stack import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.neverEqualPolicy @@ -9,48 +8,35 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.runtime.toMutableStateList -public fun List.toMutableStateStack( +public fun List.toMutableSnapshotStateStack( minSize: Int = 0 -): SnapshotStateStack = - SnapshotStateStack(this, minSize) +): SnapshotStateStack = SnapshotStateStack(this, minSize) -public fun mutableStateStackOf( +public fun mutableSnapshotStateStackOf( vararg items: Item, minSize: Int = 0 -): SnapshotStateStack = - SnapshotStateStack(*items, minSize = minSize) +): SnapshotStateStack = SnapshotStateStack((items as List), minSize = minSize) @Composable -public fun rememberStateStack( - vararg items: Item, - minSize: Int = 0 -): SnapshotStateStack = - rememberStateStack(items.toList(), minSize) - -@Composable -public fun rememberStateStack( +public fun rememberSnapshotStateStack( items: List, minSize: Int = 0 -): SnapshotStateStack = - rememberSaveable(saver = stackSaver(minSize)) { - SnapshotStateStack(items, minSize) - } +): SnapshotStateStack = rememberSaveable(saver = stackSaver(minSize)) { + SnapshotStateStack(items, minSize) +} private fun stackSaver( minSize: Int -): Saver, Any> = - listSaver( - save = { stack -> stack.items }, - restore = { items -> SnapshotStateStack(items, minSize) } - ) +): Saver, Any> = listSaver( + save = { stack -> stack.items }, + restore = { items -> SnapshotStateStack(items, minSize) } +) public class SnapshotStateStack( items: List, minSize: Int = 0 -) : Stack { +) : SnapshotStatePropertyHolderStack(items, minSize), Stack { public constructor( vararg items: Item, @@ -60,37 +46,9 @@ public class SnapshotStateStack( minSize = minSize ) - init { - require(minSize >= 0) { "Min size $minSize is less than zero" } - require(items.size >= minSize) { "Stack size ${items.size} is less than the min size $minSize" } - } - - @PublishedApi - internal val stateStack: SnapshotStateList = items.toMutableStateList() - public override var lastEvent: StackEvent by mutableStateOf(StackEvent.Idle, neverEqualPolicy()) private set - public override val items: List by derivedStateOf { - stateStack.toList() - } - - public override val lastItemOrNull: Item? by derivedStateOf { - stateStack.lastOrNull() - } - - public override val size: Int by derivedStateOf { - stateStack.size - } - - public override val isEmpty: Boolean by derivedStateOf { - stateStack.isEmpty() - } - - public override val canPop: Boolean by derivedStateOf { - stateStack.size > minSize - } - public override infix fun push(item: Item) { stateStack += item lastEvent = StackEvent.Push @@ -131,10 +89,6 @@ public class SnapshotStateStack( false } - public override fun popAll() { - popUntil { false } - } - public override infix fun popUntil(predicate: (Item) -> Boolean): Boolean { var success = false val shouldPop = { @@ -154,6 +108,10 @@ public class SnapshotStateStack( return success } + public override fun popAll() { + popUntil { false } + } + public override operator fun plusAssign(item: Item) { push(item) } diff --git a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/Stack.kt b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/Stack.kt index bf65e8fd..0dcd2cfb 100644 --- a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/Stack.kt +++ b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/Stack.kt @@ -10,12 +10,10 @@ public enum class StackEvent { Idle } -public interface Stack { +public interface PropertyHolderStack { public val items: List - public val lastEvent: StackEvent - public val lastItemOrNull: Item? public val size: Int @@ -23,6 +21,11 @@ public interface Stack { public val isEmpty: Boolean public val canPop: Boolean +} + +public interface Stack : PropertyHolderStack { + + public val lastEvent: StackEvent public infix fun push(item: Item) @@ -34,15 +37,47 @@ public interface Stack { public infix fun replaceAll(items: List) + public infix fun popUntil(predicate: (Item) -> Boolean): Boolean + public fun pop(): Boolean public fun popAll() - public infix fun popUntil(predicate: (Item) -> Boolean): Boolean - public operator fun plusAssign(item: Item) public operator fun plusAssign(items: List) public fun clearEvent() } + +public data class StackLastAction( + val invoker: Item?, + val event: StackEvent, +) + +/** + * A [PropertyHolderStack] a stack that keeps track of the last action performed in it. + * Crucial API difference from [Stack] is that this interface can't perform infix or operator functions. + */ +public interface WithLastActionStack : PropertyHolderStack { + + public val lastAction: StackLastAction + + public fun push(invoker: Item, item: Item) + + public fun push(invoker: Item, items: List) + + public fun replace(invoker: Item, item: Item) + + public fun replaceAll(invoker: Item, item: Item) + + public fun replaceAll(invoker: Item, items: List) + + public fun pop(invoker: Item): Boolean + + public fun popUntil(invoker: Item, predicate: (Item) -> Boolean): Boolean + + public fun popAll(invoker: Item) + + public fun clearEvent(invoker: Item) +} diff --git a/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/WithLastActionSnapshotStackState.kt b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/WithLastActionSnapshotStackState.kt new file mode 100644 index 00000000..353c33e9 --- /dev/null +++ b/voyager-core/src/commonMain/kotlin/cafe/adriel/voyager/core/stack/WithLastActionSnapshotStackState.kt @@ -0,0 +1,127 @@ +package cafe.adriel.voyager.core.stack + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.neverEqualPolicy +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue + +public fun List.toMutableStateStack( + minSize: Int = 0 +): WithLastActionSnapshotStackState = WithLastActionSnapshotStackState(this, minSize) + +public fun mutableStateStackOf( + vararg items: Item, + minSize: Int = 0 +): WithLastActionSnapshotStackState = WithLastActionSnapshotStackState((items as List), minSize = minSize) + +@Composable +public fun rememberStateStack( + vararg items: Item, + minSize: Int = 0 +): WithLastActionSnapshotStackState = rememberStateStack(items.toList(), minSize) + +@Composable +public fun rememberStateStack( + items: List, + minSize: Int = 0 +): WithLastActionSnapshotStackState = rememberSaveable(saver = stackSaver(minSize)) { + WithLastActionSnapshotStackState(items, minSize) +} + +private fun stackSaver( + minSize: Int +): Saver, Any> = listSaver( + save = { stack -> stack.items }, + restore = { items -> WithLastActionSnapshotStackState(items, minSize) } +) + +public class WithLastActionSnapshotStackState( + items: List, + minSize: Int = 1 +) : SnapshotStatePropertyHolderStack(items, minSize), WithLastActionStack { + + public constructor( + vararg items: Item, + minSize: Int = 0 + ) : this( + items = items.toList(), + minSize = minSize + ) + + public override var lastAction: StackLastAction by mutableStateOf( + value = StackLastAction(null, StackEvent.Idle), policy = neverEqualPolicy() + ) + private set + + override fun push(invoker: Item, item: Item) { + stateStack += item + lastAction = StackLastAction(invoker, StackEvent.Push) + } + + override fun push(invoker: Item, items: List) { + stateStack += items + lastAction = StackLastAction(invoker, StackEvent.Push) + } + + override fun replace(invoker: Item, item: Item) { + if (stateStack.isEmpty()) { + push(invoker, item) + } else { + stateStack[stateStack.lastIndex] = item + } + lastAction = StackLastAction(invoker, StackEvent.Replace) + } + + override fun replaceAll(invoker: Item, item: Item) { + stateStack.clear() + stateStack += item + lastAction = StackLastAction(invoker, StackEvent.Replace) + } + + override fun replaceAll(invoker: Item, items: List) { + stateStack.clear() + stateStack += items + lastAction = StackLastAction(invoker, StackEvent.Replace) + } + + override fun pop(invoker: Item): Boolean { + return if (canPop) { + stateStack.removeLast() + lastAction = StackLastAction(invoker, StackEvent.Pop) + true + } else { + false + } + } + + override fun popUntil(invoker: Item, predicate: (Item) -> Boolean): Boolean { + var success = false + val shouldPop = { + lastItemOrNull + ?.let(predicate) + ?.also { success = it } + ?.not() + ?: false + } + + while (canPop && shouldPop()) { + stateStack.removeLast() + } + + lastAction = StackLastAction(invoker, StackEvent.Pop) + + return success + } + + override fun popAll(invoker: Item) { + popUntil(invoker) { false } + } + + override fun clearEvent(invoker: Item) { + lastAction = StackLastAction(invoker, StackEvent.Idle) + } +} diff --git a/voyager-navigator/build.gradle.kts b/voyager-navigator/build.gradle.kts index b717c6eb..1b1a8be8 100644 --- a/voyager-navigator/build.gradle.kts +++ b/voyager-navigator/build.gradle.kts @@ -35,3 +35,7 @@ kotlin { } } } + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).all { + kotlinOptions.freeCompilerArgs = listOf("-Xcontext-receivers") +} diff --git a/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorComposeUtils.kt b/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorComposeUtils.kt new file mode 100644 index 00000000..15dccbcd --- /dev/null +++ b/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorComposeUtils.kt @@ -0,0 +1,50 @@ +package cafe.adriel.voyager.navigator + +import cafe.adriel.voyager.core.screen.Screen + +public fun Screen.self() : Screen = this + +context(Screen) +public fun Navigator.push(screen: Screen) { + push(self(), screen) +} + +context(Screen) +public fun Navigator.push(screens: List) { + push(self(), screens) +} + +context(Screen) +public fun Navigator.replace(screen: Screen) { + replace(self(), screen) +} + +context(Screen) +public fun Navigator.replaceAll(screen: Screen) { + replaceAll(self(), screen) +} + +context(Screen) +public fun Navigator.replaceAll(screens: List) { + replaceAll(self(), screens) +} + +context(Screen) +public fun Navigator.pop() { + pop(self()) +} + +context(Screen) +public fun Navigator.popUntil(predicate: (Screen) -> Boolean) { + popUntil(self(), predicate) +} + +context(Screen) +public fun Navigator.popAll() { + popAll(self()) +} + +context(Screen) +public fun Navigator.clearEvent() { + clearEvent(self()) +} diff --git a/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.android.kt b/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.android.kt index 4c2f7b18..61141f7e 100644 --- a/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.android.kt +++ b/voyager-navigator/src/androidMain/kotlin/cafe/adriel/voyager/navigator/NavigatorSaver.android.kt @@ -3,6 +3,7 @@ package cafe.adriel.voyager.navigator import android.os.Parcelable import androidx.compose.runtime.saveable.listSaver import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.stack.WithLastActionStack /** * Navigator Saver that forces all Screens be [Parcelable], if not, it will throw a exception while trying to save 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..2c0c7ec5 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 @@ -18,8 +18,10 @@ import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleStore 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.SnapshotStateStack import cafe.adriel.voyager.core.stack.Stack -import cafe.adriel.voyager.core.stack.toMutableStateStack +import cafe.adriel.voyager.core.stack.WithLastActionSnapshotStackState +import cafe.adriel.voyager.core.stack.WithLastActionStack import cafe.adriel.voyager.navigator.internal.ChildrenNavigationDisposableEffect import cafe.adriel.voyager.navigator.internal.LocalNavigatorStateHolder import cafe.adriel.voyager.navigator.internal.NavigatorBackHandler @@ -108,7 +110,7 @@ public class Navigator @InternalVoyagerApi constructor( private val stateHolder: SaveableStateHolder, public val disposeBehavior: NavigatorDisposeBehavior, public val parent: Navigator? = null -) : Stack by screens.toMutableStateStack(minSize = 1) { +) : WithLastActionStack by WithLastActionSnapshotStackState(screens, 1) { public val level: Int = parent?.level?.inc() ?: 0 @@ -152,15 +154,15 @@ public class Navigator @InternalVoyagerApi constructor( ) } - public fun popUntilRoot() { - popUntilRoot(this) + public fun popUntilRoot(invoker: Screen) { + popUntilRoot(invoker, this) } - private tailrec fun popUntilRoot(navigator: Navigator) { - navigator.popAll() + private tailrec fun popUntilRoot(invoker: Screen, navigator: Navigator) { + navigator.popAll(invoker) if (navigator.parent != null) { - popUntilRoot(navigator.parent) + popUntilRoot(invoker, navigator.parent) } } diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorOld.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorOld.kt new file mode 100644 index 00000000..e24e0364 --- /dev/null +++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/NavigatorOld.kt @@ -0,0 +1,96 @@ +package cafe.adriel.voyager.navigator + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.SaveableStateHolder +import cafe.adriel.voyager.core.annotation.InternalVoyagerApi +import cafe.adriel.voyager.core.concurrent.ThreadSafeMap +import cafe.adriel.voyager.core.concurrent.ThreadSafeSet +import cafe.adriel.voyager.core.lifecycle.MultipleProvideBeforeScreenContent +import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleStore +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.SnapshotStateStack +import cafe.adriel.voyager.core.stack.Stack +import cafe.adriel.voyager.navigator.lifecycle.NavigatorKey + +public class NavigatorOld @InternalVoyagerApi constructor( + screens: List, + @InternalVoyagerApi public val key: String, + private val stateHolder: SaveableStateHolder, + public val disposeBehavior: NavigatorDisposeBehavior, + public val parent: NavigatorOld? = null +) : Stack by SnapshotStateStack(screens, 0) { + + public val level: Int = + parent?.level?.inc() ?: 0 + + public val lastItem: Screen by derivedStateOf { + lastItemOrNull ?: error("Navigator has no screen") + } + + private val stateKeys = ThreadSafeSet() + + internal val children = ThreadSafeMap() + + @Composable + public fun saveableState( + key: String, + screen: Screen = lastItem, + content: @Composable () -> Unit + ) { + val stateKey = "${screen.key}:$key" + stateKeys += stateKey + + @Composable + fun provideSaveableState(suffixKey: String, content: @Composable () -> Unit) { + val providedStateKey = "$stateKey:$suffixKey" + stateKeys += providedStateKey + stateHolder.SaveableStateProvider(providedStateKey, content) + } + + val lifecycleOwner = rememberScreenLifecycleOwner(screen) + val navigatorScreenLifecycleOwners = getNavigatorScreenLifecycleProvider(screen) + + val composed = remember(lifecycleOwner, navigatorScreenLifecycleOwners) { + listOf(lifecycleOwner) + navigatorScreenLifecycleOwners + } + MultipleProvideBeforeScreenContent( + screenLifecycleContentProviders = composed, + provideSaveableState = { suffix, content -> provideSaveableState(suffix, content) }, + content = { + stateHolder.SaveableStateProvider(stateKey, content) + } + ) + } + + public fun popUntilRoot() { + popUntilRoot(this) + } + + private tailrec fun popUntilRoot(navigator: NavigatorOld) { + navigator.popAll() + + if (navigator.parent != null) { + popUntilRoot(navigator.parent) + } + } + + @InternalVoyagerApi + public fun dispose( + screen: Screen + ) { + ScreenLifecycleStore.remove(screen) + stateKeys + .toSet() // Copy + .asSequence() + .filter { it.startsWith(screen.key) } + .forEach { key -> + stateHolder.removeState(key) + stateKeys -= key + } + } +} diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt index 6eca94e9..ad9205ce 100644 --- a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt +++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt @@ -16,9 +16,11 @@ internal fun NavigatorBackHandler( BackHandler( enabled = navigator.canPop || navigator.parent?.canPop ?: false, onBack = { - if (onBackPressed(navigator.lastItem)) { - if (navigator.pop().not()) { - navigator.parent?.pop() + with(navigator) { + if (onBackPressed(lastItem)) { + if (pop(lastItem).not()) { + parent?.pop(lastItem) + } } } } diff --git a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt index 81408884..12844b46 100644 --- a/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt +++ b/voyager-navigator/src/commonMain/kotlin/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt @@ -30,11 +30,11 @@ internal fun StepDisposableEffect( DisposableEffect(currentScreens) { onDispose { val newScreenKeys = navigator.items.map { it.key } - if (navigator.lastEvent in disposableEvents) { + if (navigator.lastAction.event in disposableEvents) { currentScreens.filter { it.key !in newScreenKeys }.forEach { navigator.dispose(it) } - navigator.clearEvent() + navigator.clearEvent(navigator.lastItem) } } } @@ -78,5 +78,5 @@ internal fun disposeNavigator(navigator: Navigator) { navigator.dispose(screen) } NavigatorLifecycleStore.remove(navigator) - navigator.clearEvent() + navigator.clearEvent(navigator.lastItem) } 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..07cf37e7 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 @@ -61,7 +61,12 @@ public class TabNavigator internal constructor( public var current: Tab get() = navigator.lastItem as Tab - set(tab) = navigator.replaceAll(tab) + private set(value) {} + + public fun setCurrent(invoker: Tab, newTab: Tab) { + navigator.replaceAll(invoker, newTab) + current = newTab + } @Composable public fun saveableState( diff --git a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScaleTransition.kt b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScaleTransition.kt index ee957af1..bc920f6d 100644 --- a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScaleTransition.kt +++ b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScaleTransition.kt @@ -26,7 +26,7 @@ public fun ScaleTransition( modifier = modifier, content = content, transition = { - val (initialScale, targetScale) = when (navigator.lastEvent) { + val (initialScale, targetScale) = when (navigator.lastAction.event) { StackEvent.Pop -> ExitScales else -> EnterScales } diff --git a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScreenTransition.kt b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScreenTransition.kt index c09bb2d8..6eb9c7fa 100644 --- a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScreenTransition.kt +++ b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/ScreenTransition.kt @@ -25,7 +25,7 @@ public fun ScreenTransition( modifier = modifier, content = content, transition = { - when (navigator.lastEvent) { + when (navigator.lastAction.event) { StackEvent.Pop -> exitTransition() else -> enterTransition() } diff --git a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/SlideTransition.kt b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/SlideTransition.kt index efb5311b..668dfe39 100644 --- a/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/SlideTransition.kt +++ b/voyager-transitions/src/commonMain/kotlin/cafe/adriel/voyager/transitions/SlideTransition.kt @@ -12,6 +12,7 @@ import androidx.compose.animation.togetherWith import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset +import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.stack.StackEvent import cafe.adriel.voyager.navigator.Navigator @@ -31,7 +32,7 @@ public fun SlideTransition( modifier = modifier, content = content, transition = { - val (initialOffset, targetOffset) = when (navigator.lastEvent) { + val (initialOffset, targetOffset) = when (navigator.lastAction.event) { StackEvent.Pop -> ({ size: Int -> -size }) to ({ size: Int -> size }) else -> ({ size: Int -> size }) to ({ size: Int -> -size }) } @@ -39,10 +40,10 @@ public fun SlideTransition( when (orientation) { SlideOrientation.Horizontal -> slideInHorizontally(animationSpec, initialOffset) togetherWith - slideOutHorizontally(animationSpec, targetOffset) + slideOutHorizontally(animationSpec, targetOffset) SlideOrientation.Vertical -> slideInVertically(animationSpec, initialOffset) togetherWith - slideOutVertically(animationSpec, targetOffset) + slideOutVertically(animationSpec, targetOffset) } } )