Tomáš Repčík - 29. 10. 2023

Jetpack Compose Transition Animations

The accompanist compose animations got deprecated and migrated to the standard compose navigation library

Till recently, the navigation library did not provide support for the animations. If you wanted animations, you had to do them mostly independently or adopt an experimental accompanist library. It has changed with the update to 2.7.x and higher of the navigation library.

For those, who are here to migrate the animations from accompanist to the navigation library, please go below to the migration section.

Dependency

Firstly, add the following dependency to your build.gradle:

    implementation "androidx.navigation:navigation-compose:2.7.4"

The latest version can be found here.

Creating a transition between 2 screens

If you have never used the compose navigation, you can read my tutorial about it: Android Jetpack Compose and Navigation

For the base build, we have to have some foundation for the app, which is contained within the following MainCompose. The main compose contains NavController, which makes the app navigate through different composables. NavHost nests all the routes, which your user can follow.

@Composable
fun MainCompose(
    navController: NavHostController = rememberNavController(),
) {
    AppAnimatedNavigationExample {
        Surface {
            NavHost(
                navController
            ) {
                exampleNavGraph(navController)
            }
        }
    }
}

In the beginning, we will work only with one navigation graph with 2 screens. WelcomeScreen has button to navigate to OtherScreen and OtherScreen has a back button to pop the navigation stack. Let’s have a look at the implementation.

fun NavGraphBuilder.exampleGraph(navController: NavController) {
    navigation(
        startDestination = "WelcomeScreen", route = "ExampleRoute"
    ) {
        composable("WelcomeScreen", enterTransition = {
            return@composable fadeIn(tween(1000))
        }, exitTransition = {
            return@composable slideOutOfContainer(
                AnimatedContentTransitionScope.SlideDirection.Start, tween(700)
            )
        }, popEnterTransition = {
            return@composable slideIntoContainer(
                AnimatedContentTransitionScope.SlideDirection.End, tween(700)
            )
        }) {
            WelcomeScreen(navController)
        }
        composable(
            "OtherScreen",
            enterTransition = {
                return@composable slideIntoContainer(
                    AnimatedContentTransitionScope.SlideDirection.Start, tween(700)
                )
            },
            popExitTransition = {
                return@composable slideOutOfContainer(
                    AnimatedContentTransitionScope.SlideDirection.End, tween(700)
                )
            },
        ) {
            OtherScreen(navController)
        }
    }
}

Transitions

The navigation works as before with the compose navigation dependency. Nothing has changed. However, we now have 4 new fields at composable.

For example, if the app is started, then the composable uses entryTransition to show the WelcomeScreen composable. If an user decides to move to OtherScreen, exitTransition is called for WelcomeScreen and is followed by another entryTransition for OtherScreen. If an user decides to go back to WelcomeScreen, popExitTransition is called for OtherScreen and is followed by another popEnterTransition for WelcomeScreen.

Animations

Firstly, it is important to point out that enter and exit animations use different types. The enter animations expect EnterTransition types and exit animations expect ExitTransition types. Usually, if you want to create animation, you want to animate composable into screen (EnterAnimation). The previous composable should be animated out (ExitAnimation) of the screen.

Examples of enter animations:

fadeIn(tween(700))
scaleIn(spring(Spring.DampingRatioHighBouncy))
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, tween(700))
expandIn(tween(700, easing = LinearEasing))

Examples of exit animations:

fadeOut(tween(700))
scaleOut(spring(Spring.DampingRatioHighBouncy))
slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, tween(700))
shrinkOut(tween(700, easing = LinearEasing))

For more, I would like to encourage you to have a look at all animation parameters here and experiment with different combinations of animation specifications and types. You are able to create your own animation too.

You might notice, that you do not see slideIn/slideOut in examples. This is because some of the animations require more specifications, but for the sake of simplicity there are available alternatives as slideIntoContainer, which make transitions of whole composables without much detail needed.

Do not try to use Left or Right equivalents of direction, but rather Start and End, because you will encounter problems with internationalization for regions, where they have flipped sides of reading.

Practical Example

For a complete example, the app will follow this user flow. The user will go through some introduction and afterwards, he will be moved to the main screen. During the process, the app will demonstrate multiple basic animations to transit through screens.

The most important are the composables within the navigation graph in MainCompose. The avigation graph contains two separate graphs, one for the intro and two second one for the main screen.

@Composable
fun MainCompose(
    navController: NavHostController = rememberNavController(),
) {
    AppAnimatedNavigationExample {
        Surface {
            NavHost(
                navController,
                startDestination = NavRoutes.IntroRoute.name
            ) {
                // navigation graphs
                introGraph(navController)
                mainGraph(navController)
            }
        }
    }
}

Every graph now contains configured composable with transition animations.

Intro transitions

fun NavGraphBuilder.introGraph(navController: NavController) {
    navigation(
        startDestination = IntroNavOption.WelcomeScreen.name, route = NavRoutes.IntroRoute.name
    ) {
        composable(IntroNavOption.WelcomeScreen.name, enterTransition = {
            return@composable fadeIn(tween(1000))
        }, exitTransition = {
            return@composable fadeOut(tween(700))
        }, popEnterTransition = {
            return@composable slideIntoContainer(
                AnimatedContentTransitionScope.SlideDirection.End, tween(700)
            )
        }) {
            WelcomeScreen(navController)
        }
        composable(
            IntroNavOption.MotivationScreen.name,
            enterTransition = {
                return@composable slideIntoContainer(
                    AnimatedContentTransitionScope.SlideDirection.Start, tween(700)
                )
            },
            popExitTransition = {
                return@composable slideOutOfContainer(
                    AnimatedContentTransitionScope.SlideDirection.End, tween(700)
                )
            },
        ) {
            MotivationScreen(navController)
        }
        composable(IntroNavOption.RecommendationScreen.name, enterTransition = {
            return@composable expandIn(tween(700))
        }, exitTransition = {
            return@composable shrinkOut(tween(700))
        }, popExitTransition = { return@composable shrinkOut() }) {
            RecommendationScreen(navController)
        }
    }
}

enum class IntroNavOption {
    WelcomeScreen, MotivationScreen, RecommendationScreen
}

Main graph transitions

fun NavGraphBuilder.mainGraph(navController: NavHostController) {
    navigation(startDestination = MainNavOption.HomeScreen.name, route = NavRoutes.MainRoute.name) {
        composable(MainNavOption.HomeScreen.name, enterTransition = {
            return@composable slideIntoContainer(
                AnimatedContentTransitionScope.SlideDirection.Up,
                tween(2000)
            )
        }) {
            HomeScreen(navController)
        }
    }
}

enum class MainNavOption {
    HomeScreen
}

Final result

The whole project can be found on Github

Migration

  1. Remove Accompanist dependency and replace/update your compose navigation dependency as it is on top of the page (version 2.7.0 and higher)
  2. rememberAnimatedNavController —> rememberNavController
  3. AnimatedNavHost —> NavHost
  4. AnimatedComposeNavigator —> ComposeNavigator
  5. AnimatedComposeNavigator() —> ComposeNavigator()
  6. AnimatedComposeNavigator.Destination —> ComposeNavigator.Destination
  7. Remove all Accompanist imports from the project

Resources

Navigation | Jetpack | Android Developers

Guide - Accompanist

Subscribe for more
LinkedIn GitHub Medium