Jetpack Compose:实现从点击卡片展开至详情页的入场过渡动画
解决Jetpack Compose详情页从点击卡片展开的入场过渡问题
核心问题分析
用navArgument()传递卡片位置尺寸无效,是因为导航参数在enterTransition初始化阶段还未完成解析,无法及时获取到正确值。我们需要通过状态暂存+CompositionLocal传递的方式,在点击卡片时实时保存位置信息,再在详情页入场过渡中读取使用。
具体实现步骤
1. 创建过渡状态容器
定义用于暂存卡片位置尺寸的数据类,以及状态持有类,通过CompositionLocal在整个Compose树中共享:
// 存储卡片的全局位置和尺寸 data class CardTransitionData( val globalOffset: Offset, val size: Size ) // 状态持有类,管理过渡数据 class TransitionStateHolder { val cardTransitionData = mutableStateOf<CardTransitionData?>(null) } // 定义CompositionLocal,用于在Compose树中传递状态 val LocalTransitionStateHolder = compositionLocalOf<TransitionStateHolder> { error("TransitionStateHolder未在父Compose中提供") }
在应用根节点初始化并提供这个状态:
@Composable fun MyApp() { val transitionStateHolder = remember { TransitionStateHolder() } CompositionLocalProvider(LocalTransitionStateHolder provides transitionStateHolder) { val navController = rememberNavController() NavHost(navController = navController, startDestination = "grid") { // 注册网格页和详情页 grid(navController) details(navController) } } }
2. 点击卡片时保存过渡数据
在网格页的卡片组件中,通过Modifier.onGloballyPositioned()获取卡片的全局坐标(相对于屏幕)和尺寸,点击时保存到状态中:
@Composable fun GridCard(item: Item, navController: NavHostController) { Card( modifier = Modifier .onGloballyPositioned { coordinates -> // 获取卡片在屏幕中的全局偏移和尺寸 val globalOffset = coordinates.positionInWindow() val size = coordinates.size.toSize() LocalTransitionStateHolder.current.cardTransitionData.value = CardTransitionData(globalOffset, size) } .clickable { navController.navigate("details/${item.id}") } ) { // 卡片内容布局 // ... } }
3. 详情页配置入场过渡
在NavGraphBuilder.details()中,读取暂存的过渡数据,计算expandFrom和initialSize参数:
fun NavGraphBuilder.details(navController: NavHostController) { composable( route = "details/{itemId}", enterTransition = { val transitionData = LocalTransitionStateHolder.current.cardTransitionData.value transitionData?.let { data -> // 将卡片全局偏移转换为过渡所需的IntOffset val expandFromOffset = IntOffset( x = data.globalOffset.x.roundToInt(), y = data.globalOffset.y.roundToInt() ) expandIn( animationSpec = tween(500), // 指定展开的起始位置 expandFrom = { _, _, _ -> expandFromOffset }, // 指定初始尺寸为点击卡片的尺寸 initialSize = { IntSize( data.size.width.roundToInt(), data.size.height.roundToInt() ) } ) } ?: fadeIn() // 无过渡数据时使用淡入兜底 }, exitTransition = { fadeOut() } ) { backStackEntry -> val itemId = backStackEntry.arguments?.getString("itemId") DetailsScreen(itemId = itemId) // 页面销毁时清空过渡数据,避免下次进入复用旧数据 LaunchedEffect(Unit) { onDispose { LocalTransitionStateHolder.current.cardTransitionData.value = null } } } }
4. 屏幕旋转兼容性处理
如果需要支持屏幕旋转,将TransitionStateHolder替换为ViewModel,确保状态在旋转后不丢失:
class TransitionViewModel : ViewModel() { val cardTransitionData = mutableStateOf<CardTransitionData?>(null) }
在根Compose中获取ViewModel并传递:
@Composable fun MyApp() { val transitionViewModel = viewModel<TransitionViewModel>() CompositionLocalProvider(LocalTransitionStateHolder provides transitionViewModel) { // ... 原有代码 } }
内容的提问来源于stack exchange,提问作者László Stahorszki
相关产品推荐
相关产品推荐

