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

Jetpack Compose自定义抽屉上滑退出动画卡顿求优化

Jetpack Compose Top Sheet抽屉上滑关闭卡顿修复方案

问题描述

在Jetpack Compose中实现了类似Top Sheet的自定义抽屉,其余功能正常,但上滑关闭抽屉时,退出动画不流畅,会卡顿一秒后Card才消失。

问题根源

当前代码在拖拽结束时直接将offsetY重置为0并立刻设置isDrawerVisible = false,导致手动状态重置与AnimatedVisibility的退出动画冲突,打断了平滑过渡流程,进而出现卡顿。同时使用mutableFloatStateOf直接管理偏移量,没有利用Compose动画API衔接拖拽后的状态过渡。

修复方案

  • 使用animateFloatAsState管理偏移量,让拖拽结束后的状态变化自动触发平滑动画
  • 拖拽结束时根据拖拽方向和距离阈值判断是否关闭,让动画自然过渡而非硬重置状态
  • 调整AnimatedVisibility的进出动画,确保与拖拽位置衔接一致
  • 添加拖拽阈值判断,提升交互体验

修改后的完整代码

// 替换原有状态定义
val offsetY by animateFloatAsState(
    targetValue = targetOffsetY,
    animationSpec = tween(durationMillis = 300)
)
var targetOffsetY by remember { mutableFloatStateOf(0f) }
var isDrawerVisible by remember { mutableStateOf(false) }
var direction by remember { mutableIntStateOf(-1) }
val drawerHeight = remember { mutableIntStateOf(0) } // 记录抽屉高度用于计算拖拽阈值

// ** Drawer **
AnimatedVisibility(
    visible = isDrawerVisible,
    enter = slideInVertically(initialOffsetY = { -it }),
    exit = slideOutVertically(targetOffsetY = { -it })
) {
    Box(
        modifier = Modifier
            .offset { IntOffset(0, offsetY.toInt()) }
            .onSizeChanged { drawerHeight.intValue = it.height } // 初始化抽屉高度
            .pointerInput(Unit) {
                coroutineScope {
                    detectDragGestures(
                        onDragEnd = {
                            Timber.d("DRAG_DEBUG:: Drag End")
                            val closeThreshold = drawerHeight.intValue / 3 // 设置关闭阈值为抽屉高度的1/3
                            
                            // 上滑拖拽且偏移量超过阈值,触发关闭流程
                            if (direction == 2 && offsetY <= -closeThreshold) {
                                targetOffsetY = -drawerHeight.intValue.toFloat()
                                // 等待动画结束后再隐藏抽屉,避免动画冲突
                                launch {
                                    delay(300) // 与动画时长保持一致
                                    isDrawerVisible = false
                                    targetOffsetY = 0f // 重置偏移量,确保下次打开正常显示
                                }
                            } else {
                                // 拖拽距离不足,回弹到初始位置
                                targetOffsetY = 0f
                            }
                        }
                    ) { change, dragAmount ->
                        change.consume()

                        val (_, y) = dragAmount
                        direction = if (y > 0) 1 else 2 // 1=下滑,2=上滑

                        // 限制拖拽范围:只能向上拖拽(offsetY ≤0),且不能超过抽屉自身高度
                        val newOffset = offsetY + y
                        if (newOffset >= -drawerHeight.intValue && newOffset <= 0) {
                            targetOffsetY = newOffset
                        }
                    }
                }
            }
            .fillMaxWidth()
    ) {
        // 原有Card内容保持不变
        Card(
            modifier = Modifier.fillMaxWidth(),
            shape = RoundedCornerShape(bottomStart = 10.dp, bottomEnd = 10.dp),
            colors = CardDefaults.cardColors(containerColor = Color.White),
            elevation = CardDefaults.cardElevation(defaultElevation = 5.dp),
        ) {
            Column(modifier = Modifier.padding(15.dp)) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Image(
                        modifier = Modifier.clickable {
                            // 点击关闭按钮时触发滑出动画
                            targetOffsetY = -drawerHeight.intValue.toFloat()
                            launch {
                                delay(300)
                                isDrawerVisible = false
                                targetOffsetY = 0f
                            }
                        },
                        painter = painterResource(id = R.drawable.icon_drawer_close),
                        contentDescription = stringResource(id = R.string.text_close)
                    )

                    Spacer(modifier = Modifier.weight(1f))

                    Image(
                        modifier = Modifier.height(53.dp),
                        painter = painterResource(id = R.drawable.icon_drawer_logo),
                        contentDescription = stringResource(id = R.string.text_close),
                        contentScale = ContentScale.FillHeight
                    )

                    Spacer(modifier = Modifier.weight(1f))

                    Icon(
                        modifier = Modifier.size(50.dp),
                        imageVector = Icons.Default.Close,
                        contentDescription = "",
                        tint = Color.White
                    )
                }

                Spacer(modifier = Modifier.height(10.dp))

                LazyVerticalGrid(columns = GridCells.Fixed(4)) {
                    items(viewModel.getDrawerList(context)) {
                        Column(
                            modifier = Modifier.padding(top = 15.dp),
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.Center
                        ) {
                            Image(
                                painter = painterResource(id = it.icon),
                                contentDescription = ""
                            )
                            MyText(
                                modifier = Modifier.padding(top = 5.dp),
                                text = it.title,
                                fontSize = 13.sp
                            )
                        }
                    }
                }

                Spacer(modifier = Modifier.height(25.dp))

                Image(
                    modifier = Modifier.fillMaxWidth().height(4.dp),
                    painter = painterResource(id = R.drawable.icon_drawer_bar),
                    contentDescription = "",
                    contentScale = ContentScale.FillHeight
                )
            }
        }
    }
}

关键修改说明

  1. 动画化状态管理:通过animateFloatAsState自动处理偏移量的平滑过渡,替代手动赋值的生硬变化。
  2. 拖拽阈值控制:基于抽屉高度设置关闭阈值,只有拖拽足够距离才触发关闭,减少误操作。
  3. 动画衔接优化:关闭时先执行滑出动画,延迟后再隐藏抽屉,避免与AnimatedVisibility的动画逻辑冲突。
  4. 拖拽范围限制:防止过度拖拽超出合理范围,保证交互的合理性。
  5. 统一关闭逻辑:点击关闭按钮时执行与拖拽关闭一致的动画,保持交互体验统一。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 23:27:32