16、Compose动画之手势动画:animateScroll、animateDrag、animateTransform

手势动画,说白了就是让界面跟着用户的手指动起来。

在传统View时代,我们处理手势动画通常要写一大堆监听器,什么onTouchEvent、GestureDetector、Scroller,还要自己处理惯性计算、边界回弹。说实话,那套东西写起来挺痛苦的。我记得刚入行那会儿,光是做一个可拖拽的卡片,就折腾了两三天。

到了Compose,情况完全不一样了。手势动画被封装成了几个非常优雅的API,你只需要告诉系统「我要滚动」、「我要拖拽」或者「我要变换」,剩下的交给框架就好。

16.1 animateScroll:让列表滚动更丝滑

animateScroll是Compose中最基础的手势动画API。它专门用来处理滚动场景,比如列表、页面、或者任何需要滚动的容器。

我个人习惯把animateScroll理解为「有动画的scrollState」。普通的scrollState只能记录滚动位置,而animateScroll能让你以动画的方式跳转到某个位置。

核心用法:

@Composable
fun SmoothScrollExample() {
    val scrollState = rememberScrollState()
    
    Column(
        modifier = Modifier
            .verticalScroll(scrollState)
            .fillMaxSize()
    ) {
        // 内容区域
        repeat(50) { index ->
            Text("Item $index", modifier = Modifier.padding(16.dp))
        }
    }
    
    // 点击按钮,平滑滚动到顶部
    Button(
        onClick = {
            // 这就是animateScroll的核心调用
            coroutineScope.launch {
                scrollState.animateScrollTo(0)
            }
        }
    ) {
        Text("回到顶部")
    }
}

这里有个细节要注意:animateScrollTo是一个挂起函数,必须在协程里调用。我刚开始用的时候经常忘记加launch,结果编译报错还一脸懵。

避坑指南:

我曾经在嵌套滚动场景里踩过坑。如果你同时用了verticalScroll和nestedScroll,animateScrollTo的动画时长可能会被截断。解决方案是给animateScrollTo指定一个较长的动画时长,或者用scroll.animateScrollBy来逐步滚动。

16.2 animateDrag:让元素跟着手指走

animateDrag是处理拖拽动画的主力。它和传统的onTouchEvent不同,你不需要手动计算手指移动的距离,也不需要处理ACTION_MOVE、ACTION_UP这些事件。Compose帮你把这些都封装好了。

说白了,你只需要告诉系统「这个元素可以被拖拽」,然后提供一个偏移量的状态,剩下的交给animateDrag。

@Composable
fun DraggableCard() {
    var offsetX by remember { mutableFloatStateOf(0f) }
    var offsetY by remember { mutableFloatStateOf(0f) }
    
    Box(
        modifier = Modifier
            .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
            .pointerInput(Unit) {
                // 这就是animateDrag的核心
                detectDragGestures { change, dragAmount ->
                    change.consume()
                    offsetX += dragAmount.x
                    offsetY += dragAmount.y
                }
            }
            .size(100.dp)
            .background(Color.Blue)
    )
}

嗯,这里要注意一点:detectDragGestures返回的dragAmount是每次移动的增量,不是绝对位置。如果你需要做边界限制,比如不让卡片拖出屏幕,那就得自己判断offsetX和offsetY的范围。

重要提醒:

animateDrag本身不处理惯性滑动。如果你希望拖拽结束后卡片还能继续滑动一段距离(就像iOS的列表那样),需要配合animateDecay使用。这个我们后面会讲到。

16.3 animateTransform:自由变换的终极武器

animateTransform是三者中最强大的。它不仅能处理位移,还能处理缩放、旋转、甚至倾斜。你可以把它理解为「带动画的Matrix变换」。

我在项目中遇到过这样一个需求:用户双指缩放图片,同时还能旋转。如果用传统View写,得自己处理ScaleGestureDetector和RotationGestureDetector,还要考虑变换矩阵的叠加。用Compose的animateTransform,代码量直接砍半。

@Composable
fun TransformableImage() {
    var scale by remember { mutableFloatStateOf(1f) }
    var rotation by remember { mutableFloatStateOf(0f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    
    Image(
        painter = painterResource(id = R.drawable.sample),
        contentDescription = null,
        modifier = Modifier
            .graphicsLayer {
                // 应用变换
                scaleX = scale
                scaleY = scale
                rotationZ = rotation
                translationX = offset.x
                translationY = offset.y
            }
            .pointerInput(Unit) {
                // 这就是animateTransform的核心
                detectTransformGestures { _, pan, zoom, rotationChange ->
                    scale *= zoom
                    rotation += rotationChange
                    offset += pan
                }
            }
            .fillMaxSize()
    )
}

你想想看,一个手势同时处理缩放、旋转、平移,这在传统View里得写多少代码?而在Compose里,一个detectTransformGestures就搞定了。

性能优化建议:

使用graphicsLayer而不是Modifier.offset或Modifier.scale,因为graphicsLayer会触发硬件加速,性能更好。我在处理高分辨率图片时深有体会,用graphicsLayer后帧率从30fps直接飙到60fps。

16.4 三者的对比与选择

说了这么多,你可能有点晕:这三个到底该用哪个?我整理了一个表格,方便你快速决策。

API 适用场景 动画类型 是否需要协程
animateScroll 列表滚动、页面跳转 线性滚动
animateDrag 拖拽卡片、滑动删除 自由拖拽
animateTransform 图片缩放、旋转、双指操作 复合变换

简单来说:

  • 如果你只是想让列表平滑滚动,用animateScroll
  • 如果你想让元素跟着手指移动,用animateDrag
  • 如果你需要同时处理缩放、旋转、平移,用animateTransform

16.5 知识体系总览

为了让你更直观地理解这三个API的关系,我画了一张图。你可以看到,它们虽然功能不同,但底层都依赖Compose的动画系统和手势检测系统。

Compose手势动画知识体系 手势检测层 pointerInput / detectDragGestures / detectTransformGestures animateScroll 列表滚动 / 页面跳转 animateDrag 拖拽 / 滑动删除 animateTransform 缩放 / 旋转 / 平移 Compose动画引擎 Animatable / animate*AsState / AnimationSpec Canvas / graphicsLayer 渲染 硬件加速 / 60fps 流畅体验 三者共享底层动画引擎,但面向不同的交互场景

从这张图你可以看到,手势检测层负责捕捉用户的操作,然后传递给三个具体的API,最终由Compose动画引擎驱动渲染层完成动画。每一层各司其职,这也是Compose设计哲学的一个缩影。

我的个人建议:

刚开始学的时候,不要试图一次性掌握所有API。先从animateScroll入手,因为它最直观。等你熟悉了滚动动画的写法,再尝试animateDrag做拖拽,最后挑战animateTransform做复合变换。循序渐进,反而学得更快。

好了,这一章的内容就到这里。手势动画是Compose动画体系里非常实用的一块,掌握了它们,你的App交互体验会上一个台阶。下一章我们会聊更高级的动画组合技巧,敬请期待。


公众号:蓝海资料掘金营,微信deep3321