You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

升级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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 17:44:58