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

如何将Jetpack Compose SharedTransition与AnchoredDraggable结合实现可拖拽播放器?

实现支持拖拽+点击联动过渡动画的Jetpack Compose媒体播放器

核心方案:用拖拽状态驱动过渡动画

通过统一状态管理将AnchoredDraggable的拖拽状态与AnimatedContent/SharedTransition的过渡逻辑绑定,同时通过层级和触摸区域控制解决底部导航遮挡问题。

1. 统一状态控制

先定义包含拖拽状态和播放状态的UI状态类,确保拖拽、点击操作共享同一状态源:

enum class PlayerExpansionState { COLLAPSED, EXPANDED }

@OptIn(ExperimentalMaterial3Api::class)
data class PlayerUiState(
    val expansionState: PlayerExpansionState,
    val draggableState: AnchoredDraggableState<PlayerExpansionState>
)

初始化拖拽状态时,绑定折叠/展开对应的锚点位置:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun rememberPlayerUiState(): PlayerUiState {
    val density = LocalDensity.current
    val draggableState = remember {
        AnchoredDraggableState(
            initialValue = PlayerExpansionState.COLLAPSED,
            positionalThreshold = { totalDistance -> totalDistance * 0.4f }, // 拖拽40%触发状态切换
            velocityThreshold = { with(density) { 800.dp.toPx() } },
            animationSpec = spring(stiffness = Spring.StiffnessMedium)
        ).apply {
            updateAnchors(
                anchors = mapOf(
                    0f to PlayerExpansionState.COLLAPSED,
                    with(density) { 480.dp.toPx() } to PlayerExpansionState.EXPANDED
                )
            )
        }
    }
    return PlayerUiState(
        expansionState = draggableState.currentValue,
        draggableState = draggableState
    )
}

2. 拖拽+过渡动画联动

使用AnimatedContent包裹播放器UI,让draggableState的当前值作为过渡目标,同时用offset修饰符跟随拖拽位移:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PlayerContainer(uiState: PlayerUiState) {
    val (expansionState, draggableState) = uiState

    AnimatedContent(
        targetState = expansionState,
        transitionSpec = {
            // 复用你原SharedTransition的过渡效果,比如缩放+淡入淡出
            scaleIn() with scaleOut() + fadeIn() with fadeOut()
        }
    ) { currentState ->
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(if (currentState == PlayerExpansionState.EXPANDED) 480.dp else 64.dp)
                .anchoredDraggable(draggableState, Orientation.Vertical)
                .offset(y = draggableState.offset.toDp())
                .clickable {
                    // 点击触发状态切换动画
                    val targetState = if (currentState == PlayerExpansionState.COLLAPSED) {
                        PlayerExpansionState.EXPANDED
                    } else {
                        PlayerExpansionState.COLLAPSED
                    }
                    draggableState.animateTo(targetState)
                }
        ) {
            // 对应状态的播放器UI
            when (currentState) {
                PlayerExpansionState.COLLAPSED -> MiniPlayer()
                PlayerExpansionState.EXPANDED -> FullPlayer()
            }
        }
    }
}

如果需要用SharedTransition实现元素共享动画,可在状态切换时手动触发:

// 在SharedTransitionScope内
LaunchedEffect(expansionState) {
    if (expansionState == PlayerExpansionState.EXPANDED) {
        startSharedTransition(
            rememberSharedContentState(key = "player_album_art")
        )
    }
}

3. 解决底部导航遮挡问题

问题根源是拖拽组件的透明区域会拦截触摸事件,可通过以下两种方式解决:

  • 限制触摸区域:给折叠态播放器设置明确的点击/拖拽区域,避免透明部分响应事件:
// 迷你播放器仅在自身可见区域响应交互
Box(
    modifier = Modifier
        .fillMaxWidth()
        .height(64.dp)
        .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))
        .clickable { /* 点击逻辑 */ }
        .anchoredDraggable(draggableState, Orientation.Vertical)
) {
    MiniPlayerContent()
}
  • 提升导航层级:给底部导航设置更高的zIndex,确保优先响应点击:
BottomNavigation(
    modifier = Modifier.zIndex(1f) // 高于播放器层级
) {
    // 导航项
}

4. 完整交互逻辑

  • 点击迷你播放器:触发draggableState.animateTo(EXPANDED),同步启动AnimatedContent过渡动画
  • 拖拽播放器:实时跟随位移,松手后根据拖拽距离/速度自动切换到最近锚点,同步触发内容过渡
  • 拖拽展开态播放器回底部:自动切换到折叠态,完成反向过渡动画

内容的提问来源于stack exchange,提问作者adam_beers

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 14:57:01