升级Navigation Compose 2.8.0-beta02后页面切换动画异常
问题:Jetpack Compose 类型安全路由升级后页面过渡动画异常
之前用字符串路由的Jetpack Compose Navigation实现页面滑动切换动画完全正常,但升级导航库到测试版:
navigationComposeVersion = "2.8.0-beta02"
并切换为类型安全路由后,过渡动画出现两个异常:
- 页面跳转时动画过快(预期是500ms的慢速滑动)
- 返回上一页时完全没有动画
以下是相关代码:
SlideComposable.kt
import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.core.tween import androidx.compose.runtime.Composable import androidx.navigation.NavBackStackEntry import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable /** * 滑动动画时长常量 */ const val SlideAnimationDuration = 500 /** * [NavGraphBuilder]的扩展函数,为特定[BaseScreen]子类创建带滑动动画的可组合项 * * @param content 包含[AnimatedContentScope]和[NavBackStackEntry]的可组合内容块 */ inline fun <reified T : BaseScreen> NavGraphBuilder.slideComposable( noinline content: @Composable (AnimatedContentScope.(NavBackStackEntry) -> Unit), ) { composable<T>( enterTransition = { slideIntoContainer( towards = AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(SlideAnimationDuration), ) }, exitTransition = { slideOutOfContainer( towards = AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(SlideAnimationDuration), ) }, popEnterTransition = { slideIntoContainer( towards = AnimatedContentTransitionScope.SlideDirection.Right, animationSpec = tween(SlideAnimationDuration), ) }, popExitTransition = { slideOutOfContainer( towards = AnimatedContentTransitionScope.SlideDirection.Right, animationSpec = tween(SlideAnimationDuration), ) }, content = content, ) }
SearchNavGraph.kt
import androidx.navigation.NavGraphBuilder import androidx.navigation.toRoute import com.xxx.android.xxxserch.presentation.listingdetails.screen.ListingDetailsScreen import com.xxx.android.xxxserch.presentation.search.screen.SearchScreen import com.xxx.android.foundation.navigation.Screen import com.xxx.android.foundation.navigation.navigationmanagers.HomeNavigationManager import com.xxx.android.presentation.appstate.XXXAppState import com.commonlibs.android.foundation.navigation.customcomponents.slideComposable fun NavGraphBuilder.searchNavGraph(appState: XXXAppState) { val homeNavigationManager = HomeNavigationManager(appState) slideComposable<Screen.SearchScreen> { val searchScreen = it.toRoute<Screen.SearchScreen>() SearchScreen( loadLocationsScreen = { homeNavigationManager.gotoLocationsScreen() }, loadFilterScreen = homeNavigationManager.gotoFilterScreen, searchText = searchScreen.searchKeyword, backToPrevScreen = homeNavigationManager.backToPrevScreen, loadHomeDetailsScreen = homeNavigationManager.gotoHomeDetailScreen, ) } slideComposable<Screen.ListingDetailScreen> { val listingDetailsScreen = it.toRoute<Screen.ListingDetailScreen>() ListingDetailsScreen( homeId = listingDetailsScreen.homeId, backToPreviousScreen = homeNavigationManager.backToPrevScreen, ) } }
HomeNavGraph.kt
fun NavGraphBuilder.homeNavGraph(appState: XXXAppState) { val homeNavigationManager = HomeNavigationManager(appState) slideComposable<BottomNavItem.Home> { HomeScreen( loadLocationsScreen = { homeNavigationManager.gotoLocationsScreen() }, loadFilterScreen = homeNavigationManager.gotoFilterScreen, loadSearchModule = homeNavigationManager.gotoSearchModule, loadDetailsScreen = homeNavigationManager.gotoDetailScreen, ) } slideComposable<Screen.HomeFilterScreen> { HomeFilterScreen( goBackToPrevScreen = homeNavigationManager.backToPrevScreen, ) } }
导航函数
fun XXXAppState.navigate(screen: BaseScreen) { if (navController.isLifecycleResumed) { navController.navigate(screen) { launchSingleTop = true } } } class HomeNavigationManager(private val appState: XXXAppState) { val backToPrevScreen: () -> Unit = { appState.popUp() } val gotoFilterScreen: () -> Unit = { appState.navigate(Screen.HomeFilterScreen) } val gotoSearchModule: (String) -> Unit = { appState.navigate(Screen.SearchScreen(searchKeyword = it)) } val gotoHomeDetailScreen: (String) -> Unit = { appState.navigate(Screen.ListingDetailScreen(it)) } }
屏幕密封类
@Serializable sealed class Screen : BaseScreen() { @Serializable data object HomeFilterScreen : Screen() @Serializable data class SearchScreen(val searchKeyword: String) : Screen() @Serializable data class ListingDetailScreen(val careHomeId: String) : Screen() }
解决方案
1. 移除或调整launchSingleTop设置
你在navigate方法里加了launchSingleTop = true,这会导致如果目标页面已经在栈顶,系统会直接复用现有实例,跳过动画逻辑或者让动画异常:
- 若不需要单页面复用,直接删掉
launchSingleTop = true; - 若必须保留,需确保每次跳转的路由参数唯一(比如
SearchScreen的searchKeyword变化时,视为新页面而非复用旧实例)。
2. 适配类型安全路由的动画判断逻辑
Navigation 2.8.0-beta版本中,类型安全路由的动画处理逻辑有变更,原写法可能未正确识别路由切换关系。修改slideComposable函数,添加路由匹配判断,确保动画仅在对应路由切换时触发:
inline fun <reified T : BaseScreen> NavGraphBuilder.slideComposable( noinline content: @Composable (AnimatedContentScope.(NavBackStackEntry) -> Unit), ) { composable<T>( enterTransition = { // 确认目标是当前指定的路由类型,再执行动画 if (targetState.destination.route == T::class.simpleName) { slideIntoContainer( towards = AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(SlideAnimationDuration), ) } else { null } }, exitTransition = { if (targetState.destination.route == T::class.simpleName) { slideOutOfContainer( towards = AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(SlideAnimationDuration), ) } else { null } }, popEnterTransition = { if (initialState.destination.route == T::class.simpleName) { slideIntoContainer( towards = AnimatedContentTransitionScope.SlideDirection.Right, animationSpec = tween(SlideAnimationDuration), ) } else { null } }, popExitTransition = { if (initialState.destination.route == T::class.simpleName) { slideOutOfContainer( towards = AnimatedContentTransitionScope.SlideDirection.Right, animationSpec = tween(SlideAnimationDuration), ) } else { null } }, content = content, ) }
3. 检查popUp方法的实现
确保appState.popUp()是直接调用系统的返回逻辑,避免自定义逻辑跳过返回动画:
fun XXXAppState.popUp() { navController.popBackStack() }
4. 更换导航库版本
若上述方法无效,可暂时降级到Navigation 2.7.x稳定版,或升级到最新测试/稳定版——beta版本本身可能存在动画兼容bug,后续版本大概率会修复。
内容的提问来源于stack exchange,提问作者Sudheesh Mohan
相关产品推荐
相关产品推荐

