Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 40e160e4 authored by Chaohui Wang's avatar Chaohui Wang Committed by Automerger Merge Worker
Browse files

Migrate from AnimatedNavHost am: 93cb527c am: 36f9dc9c

parents cf8f7bf3 36f9dc9c
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -62,7 +62,7 @@ dependencies {
    api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion")
    api("androidx.lifecycle:lifecycle-livedata-ktx")
    api("androidx.lifecycle:lifecycle-runtime-compose")
    api("androidx.navigation:navigation-compose:2.6.0-alpha08")
    api("androidx.navigation:navigation-compose:2.7.0-beta01")
    api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha")
    api("com.google.android.material:material:1.7.0-alpha03")
    debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion")
+7 −40
Original line number Diff line number Diff line
@@ -14,8 +14,6 @@
 * limitations under the License.
 */

@file:OptIn(ExperimentalAnimationApi::class)

package com.android.settingslib.spa.framework

import android.content.Intent
@@ -24,20 +22,17 @@ import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.AnimatedContentTransitionScope
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.remember
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.NullPageProvider
@@ -46,12 +41,10 @@ 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.common.createSettingsPage
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.animatedComposable
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.PageLogger
import com.android.settingslib.spa.framework.util.getDestination
@@ -108,7 +101,7 @@ internal fun BrowseContent(
    isPageEnabled: (SettingsPage) -> Boolean,
    initialIntent: Intent?,
) {
    val navController = rememberAnimatedNavController()
    val navController = rememberNavController()
    CompositionLocalProvider(navController.localNavController()) {
        val controller = LocalNavController.current as NavControllerWrapperImpl
        controller.NavContent(sppRepository.getAllProviders()) { page ->
@@ -133,41 +126,15 @@ private fun NavControllerWrapperImpl.NavContent(
    allProvider: Collection<SettingsPageProvider>,
    content: @Composable (SettingsPage) -> Unit,
) {
    AnimatedNavHost(
    NavHost(
        navController = navController,
        startDestination = NullPageProvider.name,
    ) {
        val slideEffect = tween<IntOffset>(durationMillis = 300)
        val fadeEffect = tween<Float>(durationMillis = 300)
        composable(NullPageProvider.name) {}
        for (spp in allProvider) {
            composable(
            animatedComposable(
                route = spp.name + spp.parameter.navRoute(),
                arguments = spp.parameter,
                enterTransition = {
                    slideIntoContainer(
                        AnimatedContentTransitionScope.SlideDirection.Start,
                        animationSpec = slideEffect
                    ) + fadeIn(animationSpec = fadeEffect)
                },
                exitTransition = {
                    slideOutOfContainer(
                        AnimatedContentTransitionScope.SlideDirection.Start,
                        animationSpec = slideEffect
                    ) + fadeOut(animationSpec = fadeEffect)
                },
                popEnterTransition = {
                    slideIntoContainer(
                        AnimatedContentTransitionScope.SlideDirection.End,
                        animationSpec = slideEffect
                    ) + fadeIn(animationSpec = fadeEffect)
                },
                popExitTransition = {
                    slideOutOfContainer(
                        AnimatedContentTransitionScope.SlideDirection.End,
                        animationSpec = slideEffect
                    ) + fadeOut(animationSpec = fadeEffect)
                },
            ) { navBackStackEntry ->
                val page = remember { spp.createSettingsPage(navBackStackEntry.arguments) }
                content(page)
+0 −79
Original line number Diff line number Diff line
/*
 * 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"
    }
}
+100 −0
Original line number Diff line number Diff line
@@ -16,56 +16,85 @@

package com.android.settingslib.spa.framework.compose

import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.IntOffset
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDeepLink
import androidx.navigation.NavGraphBuilder
import androidx.navigation.get
import androidx.navigation.compose.composable

/**
 * Add the [Composable] to the [NavGraphBuilder]
 * Add the [Composable] to the [NavGraphBuilder] with animation
 *
 * @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(
internal fun NavGraphBuilder.animatedComposable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    enterTransition: (AnimatedScope.() -> EnterTransition?)? = null,
    exitTransition: (AnimatedScope.() -> ExitTransition?)? = null,
    popEnterTransition: (AnimatedScope.() -> EnterTransition?)? = enterTransition,
    popExitTransition: (AnimatedScope.() -> 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 }
        }
    content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) = composable(
    route = route,
    arguments = arguments,
    deepLinks = deepLinks,
    enterTransition = {
        slideIntoContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.Start,
            animationSpec = slideInEffect,
            initialOffset = offsetFunc,
        ) + fadeIn(animationSpec = fadeInEffect)
    },
    exitTransition = {
        slideOutOfContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.Start,
            animationSpec = slideOutEffect,
            targetOffset = offsetFunc,
        ) + fadeOut(animationSpec = fadeOutEffect)
    },
    popEnterTransition = {
        slideIntoContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.End,
            animationSpec = slideInEffect,
            initialOffset = offsetFunc,
        ) + fadeIn(animationSpec = fadeInEffect)
    },
    popExitTransition = {
        slideOutOfContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.End,
            animationSpec = slideOutEffect,
            targetOffset = offsetFunc,
        ) + fadeOut(animationSpec = fadeOutEffect)
    },
    content = content,
)
}

private const val FADE_OUT_MILLIS = 75
private const val FADE_IN_MILLIS = 300

private val slideInEffect = tween<IntOffset>(
    durationMillis = FADE_IN_MILLIS,
    delayMillis = FADE_OUT_MILLIS,
    easing = LinearOutSlowInEasing,
)
private val slideOutEffect = tween<IntOffset>(durationMillis = FADE_IN_MILLIS)
private val fadeOutEffect = tween<Float>(
    durationMillis = FADE_OUT_MILLIS,
    easing = FastOutLinearInEasing,
)
private val fadeInEffect = tween<Float>(
    durationMillis = FADE_IN_MILLIS,
    delayMillis = FADE_OUT_MILLIS,
    easing = LinearOutSlowInEasing,
)
private val offsetFunc: (offsetForFullSlide: Int) -> Int = { it.div(5) }
+0 −252
Original line number Diff line number Diff line
/*
 * 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.AnimatedContentTransitionScope
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: (AnimatedScope.() -> EnterTransition) = { fadeIn(animationSpec = tween(700)) },
    exitTransition: (AnimatedScope.() -> ExitTransition) = { fadeOut(animationSpec = tween(700)) },
    popEnterTransition: (AnimatedScope.() -> EnterTransition) = enterTransition,
    popExitTransition: (AnimatedScope.() -> 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: (AnimatedScope.() -> EnterTransition) = { fadeIn(animationSpec = tween(700)) },
    exitTransition: (AnimatedScope.() -> ExitTransition) = { fadeOut(animationSpec = tween(700)) },
    popEnterTransition: (AnimatedScope.() -> EnterTransition) = enterTransition,
    popExitTransition: (AnimatedScope.() -> 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: AnimatedScope.() -> 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: AnimatedScope.() -> 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 = composeNavigator.backStack.value.size.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)
}

@OptIn(ExperimentalAnimationApi::class)
internal typealias AnimatedScope = AnimatedContentTransitionScope<NavBackStackEntry>

@ExperimentalAnimationApi
internal val enterTransitions = mutableMapOf<String?, (AnimatedScope.() -> EnterTransition?)?>()

@ExperimentalAnimationApi
internal val exitTransitions = mutableMapOf<String?, (AnimatedScope.() -> ExitTransition?)?>()
@ExperimentalAnimationApi
internal val popEnterTransitions = mutableMapOf<String?, (AnimatedScope.() -> EnterTransition?)?>()

@ExperimentalAnimationApi
internal val popExitTransitions = mutableMapOf<String?, (AnimatedScope.() -> ExitTransition?)?>()
Loading