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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 23:08:18