19、动画与ConstraintLayout:ConstraintSet过渡动画、MotionLayout入门、MotionScene配置
动画这件事,在Android里其实一直有个尴尬的地方:布局和动画是两套东西。你写布局用XML,做动画要么用属性动画硬编码,要么用View动画那一套。但如果你用过ConstraintLayout,你会发现它其实埋了一个很深的伏笔——ConstraintSet。
我个人习惯把ConstraintSet看作是布局的“快照”。你定义好两个不同的布局状态,然后让系统帮你做过渡动画。说白了,就是布局之间的补间动画。而MotionLayout,则是把这个思路推到了极致。
19.1 ConstraintSet过渡动画:布局也能动起来
先说说ConstraintSet。它本质上就是一组约束条件的集合。你可以把它理解成布局文件的“内存版本”。
举个例子,我想让一个按钮从左上角移动到右下角。传统做法是用属性动画改TranslationX和TranslationY。但用ConstraintSet,你只需要定义两个布局状态:
// 第一个状态:按钮在左上角
val constraintSet1 = ConstraintSet()
constraintSet1.clone(this, R.layout.state_start)
// 第二个状态:按钮在右下角
val constraintSet2 = ConstraintSet()
constraintSet2.clone(this, R.layout.state_end)
// 执行过渡动画
TransitionManager.beginDelayedTransition(constraintLayout)
constraintSet2.applyTo(constraintLayout)
嗯,就这么简单。TransitionManager会帮你自动计算中间帧。我在项目里用过这个做列表项的展开收起效果,比手动调属性动画省事多了。
19.2 MotionLayout:ConstraintLayout的进化版
MotionLayout是ConstraintLayout的子类。它把ConstraintSet过渡动画做成了声明式的配置方式。你不需要写一行Java/Kotlin代码,就能实现复杂的交互动画。
我记得第一次用MotionLayout时,最大的感受是:终于不用在代码里写AnimatorSet了。所有的动画逻辑都放在XML里,清晰得很。
MotionLayout的核心思想是:
- 定义多个ConstraintSet作为关键帧
- 通过MotionScene配置过渡路径
- 支持触摸拖拽、滑动联动等交互
说白了,它就是ConstraintSet过渡动画的“工业化”版本。
19.3 MotionScene配置:动画的剧本
MotionScene是MotionLayout的配置文件。它放在res/xml/目录下,里面定义了动画的“剧本”。
一个典型的MotionScene长这样:
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
motion:constraintSetStart="@id/start"
motion:constraintSetEnd="@id/end"
motion:duration="300">
<OnSwipe
motion:touchAnchorId="@id/button"
motion:touchAnchorSide="right"
motion:dragDirection="dragRight" />
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@id/button"
android:layout_width="64dp"
android:layout_height="64dp"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@id/button"
android:layout_width="64dp"
android:layout_height="64dp"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintBottom_toBottomOf="parent" />
</ConstraintSet>
</MotionScene>
这里有几个关键点:
- Transition:定义从哪个状态到哪个状态,以及动画时长
- OnSwipe:让动画跟随手指滑动,实现交互驱动
- ConstraintSet:定义每个状态的布局约束
19.4 知识体系总览
下面这张图是我整理的本章知识结构,方便你理解它们之间的关系:
19.5 实际项目中的经验
我在一个电商App里用过MotionLayout做商品详情页的折叠效果。顶部是商品图片,下面是详情描述。用户上滑时,图片逐渐缩小并固定在顶部,描述内容滚动上来。
如果用传统方式,你得监听ScrollView的滑动事件,然后动态调整图片的TranslationY和ScaleX/ScaleY。代码量不小,而且容易出问题。
用MotionLayout怎么做?
- 定义两个ConstraintSet:一个展开状态,一个折叠状态
- 在MotionScene里配置OnSwipe,绑定到NestedScrollView的滑动
- 在ConstraintSet里设置图片的尺寸和位置变化
代码量大概减少了60%。而且动画曲线、插值器都可以在XML里调,不用反复编译运行。
19.6 避坑指南
我曾经踩过一个坑:MotionLayout和RecyclerView一起用的时候,滑动冲突处理不好。MotionLayout的OnSwipe会拦截触摸事件,导致RecyclerView无法滚动。
解决办法是:在MotionScene里设置motion:touchRegionId,指定只有某个区域内的滑动才触发动画。或者用motion:maxAcceleration和motion:maxVelocity控制灵敏度。
另一个坑是:ConstraintSet里如果用了layout_goneMargin,过渡动画可能会跳帧。我建议尽量用visibility变化配合gone属性,而不是手动调margin。
19.7 总结
ConstraintSet过渡动画和MotionLayout,本质上都是在解决同一个问题:让布局变化变得平滑自然。区别在于,前者适合简单的两状态切换,后者适合复杂的多状态交互。
我个人建议:如果你的动画只需要在代码里触发一次,用ConstraintSet + TransitionManager就够了。但如果你的动画需要跟随用户手势、或者有多个中间状态,直接上MotionLayout。
嗯,这一章的内容就到这里。记住一点:动画不是炫技,是为了让用户觉得“这个App用起来很顺手”。MotionLayout帮你做到了这一点,而且做得挺漂亮。