diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountNavigation.kt index 364e655e4..5d40e0aba 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountNavigation.kt @@ -4,6 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable +import com.x8bit.bitwarden.ui.platform.theme.TransitionProviders private const val CREATE_ACCOUNT_ROUTE = "create_account" @@ -21,7 +22,13 @@ fun NavGraphBuilder.createAccountDestinations( onNavigateBack: () -> Unit, onNavigateToLogin: (emailAddress: String, captchaToken: String) -> Unit, ) { - composable(route = CREATE_ACCOUNT_ROUTE) { + composable( + route = CREATE_ACCOUNT_ROUTE, + enterTransition = TransitionProviders.Enter.slideUp, + exitTransition = TransitionProviders.Exit.slideDown, + popEnterTransition = TransitionProviders.Enter.slideUp, + popExitTransition = TransitionProviders.Exit.slideDown, + ) { CreateAccountScreen( onNavigateBack = onNavigateBack, onNavigateToLogin = onNavigateToLogin, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingNavigation.kt index 63a1b534f..652b8242f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingNavigation.kt @@ -4,6 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable +import com.x8bit.bitwarden.ui.platform.theme.TransitionProviders const val LANDING_ROUTE: String = "landing" @@ -21,7 +22,13 @@ fun NavGraphBuilder.landingDestinations( onNavigateToCreateAccount: () -> Unit, onNavigateToLogin: (emailAddress: String) -> Unit, ) { - composable(route = LANDING_ROUTE) { + composable( + route = LANDING_ROUTE, + enterTransition = TransitionProviders.Enter.stay, + exitTransition = TransitionProviders.Exit.stay, + popEnterTransition = TransitionProviders.Enter.stay, + popExitTransition = TransitionProviders.Exit.stay, + ) { LandingScreen( onNavigateToCreateAccount = onNavigateToCreateAccount, onNavigateToLogin = onNavigateToLogin, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt index 5da6ab30b..b39da9e90 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt @@ -7,6 +7,7 @@ import androidx.navigation.NavOptions import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument +import com.x8bit.bitwarden.ui.platform.theme.TransitionProviders private const val EMAIL_ADDRESS: String = "email_address" private const val CAPTCHA_TOKEN = "captcha_token" @@ -51,6 +52,10 @@ fun NavGraphBuilder.loginDestinations( nullable = true }, ), + enterTransition = TransitionProviders.Enter.slideUp, + exitTransition = TransitionProviders.Exit.slideDown, + popEnterTransition = TransitionProviders.Enter.slideUp, + popExitTransition = TransitionProviders.Exit.slideDown, ) { LoginScreen( onNavigateBack = onNavigateBack, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index b214b0170..d599cf562 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -19,6 +19,7 @@ import com.x8bit.bitwarden.ui.platform.feature.splash.splashDestination import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VAULT_UNLOCKED_GRAPH_ROUTE import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlockedGraph import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedGraph +import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders /** * Controls root level [NavHost] for the app. @@ -39,6 +40,10 @@ fun RootNavScreen( NavHost( navController = navController, startDestination = SPLASH_ROUTE, + enterTransition = RootTransitionProviders.Enter.fadeIn, + exitTransition = RootTransitionProviders.Exit.fadeOut, + popEnterTransition = RootTransitionProviders.Enter.fadeIn, + popExitTransition = RootTransitionProviders.Exit.fadeOut, ) { splashDestination() authGraph(navController) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index fc45612bf..208974de7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -37,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.components.PlaceholderComposable import com.x8bit.bitwarden.ui.platform.feature.settings.SETTINGS_GRAPH_ROUTE import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToSettingsGraph import com.x8bit.bitwarden.ui.platform.feature.settings.settingsGraph +import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders import com.x8bit.bitwarden.ui.tools.feature.generator.GENERATOR_ROUTE import com.x8bit.bitwarden.ui.tools.feature.generator.generatorDestination import com.x8bit.bitwarden.ui.tools.feature.generator.navigateToGenerator @@ -175,6 +176,10 @@ private fun VaultUnlockedNavBarScaffold( modifier = Modifier .consumeWindowInsets(WindowInsets.navigationBars) .padding(innerPadding), + enterTransition = RootTransitionProviders.Enter.fadeIn, + exitTransition = RootTransitionProviders.Exit.fadeOut, + popEnterTransition = RootTransitionProviders.Enter.fadeIn, + popExitTransition = RootTransitionProviders.Exit.fadeOut, ) { vaultDestination() sendDestination() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/Transition.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/Transition.kt new file mode 100644 index 000000000..c038386bc --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/Transition.kt @@ -0,0 +1,208 @@ +package com.x8bit.bitwarden.ui.platform.theme + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.navigation.NavBackStackEntry +import androidx.navigation.compose.NavHost + +typealias EnterTransitionProvider = + (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?) + +typealias ExitTransitionProvider = + (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?) + +typealias NonNullEnterTransitionProvider = + (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) + +typealias NonNullExitTransitionProvider = + (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) + +/** + * The default transition time (in milliseconds) for all transitions in the [TransitionProviders]. + */ +const val DEFAULT_TRANSITION_TIME_MS: Int = 350 + +/** + * Checks if the parent of the destination before and after the navigation is the same. This is + * useful to ignore certain enter/exit transitions when navigating between distinct, nested flows. + */ +val AnimatedContentTransitionScope.isSameGraphNavigation: Boolean + get() = initialState.destination.parent == targetState.destination.parent + +/** + * Contains standard "transition providers" that may be used to specify the [EnterTransition] and + * [ExitTransition] used when building a typical composable destination. These may return `null` + * values in order to allow transitions between nested navigation graphs to be specified by + * components higher up in the graph. + */ +object TransitionProviders { + /** + * The standard set of "enter" transition providers. + */ + object Enter { + /** + * Fades the new screen in. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val fadeIn: EnterTransitionProvider = { + RootTransitionProviders + .Enter + .fadeIn(this) + .takeIf { isSameGraphNavigation } + } + + /** + * Slides the new screen in from the bottom of the screen. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val slideUp: EnterTransitionProvider = { + RootTransitionProviders + .Enter + .slideUp(this) + .takeIf { isSameGraphNavigation } + } + + /** + * A "no-op" transition: this changes nothing about the screen but "lasts" as long as + * other standard transitions in order to leave the screen in place such that it does not + * immediately appear while the other screen transitions away. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val stay: EnterTransitionProvider = { + RootTransitionProviders + .Enter + .stay(this) + .takeIf { isSameGraphNavigation } + } + } + + /** + * The standard set of "exit" transition providers. + */ + object Exit { + /** + * Fades the current screen out. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val fadeOut: ExitTransitionProvider = { + RootTransitionProviders + .Exit + .fadeOut(this) + .takeIf { isSameGraphNavigation } + } + + /** + * Slides the current screen down to the bottom of the screen. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val slideDown: ExitTransitionProvider = { + RootTransitionProviders + .Exit + .slideDown(this) + .takeIf { isSameGraphNavigation } + } + + /** + * A "no-op" transition: this changes nothing about the screen but "lasts" as long as + * other standard transitions in order to leave the screen in place such that it does not + * immediately disappear while the other screen transitions into place. + * + * Note that this represents a `null` transition when navigating between different nested + * navigation graphs. + */ + val stay: ExitTransitionProvider = { + RootTransitionProviders + .Exit + .stay(this) + .takeIf { isSameGraphNavigation } + } + } +} + +/** + * Contains standard "transition providers" that may be used to specify the [EnterTransition] and + * [ExitTransition] used when building a root [NavHost], which requires a non-null value. + */ +object RootTransitionProviders { + /** + * The standard set of "enter" transition providers. + */ + object Enter { + /** + * Fades the new screen in. + */ + val fadeIn: NonNullEnterTransitionProvider = { + fadeIn(tween(DEFAULT_TRANSITION_TIME_MS)) + } + + /** + * Slides the new screen in from the bottom of the screen. + */ + val slideUp: NonNullEnterTransitionProvider = { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Up, + animationSpec = tween(DEFAULT_TRANSITION_TIME_MS), + ) + } + + /** + * A "no-op" transition: this changes nothing about the screen but "lasts" as long as + * other standard transitions in order to leave the screen in place such that it does not + * immediately appear while the other screen transitions away. + */ + val stay: NonNullEnterTransitionProvider = { + fadeIn( + animationSpec = tween(DEFAULT_TRANSITION_TIME_MS), + initialAlpha = 1f, + ) + } + } + + /** + * The standard set of "exit" transition providers. + */ + object Exit { + /** + * Fades the current screen out. + */ + val fadeOut: NonNullExitTransitionProvider = { + fadeOut(tween(DEFAULT_TRANSITION_TIME_MS)) + } + + /** + * Slides the current screen down to the bottom of the screen. + */ + val slideDown: NonNullExitTransitionProvider = { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Down, + animationSpec = tween(DEFAULT_TRANSITION_TIME_MS), + ) + } + + /** + * A "no-op" transition: this changes nothing about the screen but "lasts" as long as + * other standard transitions in order to leave the screen in place such that it does not + * immediately disappear while the other screen transitions into place. + */ + val stay: NonNullExitTransitionProvider = { + fadeOut( + animationSpec = tween(DEFAULT_TRANSITION_TIME_MS), + targetAlpha = 0f, + ) + } + } +} diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 6ccdb8150..08bb2c3ed 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -6,7 +6,7 @@ @android:color/transparent @null false - @null + @color/surface true true default