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会帮你自动计算中间帧。我在项目里用过这个做列表项的展开收起效果,比手动调属性动画省事多了。

小技巧:ConstraintSet不仅支持位置变化,还支持尺寸变化、可见性变化(GONE/VISIBLE)。甚至你可以把两个完全不同的布局做过渡,只要ID对应得上。

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:定义每个状态的布局约束
注意:MotionScene里的ConstraintSet必须和布局文件中的View ID完全一致。我曾经因为拼写错误,找了半天bug,结果发现是ID大小写写错了。

19.4 知识体系总览

下面这张图是我整理的本章知识结构,方便你理解它们之间的关系:

ConstraintLayout动画体系 ConstraintLayout ConstraintSet MotionLayout 布局状态快照 TransitionManager过渡 MotionScene配置文件 OnSwipe / OnClick交互 ConstraintSet是基础,MotionLayout是进阶,两者都基于ConstraintLayout

19.5 实际项目中的经验

我在一个电商App里用过MotionLayout做商品详情页的折叠效果。顶部是商品图片,下面是详情描述。用户上滑时,图片逐渐缩小并固定在顶部,描述内容滚动上来。

如果用传统方式,你得监听ScrollView的滑动事件,然后动态调整图片的TranslationY和ScaleX/ScaleY。代码量不小,而且容易出问题。

用MotionLayout怎么做?

  1. 定义两个ConstraintSet:一个展开状态,一个折叠状态
  2. 在MotionScene里配置OnSwipe,绑定到NestedScrollView的滑动
  3. 在ConstraintSet里设置图片的尺寸和位置变化

代码量大概减少了60%。而且动画曲线、插值器都可以在XML里调,不用反复编译运行。

核心要点:MotionLayout最适合的场景是“布局状态之间的过渡动画”。如果你的动画只是简单的位移、缩放,用属性动画就够了。但如果你需要多个View联动、跟随手势、响应滑动,MotionLayout是更好的选择。

19.6 避坑指南

我曾经踩过一个坑:MotionLayout和RecyclerView一起用的时候,滑动冲突处理不好。MotionLayout的OnSwipe会拦截触摸事件,导致RecyclerView无法滚动。

解决办法是:在MotionScene里设置motion:touchRegionId,指定只有某个区域内的滑动才触发动画。或者用motion:maxAccelerationmotion:maxVelocity控制灵敏度。

另一个坑是:ConstraintSet里如果用了layout_goneMargin,过渡动画可能会跳帧。我建议尽量用visibility变化配合gone属性,而不是手动调margin。

19.7 总结

ConstraintSet过渡动画和MotionLayout,本质上都是在解决同一个问题:让布局变化变得平滑自然。区别在于,前者适合简单的两状态切换,后者适合复杂的多状态交互。

我个人建议:如果你的动画只需要在代码里触发一次,用ConstraintSet + TransitionManager就够了。但如果你的动画需要跟随用户手势、或者有多个中间状态,直接上MotionLayout。

嗯,这一章的内容就到这里。记住一点:动画不是炫技,是为了让用户觉得“这个App用起来很顺手”。MotionLayout帮你做到了这一点,而且做得挺漂亮。


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