MotionLayout的简单使用

1,107 阅读23分钟

一、关键概念

  1. ConstraintLayout 子类
    MotionLayout 继承自 ConstraintLayout,所以它拥有所有 ConstraintLayout 的特性(约束、控件对齐等),并在此基础上扩展了 动画 和 场景(MotionScene) 的能力。

  2. ConstraintSet
    在 MotionLayout 中,我们可以定义多个 ConstraintSet,用来描述同一个布局在 不同状态 下控件的约束信息(位置、大小、透明度、旋转、缩放等)。

  3. Transition
    Transition 定义了从 一个 ConstraintSet 过渡到 另一个 ConstraintSet 的动画,包括:

    • 过渡开始和结束时使用的 ConstraintSet。
    • 动画的时长、插值器等信息。
  4. MotionScene
    MotionScene 是一个 XML 文件,描述了 MotionLayout 中所有的 ConstraintSet 和 Transition。MotionLayout 会读取这个文件,根据其中的配置来执行动画。


二、核心文件与结构

使用 MotionLayout,最常见的结构是:

  1. 布局文件(layout.xml)

    • 其中的根或某个父布局使用 <androidx.constraintlayout.motion.widget.MotionLayout>。
    • 通过 app:layoutDescription 属性指定一个 .xml 文件(即下面的 MotionScene 文件)。
  2. MotionScene 文件(motion_scene.xml)

    • 包含若干个 <ConstraintSet>,用来描述布局在不同状态下的属性。
    • 包含 <Transition> 元素,用来指定从何种状态过渡到另一种状态、动画的时长及插值器等。

三、使用步骤

以下是一个典型使用 MotionLayout 的流程示例:

1. 在布局文件中使用 MotionLayout

<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/motion_scene"> <!-- 这里指定 MotionScene 文件 -->

    <!-- 你的界面内容,如一个 ImageView、TextView 等 -->
    <ImageView
        android:id="@+id/imageView"
        android:src="@drawable/ic_launcher_foreground"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>

注意: 将 MotionLayout 作为根布局或嵌套在上层布局中都可以,但通常在演示动画时会把它作为根布局来使用。

2. 创建 MotionScene 文件

<!-- motion_scene.xml (放在 res/xml/ 文件夹下) -->
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!-- 1) 定义两个(或多个)ConstraintSet,分别描述开始状态与结束状态 -->
    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/imageView"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/imageView"
            android:layout_width="150dp"
            android:layout_height="150dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
    </ConstraintSet>

    <!-- 2) 定义从哪个 ConstraintSet 过渡到哪个 ConstraintSet  -->
    <Transition
        app:constraintSetStart="@+id/start"
        app:constraintSetEnd="@+id/end"
        app:duration="1000"> <!-- 动画时长 1 秒 -->
    </Transition>
    
</MotionScene>

上面例子中:

  • @+id/start 定义了控件在顶部、左侧的约束。
  • @+id/end 定义了控件在底部、右侧的约束(并且尺寸变大)。
  • 当 MotionLayout 从 start 过渡到 end 时,就会执行动画。

3. 在代码中触发动画

MotionLayout 提供了多种方式来控制过渡动画:

  1. 直接调用 motionLayout.transitionToEnd() 或 transitionToStart()

    val motionLayout = findViewById<MotionLayout>(R.id.motionLayout)
    motionLayout.transitionToEnd()   // 从 start 到 end
    // motionLayout.transitionToStart() // 从 end 回到 start
    

    当调用 transitionToEnd() 时,会自动执行从 start 约束到 end 约束的动画。

  2. 自定义进度
    可以监听手势,或自己计算一个进度数值(0~1),调用 motionLayout.progress = 0.5f 来让动画过渡到一半。

  3. 拖拽交互
    在 MotionScene 中,通过 <OnSwipe> 或 <OnClick> 等标签,可以把过渡动画跟滑动手势或点击事件结合起来,实现更复杂的交互动画。

4. <OnSwipe>

<OnSwipe> 标签可以让手势滑动(Drag/Swipe) 与动画过渡相结合,实现更加交互式的动画效果。它的原理是:当用户在指定控件上滑动时,根据滑动距离或方向,去实时更新 MotionLayout 的过渡进度,从而让界面产生跟手的动画。

一、为什么使用 <OnSwipe>?

  1. 交互式动画
    相比简单的 motionLayout.transitionToEnd() 这种“一键播放”式动画,<OnSwipe> 更适用于需要用户通过手势操作来控制动画进行的场景,比如:

    • 上下滑动弹出 / 收起面板
    • 左右滑动切换卡片
    • 侧滑菜单等
  2. 自动处理插值和进度
    当我们在 <OnSwipe> 中指定好锚点、滑动方向等信息后,MotionLayout 会负责根据滑动的距离或速度,自动去计算并更新动画的进度(progress) ,从而让过渡动画随着手势进行。

  3. 可与 KeyFrame 结合
    即使用了 <OnSwipe>,我们依旧可以使用 <KeyAttribute> / <KeyPosition> 等关键帧,为动画插入更丰富的运动效果。

二、<OnSwipe> 的关键属性

在 MotionScene 文件中的 <Transition> 标签下,可以添加一个 <OnSwipe> 子标签。主要属性有:

  1. motion:touchAnchorId

    • 用来指定手势作用的目标控件。当用户在这个控件上滑动时,MotionLayout 会跟踪手势并更新动画进度。
    • 必须是过渡中存在于布局(ConstraintSet)的 View ID(例如 @+id/yourView)。
  2. motion:touchAnchorSide

    • 指定控件的锚点方位,可选值有 left / right / top / bottom / start / end / middle 等。
    • 表示我们将以控件的哪个边或中心点作为手势跟踪的参考点。
  3. motion:dragDirection

    • 指定整体的拖拽方向,通常是 dragUp, dragDown, dragLeft, dragRight。
    • 还有 dragAuto 可以让系统自动判断拖拽方向。
  4. motion:touchRegionId (可选)

    • 如果希望手势触发区域和真正的 touchAnchorId 控件不是同一个,可以用 touchRegionId 指定一个更大的(或更小的)触控区域。
  5. motion:moveWhenScrollAtTop (可选)

    • 当锚点是一个可以滚动的 RecyclerView 或 NestedScrollView 时,只有在滚动到顶部(或边缘)时才允许触发 MotionLayout 的过渡动画。
    • 设置为 true 可以在内容还没滚动到顶时就捕获手势。
  6. motion:maxVelocity / motion:maxAcceleration (可选)

    • 用来限制用户快速滑动时,动画进度的最大变化速率和加速度,防止动画过于突兀或太快。
  7. motion:rotationCenterId (可选)

    • 当在动画中有旋转需求时,可指定一个中心点进行旋转的计算。

三、<OnSwipe> 的基本使用示例

假设我们有一个布局:

  • MotionLayout 作为根布局
  • 一个 CardView(ID 为 cardView),我们想让它通过向上滑动来缩小并移到屏幕顶部。
1. activity_main.xml(布局文件)
<androidx.constraintlayout.motion.widget.MotionLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    motion:layoutDescription="@xml/motion_scene_swipe">

    <androidx.cardview.widget.CardView
        android:id="@+id/cardView"
        android:layout_width="300dp"
        android:layout_height="400dp"
        android:layout_marginTop="200dp"
        android:layout_marginBottom="100dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:cardUseCompatPadding="true"
        android:background="@android:color/holo_blue_light" >
        
        <!-- 这里可以放一些内容 -->
        
    </androidx.cardview.widget.CardView>
</androidx.constraintlayout.motion.widget.MotionLayout>
2. motion_scene_swipe.xml(MotionScene 文件)
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <!-- ConstraintSet: start (默认状态) -->
    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/cardView"
            android:layout_width="300dp"
            android:layout_height="400dp"
            motion:layout_constraintTop_toTopOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintBottom_toBottomOf="parent"
            android:scaleX="1.0"
            android:scaleY="1.0" />
    </ConstraintSet>

    <!-- ConstraintSet: end (滑动完成后的状态) -->
    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/cardView"
            android:layout_width="200dp"
            android:layout_height="200dp"
            motion:layout_constraintTop_toTopOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintBottom_toBottomOf="parent"
            android:scaleX="0.8"
            android:scaleY="0.8" />
    </ConstraintSet>

    <!-- 定义从 start -> end 的动画过渡 -->
    <Transition
        motion:constraintSetStart="@id/start"
        motion:constraintSetEnd="@id/end"
        motion:duration="1000">
        
        <!-- 在这里配置 OnSwipe -->
        <OnSwipe
            motion:touchAnchorId="@+id/cardView"
            motion:touchAnchorSide="top"
            motion:dragDirection="dragUp"
            motion:maxVelocity="4"
            motion:maxAcceleration="2"/>
    </Transition>
</MotionScene>
3. 运行效果
  • 初始时,cardView 处于 start 状态(居中、大小=300x400、scale=1.0)。
  • 当用户手指在 cardView 顶部区域向上滑动时,MotionLayout 会根据手指滑动距离或速度,逐渐更新过渡进度,使 cardView 向上移动并缩小,直至到达 end 状态。
  • 如果手势不够大、中途松手,MotionLayout 默认会根据当前进度和手势速度进行惯性判断,可能弹回到 start 或继续到 end。

四、监听进度变化(可选)

有时我们需要在滑动过程中执行其他逻辑,比如显示/隐藏某些按钮,或者做并行动画。可以通过对 MotionLayout 设置 TransitionListener 来监听过渡进度变化:

motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
    override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {}
    
    override fun onTransitionChange(
        motionLayout: MotionLayout?,
        startId: Int,
        endId: Int,
        progress: Float
    ) {
        // progress 范围 0 ~ 1.0
        Log.d("OnSwipeDemo", "Current progress: $progress")
    }

    override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {}
    override fun onTransitionTrigger(
        p0: MotionLayout?,
        p1: Int,
        p2: Boolean,
        p3: Float
    ) {}
})

通过 progress 我们可以做更多自定义的渐变,比如改变状态栏颜色、播放音效等。


五、其他进阶用法

  1. motion:touchRegionId
    如果实际手势要在 cardView 以外的区域触发(比如要监控空白区域的点击/滑动),可以设置 touchRegionId 指向另一个 View 的 ID。

  2. motion:moveWhenScrollAtTop="true"
    如果 cardView 内部是一个可滚动列表,比如 RecyclerView,默认情况下,列表会自己滚动,而 OnSwipe 不会触发。设置 moveWhenScrollAtTop="true" 后,会在列表滑到顶(或底)后再触发 MotionLayout 的过渡。

  3. 双向或多段动画

    • 如果想要上下都能触发动画,可以放两个 <Transition>,一个 dragUp、一个 dragDown,或者用 dragAuto 自动判断方向。
    • 若还需要 KeyFrame 等细节,可以在 <Transition> 里再加 <KeyFrameSet>。
  4. 自定义过渡插值器

    • 可以用 motion:transitionEasing 或在 <OnSwipe> 里定义插值器属性,来改变动画曲线,使之更加灵敏或柔和。
  5. 与 Activity / Fragment 的返回手势结合
    如果布局需要结合系统手势返回,需要确保优先级、滑动区域不冲突,可根据项目需求进行布局或滑动区域的调整。

六、常见问题

  1. 触摸区域不生效

    • 检查 motion:touchAnchorId 是否为当前 ConstraintSet 中实际存在的控件 ID。
    • 若与 RecyclerView 等可滚动控件交互,需要 motion:moveWhenScrollAtTop="true" 或其他处理。
  2. 动画无法到达终点或中途弹回

    • 这是 MotionLayout 根据手势速度和进度做的惯性判断,若想总是“跟手”并一定到达终点,可以在必要时手动调用 motionLayout.transitionToEnd()。
  3. 方向设置不对

    • 如果要上下滑动,却设置了 dragLeft / dragRight,会导致动画无法正常触发或不符合预期。
  4. 需要与嵌套滚动控件配合

    • 如果想在“滑动内容”的同时也“滑动外层布局”,就需要更为复杂的配置,或者在 TransitionListener 中根据滚动距离来调用 setProgress() 自定义逻辑。

总结

  • <OnSwipe> 能让 MotionLayout 的动画与手势滑动结合起来,让用户在拖拽过程中即时看到动画进度的变化,营造出更自然的交互体验。

  • 核心是通过 motion:touchAnchorId + motion:touchAnchorSide + motion:dragDirection 来告诉系统“在哪个控件上进行手势捕捉、以哪个边为锚点、滑动方向为何”。

  • 搭配关键帧(KeyFrame)与 TransitionListener,可以实现极为丰富的交互动画效果。


四、进阶用法

1. 中间状态(KeyFrame / KeyAttributes)

在简单场景下,我们只用 两个 ConstraintSet 就够了。但如果需要更丰富的动画曲线或中间状态(比如:先放大再旋转,最后再位移),可以使用 KeyFrame 系列标签(KeyPosition, KeyAttributes, KeyCycle 等)来在动画过程中的特定进度插入“关键帧”,从而灵活地控制控件属性的变化轨迹。在 MotionLayout 中,关键帧(KeyFrame)是用来在动画过渡过程中的特定进度点插入额外的属性或位置控制,以便实现更丰富、更细腻的动画效果。换句话说,MotionLayout 默认只知道开始约束(ConstraintSetStart)和结束约束(ConstraintSetEnd)这两个状态,并会在这之间进行线性或简单插值;但如果在动画中途需要让某些属性出现突变、转折、循环或者非线性的运动轨迹,就需要用到关键帧。

一、为什么要用关键帧

如果没有关键帧,MotionLayout 会将属性从起始状态平滑插值到结束状态,整个动画路径往往是直线或简单的插值方式;对于大多数简单动画足够。但当我们需要下列更复杂的效果时,就需要关键帧:

  1. 多段动画或中途突变
    例如按钮在过渡一半时先放大,再继续移动到终点。
  2. 非线性或自定义的动画曲线
    例如先快速移动然后慢慢停止,或者让视图在移动过程中做绕弧线的运动。
  3. 循环或波动效果
    例如抖动、脉冲或弹簧效果等。

通过在动画的关键进度点(framePosition)插入 KeyFrame,可以精确控制动画在该时刻或该范围内表现出与默认插值不一样的行为。


二、关键帧种类

MotionLayout 中的关键帧主要分成三大类:

  1. KeyAttributes

    用于在动画的特定进度设置属性(如:透明度 alpha、旋转 rotation、缩放 scaleX/scaleY、颜色 textColor 等),还可以包括视图的布局属性(如 translationX, translationY 等)。

    • 通过在指定的进度(framePosition)定义这些属性值,能够在该进度点上重置或覆盖默认的插值值。
    • 也支持 motion:interpolator 去调整关键帧插值的速率。
  2. KeyPosition

    用于在动画的特定进度设置控件的位置,从而实现更复杂的运动轨迹。

    • 例如在“开始”和“结束”这两点之间,再插入一条“贝塞尔曲线”或特定偏移,从而让视图在过渡过程中弯曲运动或走自定义路径。
    • 常用的属性有 motion:percentX、motion:percentY、motion:pathMotionArc 等。
  3. KeyCycle

    用于在动画的特定进度做周期性/循环性的调整,如抖动、波动、弹簧振荡等效果。

    • 可以指定波形类型(sin、square、triangle 等)以及幅度与频率,来让控件的某个属性(比如 rotation、translationY)在过渡过程中呈现周期波动。

注意:

  • 在 XML 中,这些关键帧的标签分别是 <KeyAttribute>、<KeyPosition>、<KeyCycle>。
  • 关键帧只能写在 <Transition> 标签内部。

三、关键帧的核心属性

不管是 <KeyAttribute>、<KeyPosition> 还是 <KeyCycle>,都需要至少指定以下共同属性:

  1. motion:motionTarget

    • 表示关键帧应用在哪个 View 上,可以是 @+id/viewId 或者 "@string/viewName",也可以使用 OnSwipe 中的 motionTarget 来关联。
  2. motion:framePosition

    • 表示动画进度(0 ~ 100 之间的整数),即该关键帧生效的时间点。0 对应开始状态、100 对应结束状态。
    • 当动画的插值进度到达这个数值时,将会执行(或逐步影响)关键帧中的配置。

除此之外,不同关键帧类型还有各自的专属属性。例如:

  • KeyAttribute 常见属性:

    • android:alpha
    • android:rotation, android:rotationX, android:rotationY
    • android:scaleX, android:scaleY
    • android:translationX, android:translationY
    • android:layout_width, android:layout_height(在某些版本中也支持)
    • motion:interpolator(指定该关键帧插值器)
  • KeyPosition 常见属性:

    • motion:percentX, motion:percentY
      (描述在路径上该控件所在的相对位置百分比)
    • motion:drawPath (用于调试,可视化路径)
    • motion:transitionEasing (插值曲线)
    • motion:transitionPathRotate (跟随路径旋转)
  • KeyCycle 常见属性:

    • motion:waveShape(sin, square, triangle, sawtooth 等)
    • motion:wavePeriod(周期时长)
    • motion:waveOffset(相位偏移)
    • motion:waveAmplitude(振幅)
    • 以及要施加周期性变化的具体属性(android:rotation、android:translationY 等)

四、示例:KeyAttribute 的简单用法

以下示例展示了如何让一个控件在动画过渡过程中,在 50% 进度时临时改变大小和旋转角度。

1. MotionScene 文件
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <!-- 开始、结束 ConstraintSet 略 -->
    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/imageView"
            android:layout_width="100dp"
            android:layout_height="100dp"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent"/>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/imageView"
            android:layout_width="100dp"
            android:layout_height="100dp"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintBottom_toBottomOf="parent"/>
    </ConstraintSet>

    <Transition
        motion:constraintSetStart="@id/start"
        motion:constraintSetEnd="@id/end"
        motion:duration="2000">

        <!-- 在这里定义关键帧 -->
        <KeyFrameSet>
            <!-- 在进度 50% 插入一个 KeyAttribute -->
            <KeyAttribute
                motion:motionTarget="@+id/imageView"
                motion:framePosition="50"
                android:scaleX="2.0"
                android:scaleY="2.0"
                android:rotation="45"/>
        </KeyFrameSet>

    </Transition>
</MotionScene>
2. 运行效果
  • 0% ~ 50%:从左上角移动到某个中间位置,大小逐渐从 1.0 ~ 2.0(视情况插值),并旋转到 45°。
  • 50% ~ 100%:从该“放大旋转”状态再继续移动到右下角,同时按默认插值还原或保持其他属性(如果结束状态未变动,就会保持旋转 45°,或者回到 0°,要看 end 中是否有设置)。

如果 end 里没有指定 rotation,则在动画后半段会保持 45°;如果指定了 rotation="0",则会从 45° 再回到 0°。


五、示例:KeyPosition 的简单用法

让控件在中途偏离默认的线性路径,走一条弧线或自定义曲线。例如,在 50% 时,percentX = 0.5,但 percentY = 0.2,让它在中间稍微往上移动。

<MotionScene ...>

    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="1500">

        <KeyFrameSet>
            <KeyPosition
                motion:motionTarget="@+id/imageView"
                motion:framePosition="50"
                motion:percentX="0.5"
                motion:percentY="0.2"
                motion:transitionEasing="easeInOut"/>
        </KeyFrameSet>

    </Transition>
</MotionScene>

当动画过渡到 50% 时,imageView 的 X 方向处于从 start 到 end 的一半,但 Y 方向只走了 20%(或定义了特殊插值),从而产生上抛式的曲线。


六、示例:KeyCycle 的简单用法

让控件在过渡过程中进行周期性抖动。比如让 imageView 在 Y 方向上进行正弦波动。

<MotionScene ...>

    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="3000">

        <KeyFrameSet>
            <KeyCycle
                motion:motionTarget="@+id/imageView"
                motion:framePosition="50"
                motion:waveShape="sin"
                motion:wavePeriod="0.5"
                motion:waveAmplitude="50"
                android:translationY="0" />
        </KeyFrameSet>

    </Transition>
</MotionScene>
  • waveShape="sin":表示正弦波形。
  • wavePeriod="0.5":周期长度(数值越小,频率越高/波动越快)。
  • waveAmplitude="50":在正弦波上下波动的最大幅度。
  • framePosition="50" 这里并不意味着只在 50% 的地方抖动——KeyCycle 的特点是在整个动画范围内根据设置的周期波形去做插值,只是 framePosition 会用来确定波形的参考点/相位等。

注意:KeyCycle 与 KeyAttribute/KeyPosition 最大的区别在于 KeyCycle 会对属性值进行周期性变化,而不是仅在固定帧施加一次性改变。


七、关键帧插值与冲突处理

  • 当同一个属性在不同关键帧(或 ConstraintSet)里出现配置冲突时,后者会覆盖前者,但确切的行为还与进度插值相关。
  • 当有多个 KeyFrame 同时生效,需要注意 framePosition 的先后和插值曲线,避免动画出现意料之外的跳变。

建议:

  • 关键帧不要定义过多、过于密集,否则难以维护和调试。
  • 在关键帧之前尽量先确认需求逻辑,用可视化工具(如 Android Studio 的 Motion Editor)能更直观地调试关键帧效果。

八、调试 KeyFrame:motion:drawPath

在 <KeyPosition> 上可以加 motion:drawPath="true"(或 "debug") 来显示控件运动路径,方便在开发或调试阶段观察运动轨迹是否符合预期。在运行 App 时会看到一条辅助线/弧线。


九、实践与建议

  1. 保持简单,按需添加
    过多的关键帧会让动画维护复杂,除非确实需要多段动画或非线性效果,否则先尝试只用两个 ConstraintSet + 基本插值即可满足需求。

  2. 优先使用 KeyAttribute + KeyPosition

    • KeyAttribute 可以解决大部分“属性在中途需要变化”的需求。
    • KeyPosition 可以让运动路径更加灵活。
    • KeyCycle 在需要周期性/震荡/波动效果时再使用。
  3. Motion Editor 可视化
    使用 Android Studio 提供的 “Motion Editor” 能直接拖拽、添加 KeyFrame,图形化查看控件轨迹和动画曲线,对于复杂动画非常好用。

  4. 与手势交互结合
    如果还需要拖拽、滑动等实时控制动画进度,可以在 <Transition> 里加 <OnSwipe>,配合关键帧,实现更加丰富的交互动画。


总结

  • 关键帧(KeyFrame) 是 MotionLayout 赋予动画过程更精细控制的核心机制,可以在动画的任意进度点上施加额外的属性设置或运动轨迹控制。

  • 根据需求,常用的关键帧类型有:

    • KeyAttribute:针对视图各种属性的调整;
    • KeyPosition:针对运动路径的插值;
    • KeyCycle:针对属性做周期性或波动式变化。
  • 通过合理地插入关键帧,可以让动画从简单的线性插值变得更灵活、更生动,满足更多 UI/UX 需求。

<OnClick>

<OnClick> 是一种无需写任何 Java/Kotlin 代码、直接在 XML 中通过点击事件来触发动画过渡的方式。它非常适合那些**“单击控件就开始动画”**或“单击后在不同状态间切换”**等场景。下面介绍 <OnClick> 的使用方法、常见属性以及示例。

一、<OnClick> 的作用

  • 监听 View 的点击事件:当指定的目标 View 被点击时,触发对应的过渡动画。
  • 切换 ConstraintSet 状态:可以选择让动画从 start 状态过渡到 end 状态,或者从 end 返回 start,甚至可以直接跳到某个自定义的 ConstraintSet。
  • 无需手动写代码:不用在 Activity/Fragment 里去 findViewById(...) 再写 setOnClickListener(...),而是通过 MotionScene XML 声明式完成。

二、如何在 MotionScene 中添加 <OnClick>

在 MotionScene 文件的 <Transition> 标签内部,可以添加一个或多个 <OnClick>,每个 <OnClick> 用来描述:

  1. 哪一个 View (motion:targetId) 被点击时触发。
  2. 点击行为 (motion:clickAction) - 即要执行哪种动画操作。

一个最简单的示例结构如下:

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <!-- 定义过渡,从 @+id/start 到 @+id/end -->
    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="1000">

        <!-- 在此处添加 OnClick 配置 -->
        <OnClick
            motion:targetId="@+id/button"
            motion:clickAction="transitionToEnd" />

    </Transition>

    <!-- 约束集:start -->
    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/button"
            ... />
    </ConstraintSet>

    <!-- 约束集:end -->
    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/button"
            ... />
    </ConstraintSet>

</MotionScene>

布局文件

<!-- activity_main.xml -->
<androidx.constraintlayout.motion.widget.MotionLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    motion:layoutDescription="@xml/motion_scene">

    <Button
        android:id="@+id/button"
        android:text="点我开始动画"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>

此时,当用户点击按钮 button 时,会从 @+id/start 约束状态执行动画,过渡到 @+id/end 状态。


三、<OnClick> 的常见属性

  1. motion:targetId

    • 指定哪个 View 被点击时触发过渡。
    • 必须在对应的 ConstraintSet 中有相同的 ID。
  2. motion:clickAction

    • 表示点击后执行的操作,常见取值包括:

      • transitionToEnd:从当前状态过渡到 End 状态。
      • transitionToStart:从当前状态过渡到 Start 状态。
      • transitionToState:过渡到指定的另一个 ConstraintSet(需搭配 motion:constraintSetEnd="@+id/xxx" 或额外属性)。
      • toggle:若当前状态是 Start,就过渡到 End;若是 End,就回到 Start。
      • jumpToEnd / jumpToStart:与前面类似,但不带动画,是直接跳过去。
  3. motion:mode (可选)

    • 可以设置为 "toggle",也能实现从 Start ↔ End 来回切换,类似 clickAction="toggle" 的效果。
  4. motion:targetState (可选)

    • 当 clickAction="transitionToState" 时,通过 targetState 指定要过渡到哪个 ConstraintSet。

示例:点击同一个按钮,第二次再点时回到初始状态

<OnClick
    motion:targetId="@+id/button"
    motion:clickAction="toggle" />

四、更复杂的场景示例

1. 多个 <OnClick> 配合使用

有时一个 Transition 中可能有多个可点击控件,分别触发不同的动画。例如:

<Transition
    motion:constraintSetStart="@+id/start"
    motion:constraintSetEnd="@+id/end">

    <!-- 点击按钮1,过渡到 end 状态 -->
    <OnClick
        motion:targetId="@+id/button1"
        motion:clickAction="transitionToEnd" />

    <!-- 点击按钮2,直接跳到 end 状态(无动画) -->
    <OnClick
        motion:targetId="@+id/button2"
        motion:clickAction="jumpToEnd" />

</Transition>
2. 在不同 Transition 中使用 OnClick

如果定义了多个 <Transition>(例如:不同的开始/结束状态组合),可以在每个 <Transition> 中都配置 <OnClick>。不过,需要确保当前的 MotionLayout 处于可触发对应 Transition 的合适状态,否则点击可能无效。

例如,你可能在第一个 Transition 里设置点击某按钮,动画从 start -> middle;在第二个 Transition 里设置点击另外一个按钮,动画从 middle -> end。

3. 调用 transitionToState(...) + <OnClick>

如果想要点击一次就切换到指定的 ConstraintSet,可以写:

<OnClick
    motion:targetId="@+id/myView"
    motion:clickAction="transitionToState"
    motion:targetState="@+id/anotherConstraintSet" />

这样可以从当前状态直接平滑过渡到 @+id/anotherConstraintSet,而不必固定是 start 或 end。


五、<OnClick> 与 代码触发动画的对比

  • XML <OnClick> :配置简单、声明式写法,快速实现“点一下就播放动画”。适合无复杂逻辑或 Demo/简单场景。
  • 代码中调用 motionLayout.transitionToEnd() :更灵活,可基于业务逻辑(如校验、网络请求回调等)来决定何时动画。如果需要自定义条件或 业务逻辑控制,用代码会更合适。

两者并不冲突,可以共存。如果同时定义了 <OnClick> 与 Java/Kotlin 里的监听,都可触发同一个过渡,只是要小心避免重复或冲突。


六、常见问题

  1. 点击后动画没反应?

    • 检查 targetId 是否与布局中的 View ID 对应,并且处于这个 Transition 的可触发范围(即当前状态是否与 constraintSetStart 或 constraintSetEnd 对应)。
    • 确保 MotionLayout 正在使用该 MotionScene,并且 <Transition> 的 constraintSetStart/constraintSetEnd 与实际布局中的 ID 匹配。
  2. 想在点击后根据条件决定跳到 start 还是 end?

    • 可以改用 toggle;或是在 Java/Kotlin 代码中判断,然后手动调用 motionLayout.transitionToStart() 或 transitionToEnd()。
    • <OnClick> 本身比较单一,复杂逻辑时可结合代码。
  3. 多个 <Transition> 冲突

    • 如果在同一个 MotionScene 中定义了多个 <Transition>,且都带有 <OnClick>,当点击发生时,究竟触发哪个 Transition 要看 MotionLayout 当前所处的状态是否匹配该 Transition 的 start / end。
    • 如果无法匹配到任何可执行的 Transition,就不会有动画。
  4. 点击区域过小 / 无法点击

    • 确保被点击的 View(targetId)有可点区域,且不被其他视图覆盖。
    • 若只想监听点击而不想实际展示的 View,可在布局中放一个透明按钮或增大可点击区域(如 android:padding="..." 或 android:minHeight="...")。

七、总结

  • 核心思路:<OnClick> 通过声明式配置,告诉 MotionLayout“当这个 View 被点击时,我要把动画过渡到哪个状态”。
  • 使用场景:非常适合简单的点击触发动画、切换界面状态、演示性动画等。
  • 配置关键:在 <Transition> 中添加 <OnClick>,指定 targetId 和 clickAction;必须保证 View ID 与布局中的 View 对应,并匹配到正确的 start/end 状态。
  • 进阶用法:如果需要更复杂的点击逻辑(多重判断、网络回调等),可结合 MotionLayout 的代码接口来完成; <OnClick> 也能与 KeyFrame、OnSwipe 等交互式动画结合,打造更加丰富的体验。

五、一个最简单的动画

下面汇总一下基础的案例,让一个按钮从左上角移动到右下角并改变大小。

1. 主布局 (activity_main.xml)

<androidx.constraintlayout.motion.widget.MotionLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/motion_scene">

    <Button
        android:id="@+id/button"
        android:text="Click Me!"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>

2. MotionScene (motion_scene.xml)

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!-- 初始状态 -->
    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>
    </ConstraintSet>

    <!-- 结束状态 -->
    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/button"
            android:layout_width="200dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>
    </ConstraintSet>

    <!-- 动画过渡配置 -->
    <Transition
        app:constraintSetStart="@id/start"
        app:constraintSetEnd="@id/end"
        app:duration="800">

        <!-- 点击 Button 时,启动这个过渡 -->
        <OnClick
            app:targetId="@+id/button"
            app:clickAction="transitionToEnd" />
    </Transition>
</MotionScene>

当用户点击按钮时,按钮会从左上角移动到右下角并变大,动画时长为 800ms。


六、总结

  1. “声明式”动画配置
    与传统的逐帧动画或代码里写 ObjectAnimator 相比,MotionLayout 更偏“声明式”:我们在 XML 里配置布局状态、约束和动画属性,让系统自动补帧并执行动画。

  2. 可视化编辑器
    在较新版本的 Android Studio 中,可以使用 “Motion Editor” 来可视化地编辑 MotionScene 文件、添加/修改 ConstraintSet、添加 KeyFrame 等,大大提升开发效率。

  3. 分场景使用

    • 简单的单一控件动画,可能用 ObjectAnimator 就足够了。
    • 多控件、复杂布局、可交互拖拽等情况,MotionLayout 会让动画的维护更容易、更直观。
  4. 性能与流畅度

    • MotionLayout 属于在 ConstraintLayout 基础上的扩展,性能表现相对可靠。
    • 但对于极其频繁、重度的动画场景,仍需注意优化,以及适时与其他动画机制结合(如 RecyclerView 动画、粒子系统等)。
  5. MotionLayout准确的定位:

MotionLayout
=
ConstraintLayout
+
状态机
+
关键帧系统
+
属性动画
+
手势驱动
+
路径控制
+
多View联动

或者更简单地说:

MotionLayout 不是单纯的动画框架,而是一个基于 ConstraintSet 状态切换的声明式运动系统。开发者描述“状态”和“状态之间如何过渡”,MotionLayout 负责计算运动路径、关键帧、属性变化以及手势驱动过程。

这也是为什么 Google 后来很多复杂交互(折叠头部、播放器详情页、BottomSheet、搜索栏展开收起)都优先推荐 MotionLayout,而不是大量堆叠 ObjectAnimator 或 TransitionManager。