summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SettingsLib/Spa/spa/Android.bp1
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt44
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt79
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt267
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt118
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt41
6 files changed, 545 insertions, 5 deletions
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
index 40613ceaaf5f..139f3e13d5bc 100644
--- a/packages/SettingsLib/Spa/spa/Android.bp
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -27,6 +27,7 @@ android_library {
"androidx.slice_slice-builders",
"androidx.slice_slice-core",
"androidx.slice_slice-view",
+ "androidx.compose.animation_animation",
"androidx.compose.material3_material3",
"androidx.compose.material_material-icons-extended",
"androidx.compose.runtime_runtime",
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
index aa10cc82a14e..a81e2e330b0f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalAnimationApi::class)
+
package com.android.settingslib.spa.framework
import android.content.Intent
@@ -21,25 +23,31 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.VisibleForTesting
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.unit.IntOffset
import androidx.core.view.WindowCompat
import androidx.navigation.NavGraph.Companion.findStartDestination
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.rememberNavController
import com.android.settingslib.spa.R
import com.android.settingslib.spa.framework.common.LogCategory
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.compose.AnimatedNavHost
import com.android.settingslib.spa.framework.compose.LocalNavController
import com.android.settingslib.spa.framework.compose.NavControllerWrapperImpl
+import com.android.settingslib.spa.framework.compose.composable
import com.android.settingslib.spa.framework.compose.localNavController
+import com.android.settingslib.spa.framework.compose.rememberAnimatedNavController
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.framework.util.PageEvent
import com.android.settingslib.spa.framework.util.getDestination
@@ -86,7 +94,7 @@ open class BrowseActivity : ComponentActivity() {
@VisibleForTesting
@Composable
fun BrowseContent(sppRepository: SettingsPageProviderRepository, initialIntent: Intent? = null) {
- val navController = rememberNavController()
+ val navController = rememberAnimatedNavController()
CompositionLocalProvider(navController.localNavController()) {
val controller = LocalNavController.current as NavControllerWrapperImpl
controller.NavContent(sppRepository.getAllProviders())
@@ -97,15 +105,41 @@ fun BrowseContent(sppRepository: SettingsPageProviderRepository, initialIntent:
@Composable
private fun NavControllerWrapperImpl.NavContent(allProvider: Collection<SettingsPageProvider>) {
val nullPage = SettingsPage.createNull()
- NavHost(
+ AnimatedNavHost(
navController = navController,
startDestination = nullPage.sppName,
) {
+ val slideEffect = tween<IntOffset>(durationMillis = 300)
+ val fadeEffect = tween<Float>(durationMillis = 300)
composable(nullPage.sppName) {}
for (spp in allProvider) {
composable(
route = spp.name + spp.parameter.navRoute(),
arguments = spp.parameter,
+ enterTransition = {
+ slideIntoContainer(
+ AnimatedContentScope.SlideDirection.Left,
+ animationSpec = slideEffect
+ ) + fadeIn(animationSpec = fadeEffect)
+ },
+ exitTransition = {
+ slideOutOfContainer(
+ AnimatedContentScope.SlideDirection.Left,
+ animationSpec = slideEffect
+ ) + fadeOut(animationSpec = fadeEffect)
+ },
+ popEnterTransition = {
+ slideIntoContainer(
+ AnimatedContentScope.SlideDirection.Right,
+ animationSpec = slideEffect
+ ) + fadeIn(animationSpec = fadeEffect)
+ },
+ popExitTransition = {
+ slideOutOfContainer(
+ AnimatedContentScope.SlideDirection.Right,
+ animationSpec = slideEffect
+ ) + fadeOut(animationSpec = fadeEffect)
+ },
) { navBackStackEntry ->
spp.PageEvent(navBackStackEntry.arguments)
spp.Page(navBackStackEntry.arguments)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt
new file mode 100644
index 000000000000..930a83f76e3f
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDestination
+import androidx.navigation.NavOptions
+import androidx.navigation.Navigator
+
+/**
+ * Navigator that navigates through [Composable]s. Every destination using this Navigator must
+ * set a valid [Composable] by setting it directly on an instantiated [Destination] or calling
+ * [composable].
+ */
+@ExperimentalAnimationApi
+@Navigator.Name("animatedComposable")
+public class AnimatedComposeNavigator : Navigator<AnimatedComposeNavigator.Destination>() {
+ internal val transitionsInProgress get() = state.transitionsInProgress
+
+ internal val backStack get() = state.backStack
+
+ internal val isPop = mutableStateOf(false)
+
+ override fun navigate(
+ entries: List<NavBackStackEntry>,
+ navOptions: NavOptions?,
+ navigatorExtras: Extras?
+ ) {
+ entries.forEach { entry ->
+ state.pushWithTransition(entry)
+ }
+ isPop.value = false
+ }
+
+ override fun createDestination(): Destination {
+ return Destination(this, content = { })
+ }
+
+ override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
+ state.popWithTransition(popUpTo, savedState)
+ isPop.value = true
+ }
+
+ internal fun markTransitionComplete(entry: NavBackStackEntry) {
+ state.markTransitionComplete(entry)
+ }
+
+ /**
+ * NavDestination specific to [AnimatedComposeNavigator]
+ */
+ @ExperimentalAnimationApi
+ @NavDestination.ClassType(Composable::class)
+ public class Destination(
+ navigator: AnimatedComposeNavigator,
+ internal val content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
+ ) : NavDestination(navigator)
+
+ internal companion object {
+ internal const val NAME = "animatedComposable"
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt
new file mode 100644
index 000000000000..013757282427
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.with
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDestination
+import androidx.navigation.NavDestination.Companion.hierarchy
+import androidx.navigation.NavGraph
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavHostController
+import androidx.navigation.Navigator
+import androidx.navigation.compose.DialogHost
+import androidx.navigation.compose.DialogNavigator
+import androidx.navigation.compose.LocalOwnersProvider
+import androidx.navigation.createGraph
+import androidx.navigation.get
+import kotlinx.coroutines.flow.map
+
+/**
+ * Provides in place in the Compose hierarchy for self contained navigation to occur.
+ *
+ * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
+ * the provided [navController].
+ *
+ * The builder passed into this method is [remember]ed. This means that for this NavHost, the
+ * contents of the builder cannot be changed.
+ *
+ * @param navController the navController for this host
+ * @param startDestination the route for the start destination
+ * @param modifier The modifier to be applied to the layout.
+ * @param route the route for the graph
+ * @param enterTransition callback to define enter transitions for destination in this host
+ * @param exitTransition callback to define exit transitions for destination in this host
+ * @param popEnterTransition callback to define popEnter transitions for destination in this host
+ * @param popExitTransition callback to define popExit transitions for destination in this host
+ * @param builder the builder used to construct the graph
+ */
+@Composable
+@ExperimentalAnimationApi
+public fun AnimatedNavHost(
+ navController: NavHostController,
+ startDestination: String,
+ modifier: Modifier = Modifier,
+ contentAlignment: Alignment = Alignment.Center,
+ route: String? = null,
+ enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) =
+ { fadeIn(animationSpec = tween(700)) },
+ exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) =
+ { fadeOut(animationSpec = tween(700)) },
+ popEnterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) =
+ enterTransition,
+ popExitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) =
+ exitTransition,
+ builder: NavGraphBuilder.() -> Unit
+) {
+ AnimatedNavHost(
+ navController,
+ remember(route, startDestination, builder) {
+ navController.createGraph(startDestination, route, builder)
+ },
+ modifier,
+ contentAlignment,
+ enterTransition,
+ exitTransition,
+ popEnterTransition,
+ popExitTransition
+ )
+}
+
+/**
+ * Provides in place in the Compose hierarchy for self contained navigation to occur.
+ *
+ * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
+ * the provided [navController].
+ *
+ * @param navController the navController for this host
+ * @param graph the graph for this host
+ * @param modifier The modifier to be applied to the layout.
+ * @param enterTransition callback to define enter transitions for destination in this host
+ * @param exitTransition callback to define exit transitions for destination in this host
+ * @param popEnterTransition callback to define popEnter transitions for destination in this host
+ * @param popExitTransition callback to define popExit transitions for destination in this host
+ */
+@ExperimentalAnimationApi
+@Composable
+public fun AnimatedNavHost(
+ navController: NavHostController,
+ graph: NavGraph,
+ modifier: Modifier = Modifier,
+ contentAlignment: Alignment = Alignment.Center,
+ enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) =
+ { fadeIn(animationSpec = tween(700)) },
+ exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) =
+ { fadeOut(animationSpec = tween(700)) },
+ popEnterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) =
+ enterTransition,
+ popExitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) =
+ exitTransition,
+) {
+
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
+ "NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"
+ }
+ val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
+ val onBackPressedDispatcher = onBackPressedDispatcherOwner?.onBackPressedDispatcher
+
+ // on successful recompose we setup the navController with proper inputs
+ // after the first time, this will only happen again if one of the inputs changes
+ navController.setLifecycleOwner(lifecycleOwner)
+ navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
+ if (onBackPressedDispatcher != null) {
+ navController.setOnBackPressedDispatcher(onBackPressedDispatcher)
+ }
+
+ navController.graph = graph
+
+ val saveableStateHolder = rememberSaveableStateHolder()
+
+ // Find the ComposeNavigator, returning early if it isn't found
+ // (such as is the case when using TestNavHostController)
+ val composeNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
+ AnimatedComposeNavigator.NAME
+ ) as? AnimatedComposeNavigator ?: return
+ val visibleEntries by remember(navController.visibleEntries) {
+ navController.visibleEntries.map {
+ it.filter { entry ->
+ entry.destination.navigatorName == AnimatedComposeNavigator.NAME
+ }
+ }
+ }.collectAsState(emptyList())
+
+ val backStackEntry = visibleEntries.lastOrNull()
+
+ if (backStackEntry != null) {
+ val finalEnter: AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition = {
+ val targetDestination = targetState.destination as AnimatedComposeNavigator.Destination
+
+ if (composeNavigator.isPop.value) {
+ targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
+ popEnterTransitions[destination.route]?.invoke(this)
+ } ?: popEnterTransition.invoke(this)
+ } else {
+ targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
+ enterTransitions[destination.route]?.invoke(this)
+ } ?: enterTransition.invoke(this)
+ }
+ }
+
+ val finalExit: AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition = {
+ val initialDestination =
+ initialState.destination as AnimatedComposeNavigator.Destination
+
+ if (composeNavigator.isPop.value) {
+ initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
+ popExitTransitions[destination.route]?.invoke(this)
+ } ?: popExitTransition.invoke(this)
+ } else {
+ initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
+ exitTransitions[destination.route]?.invoke(this)
+ } ?: exitTransition.invoke(this)
+ }
+ }
+
+ val transition = updateTransition(backStackEntry, label = "entry")
+ transition.AnimatedContent(
+ modifier,
+ transitionSpec = {
+ val zIndex = if (composeNavigator.isPop.value) {
+ visibleEntries.indexOf(initialState).toFloat()
+ } else {
+ visibleEntries.indexOf(targetState).toFloat()
+ }
+ // If the initialState of the AnimatedContent is not in visibleEntries, we are in
+ // a case where visible has cleared the old state for some reason, so instead of
+ // attempting to animate away from the initialState, we skip the animation.
+ if (initialState in visibleEntries) {
+ ContentTransform(finalEnter(this), finalExit(this), zIndex)
+ } else {
+ EnterTransition.None with ExitTransition.None
+ }
+ },
+ contentAlignment,
+ contentKey = { it.id }
+ ) {
+ // In some specific cases, such as clearing your back stack by changing your
+ // start destination, AnimatedContent can contain an entry that is no longer
+ // part of visible entries since it was cleared from the back stack and is not
+ // animating. In these cases the currentEntry will be null, and in those cases,
+ // AnimatedContent will just skip attempting to transition the old entry.
+ // See https://issuetracker.google.com/238686802
+ val currentEntry = visibleEntries.lastOrNull { entry ->
+ it == entry
+ }
+ // while in the scope of the composable, we provide the navBackStackEntry as the
+ // ViewModelStoreOwner and LifecycleOwner
+ currentEntry?.LocalOwnersProvider(saveableStateHolder) {
+ (currentEntry.destination as AnimatedComposeNavigator.Destination)
+ .content(this, currentEntry)
+ }
+ }
+ if (transition.currentState == transition.targetState) {
+ visibleEntries.forEach { entry ->
+ composeNavigator.markTransitionComplete(entry)
+ }
+ }
+ }
+
+ val dialogNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
+ "dialog"
+ ) as? DialogNavigator ?: return
+
+ // Show any dialog destinations
+ DialogHost(dialogNavigator)
+}
+
+@ExperimentalAnimationApi
+internal val enterTransitions =
+ mutableMapOf<String?,
+ (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?>()
+
+@ExperimentalAnimationApi
+internal val exitTransitions =
+ mutableMapOf<String?, (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?>()
+
+@ExperimentalAnimationApi
+internal val popEnterTransitions =
+ mutableMapOf<String?, (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?>()
+
+@ExperimentalAnimationApi
+internal val popExitTransitions =
+ mutableMapOf<String?, (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?>()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt
new file mode 100644
index 000000000000..9e58603bbaff
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.runtime.Composable
+import androidx.navigation.NamedNavArgument
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDeepLink
+import androidx.navigation.NavGraph
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.navigation
+import androidx.navigation.get
+
+/**
+ * Add the [Composable] to the [NavGraphBuilder]
+ *
+ * @param route route for the destination
+ * @param arguments list of arguments to associate with destination
+ * @param deepLinks list of deep links to associate with the destinations
+ * @param enterTransition callback to determine the destination's enter transition
+ * @param exitTransition callback to determine the destination's exit transition
+ * @param popEnterTransition callback to determine the destination's popEnter transition
+ * @param popExitTransition callback to determine the destination's popExit transition
+ * @param content composable for the destination
+ */
+@ExperimentalAnimationApi
+public fun NavGraphBuilder.composable(
+ route: String,
+ arguments: List<NamedNavArgument> = emptyList(),
+ deepLinks: List<NavDeepLink> = emptyList(),
+ enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)? = null,
+ exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)? = null,
+ popEnterTransition: (
+ AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?
+ )? = enterTransition,
+ popExitTransition: (
+ AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?
+ )? = exitTransition,
+ content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
+) {
+ addDestination(
+ AnimatedComposeNavigator.Destination(
+ provider[AnimatedComposeNavigator::class],
+ content
+ ).apply {
+ this.route = route
+ arguments.forEach { (argumentName, argument) ->
+ addArgument(argumentName, argument)
+ }
+ deepLinks.forEach { deepLink ->
+ addDeepLink(deepLink)
+ }
+ enterTransition?.let { enterTransitions[route] = enterTransition }
+ exitTransition?.let { exitTransitions[route] = exitTransition }
+ popEnterTransition?.let { popEnterTransitions[route] = popEnterTransition }
+ popExitTransition?.let { popExitTransitions[route] = popExitTransition }
+ }
+ )
+}
+
+/**
+ * Construct a nested [NavGraph]
+ *
+ * @param startDestination the starting destination's route for this NavGraph
+ * @param route the destination's unique route
+ * @param arguments list of arguments to associate with destination
+ * @param deepLinks list of deep links to associate with the destinations
+ * @param enterTransition callback to define enter transitions for destination in this NavGraph
+ * @param exitTransition callback to define exit transitions for destination in this NavGraph
+ * @param popEnterTransition callback to define pop enter transitions for destination in this
+ * NavGraph
+ * @param popExitTransition callback to define pop exit transitions for destination in this NavGraph
+ * @param builder the builder used to construct the graph
+ *
+ * @return the newly constructed nested NavGraph
+ */
+@ExperimentalAnimationApi
+public fun NavGraphBuilder.navigation(
+ startDestination: String,
+ route: String,
+ arguments: List<NamedNavArgument> = emptyList(),
+ deepLinks: List<NavDeepLink> = emptyList(),
+ enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)? = null,
+ exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)? = null,
+ popEnterTransition: (
+ AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?
+ )? = enterTransition,
+ popExitTransition: (
+ AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?
+ )? = exitTransition,
+ builder: NavGraphBuilder.() -> Unit
+) {
+ navigation(startDestination, route, arguments, deepLinks, builder).apply {
+ enterTransition?.let { enterTransitions[route] = enterTransition }
+ exitTransition?.let { exitTransitions[route] = exitTransition }
+ popEnterTransition?.let { popEnterTransitions[route] = popEnterTransition }
+ popExitTransition?.let { popExitTransitions[route] = popExitTransition }
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt
new file mode 100644
index 000000000000..a8ac86c2fb15
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.navigation.NavDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.Navigator
+import androidx.navigation.compose.rememberNavController
+
+/**
+ * Creates a NavHostController that handles the adding of the [ComposeNavigator], [DialogNavigator]
+ * and [AnimatedComposeNavigator]. Additional [androidx.navigation.Navigator] instances should be
+ * added in a [androidx.compose.runtime.SideEffect] block.
+ *
+ * @see AnimatedNavHost
+ */
+@ExperimentalAnimationApi
+@Composable
+fun rememberAnimatedNavController(
+ vararg navigators: Navigator<out NavDestination>
+): NavHostController {
+ val animatedNavigator = remember { AnimatedComposeNavigator() }
+ return rememberNavController(animatedNavigator, *navigators)
+}