MotionLayout 运动布局
breewf

MotionLayout

什么是MotionLayout?

MotionLayout(运动布局)

MotionLayout is a new class available in the ConstraintLayout 2.0 library to help Android developers manage motion and widget animation in their application.

MotionLayout 是 ConstraintLayout 2.0 库中提供的一个新类,用于帮助 Android 开发人员在其应用程序中管理运动和组件动画

正如其名,MotionLayout 首先是一个布局,它实际上是 ConstraintLayout 的一个子类,并建立在其丰富的布局能力之上(可以使用 ConstraintLayout 的各种特性)。

创建 MotionLayout 是为了在布局转换和复杂运动处理之间架起一座桥梁,您可以将其视为属性动画框架、TransitionManager 和 CoordinatorLayout 之间的组合。它允许您描述两个布局之间的转换(如 TransitionManager)。此外,它本质上支持可查找的转换,转换可以完全由触摸驱动,并立即转换到它的任何点(MotionLayout 让布局元素能够在动画过渡期间也能够响应用户的交互)。它支持触摸处理和关键帧,允许开发人员根据自己的需要轻松自定义过渡。

MotionLayout 是完全声明性的,也就是说您可以使用 XML 描述任何转换而不需要代码(如果需要通过代码表达运动,现有的属性动画框架已经提供了一种很好的方法),无论复杂程度如何。

集成与简单使用

集成

添加 ConstraintLayout 依赖项。要在项目中使用 MotionLayout,请向应用的 build.gradle 文件添加 ConstraintLayout 2.0 依赖项。如果您使用了 AndroidX,请添加以下依赖项:

1
2
3
4
dependencies {
// 最新版本为 2.1.0
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1'
}

使用

创建 MotionLayout 文件。MotionLayout 是 ConstraintLayout 的子类,因此您可以通过替换布局资源文件中的类名称,将任何现有的 ConstraintLayout 转换为 MotionLayout,如下面的示例所示:

1
2
3
4
<!-- before: ConstraintLayout -->
<androidx.constraintlayout.widget.ConstraintLayout .../>
<!-- after: MotionLayout -->
<androidx.constraintlayout.motion.widget.MotionLayout .../>

ConstraintLayout 和 MotionLayout 在 XML 级别上的主要区别是,MotionLayout 所做的实际描述不一定包含在布局文件中。相反,MotionLayout 通常将所有这些信息保存在它引用的单独 XML 文件(一个 MotionScene )中,并且这些信息将优先于布局文件中的信息。这样,布局文件可以只包含视图及其属性,而不包含它们的位置或移动

在布局中使用 app:layoutDescription 声明一个运动场景文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/scene_01"
tools:showPaths="true">

<View
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@color/colorAccent"
android:text="Button" />

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

创建 MotionScene。MotionScene 是一个 XML 资源文件,其中包含相应布局的所有运动描述。为了将布局信息与运动描述分开,每个 MotionLayout 都引用一个单独的 MotionScene,通常存储在 res/xml 目录下。

一个 MotionScene 文件通常包含指定动画所需的所有内容。

  • 使用的 ConstraintSets(约束集)
  • 这些 ConstraintSets(约束集) 之间的转换
  • 关键帧,点击、触摸处理等

简单案例

要实现上图所示的运动,可以有两种方案。

实现1

引用已有的 layouts

一个指向初始的 layout(view在屏幕左端),一个指向结束的 layout(view在屏幕的右端),我们需要定义两个 layout。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// view在左端的layout: motion_01_cl_start
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
android:background="@color/colorAccent"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>


// view在右端的layout: motion_01_cl_end
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="8dp"
android:background="@color/colorAccent"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

然后定义一个 MotionLayout 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/scene_01"
tools:showPaths="true">

<View
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@color/colorAccent"
android:text="Button" />

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

注意这个 layout 文件指向了一个 MotionScene 文件—scene_01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto">

<Transition
motion:constraintSetEnd="@layout/motion_01_cl_end"
motion:constraintSetStart="@layout/motion_01_cl_start"
motion:duration="1000">
<OnSwipe
motion:dragDirection="dragRight"
motion:touchAnchorId="@+id/button"
motion:touchAnchorSide="right" />
</Transition>

</MotionScene>

scene_01 通过指定开始和结束的 ConstraintSet (motion_01_cl_start 和 motion_01_cl_end),规定了过渡动画。

实现2

将 ConstraintSets 直接定义在 MotionScene 中

MotionLayout 同样支持直接在 MotionScene 中定义 ConstraintSets,我们定义一个与实现 1 中一样的 MotionLayout 文件,只需要引用 scene_02.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.motion.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/motionLayout"
app:layoutDescription="@xml/scene_02"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/button"
android:background="@color/colorAccent"
android:layout_width="64dp"
android:layout_height="64dp"
android:text="Button" />

</android.support.constraint.motion.MotionLayout>

在 scene_02.xml 中,Transition 标签内部是一致的,区别在于我们直接将 start 和 end 的 Constrait 定义在了当前文件中,并由一个 ConstraintSet 标签包含着。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="utf-8"?>
<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="1000">
<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"
android:layout_marginStart="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
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"
android:layout_marginEnd="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

</MotionScene>

方案对比

上述两种实现方式,实现1布局文件较多,实现2只需要一个文件,就可以包括所有的 ConstraintSet,相对更清晰。另外实现1的方式在 xml 布局预览时无法预览 start 和 end 场景(当然也无法预览动画),而实现2可以。

MotionLayout 配置项

  • app:applyMotionScene=”boolean” 表示是否应用 MotionScene。此属性的默认值为 true
  • app:showPaths=”boolean” 表示在运动进行时是否显示运动路径。此属性的默认值为 false
  • app:progress=”float” 可让您明确指定进度。您可以使用从 0(转换开始)到 1(转换结束)之间的任意浮点值
  • app:currentState=”reference” 可以指定具体的 ConstraintSet
  • app:motionDebug 可让您显示与运动有关的其他调试信息。可能的值为“SHOW_PROGRESS”、“SHOW_PATH”或“SHOW_ALL”

MotionScene

ConstraintSet

它是存放 View 约束和属性的的集合,描述 View 约束和属性是通过 Constraint 标签。一般情况下一个动画有两个节点 start 和 end,start 节点作为动画开始时的状态,end 节点作为动画结束时的状态。

Constraint 的属性完全等同于 ConstraintLayout 的属性。

除了位置和边界之外,可配置的属性包括:

  • alpha
  • visibility
  • elevation
  • rotation、rotationX、rotationY
  • translationX、translationY、translationZ
  • scaleX、scaleY

自定义属性:您可以使用 CustomAttribute 来实现自定义的属性,一个 CustomAttribute 本身包含两个属性:

  • motion:attributeName 是必需属性,并且属性需要在类中提供相应的 get、set 方法。例如,backgroundColor 受支持,因为我们的视图具有基本的 getBackgroundColor() 和 setBackgroundColor() 方法。
  • motion:customColorValue(或 Integer、Float、String、Dimension、Boolean)
    • motion:customColorValue 适用于颜色
    • motion:customIntegerValue 适用于整数
    • motion:customFloatValue 适用于浮点值
    • motion:customStringValue 适用于字符串
    • motion:customDimension 适用于尺寸
    • motion:customBoolean 适用于布尔值

ConstraintOverride:对于非 layout_constraintXXX_toXXXOf 的约束,可以使用 ConstraintOverride 来直接覆写,这样可以少写很多重复的约束。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<ConstraintSet android:id="@+id/start">

<ConstraintOverride
android:id="@id/button1"
android:layout_width="64dp" />

</ConstraintSet>

<ConstraintSet android:id="@+id/end">

<ConstraintOverride
android:id="@id/button1"
android:layout_width="100dp" />

</ConstraintSet>

OnClick

点击事件。

  • app:targetId 目标 View 的 id,点击操作的对象
  • app:clickAction 设置点击时执行的动作,此属性可以设置以下几个值
    • toggle:默认值。在 Start 场景和 End 场景之间切换。
    • transitionToStart:过渡到 Start 场景。
    • transitionToEnd:过渡到 End 场景。
    • jumpToStart:跳到 Start 场景(不执行过渡动画)。
    • jumpToEnd:跳到 End 场景(不执行过渡动画)。

OnSwipe

由用户滑动触发,它会根据用户滑动行为调整动画的进度。

  • app:touchAnchorId 目标 View 的 id,设置滑动操作要关联到的对象
  • app:touchAnchorSide 用户滑动界面时 MotionLayout 将尝试在 touchAnchorId 指定的视图和用户手指之间保持恒定的距离,而此属性指定的是手指和 view 的哪一侧保持恒定的距离。有以下可选值:
    • top
    • start
    • left
    • right
    • end
    • bottom
    • middle
  • app:dragDirection:设置拖动的方向。例如,motion:dragDirection=”dragRight” 表示当您向右拖动时,进度会增加。有以下可选值:
    • dragUp:手指从下往上拖动(↑)
    • dragDown:手指从上往下拖动(↓)
    • dragLeft:手指从右往左拖动(←)
    • dragStart:手指从右往左拖动(←)
    • dragRight:手指从左往右拖动(→)
    • dragEnd:手指从左往右拖动(→)
  • app:maxVelocity:设置动画在拖动时的最大速度(单位:像素每秒 px/s)。
  • app:maxAcceleration:设置动画在拖动时的最大加速度(单位:像素每二次方秒 px/s^2)。
  • app:dragScale: 控制视图相对于滑动长度的移动距离。默认值为 1,表明视图移动的距离应与滑动距离一致。如果 dragScale 小于 1,视图移动的距离会小于滑动距离(例如,dragScale 为 0.5 意味着如果滑动移动 4 厘米,目标视图会移动 2 厘米)。如果 dragScale 大于 1,视图移动的距离会大于滑动距离(例如,dragScale 为 1.5 意味着如果滑动移动 4 厘米,目标视图会移动 6 厘米)。

Transition

  • Transition 包含运动的基本定义。
    • constraintSetStart 定义运动的初始状态,这可以是 ConstraintSet 的 id,也可以是布局文件(要指定 ConstraintSet,请将此属性设置为@+id/constraintSetId;要指定布局,请设置为@layout/layoutState)。
    • constraintSetEnd 定义运动的最终状态,取值同 constraintSetStart
    • duration 定义运动的持续时间,以毫秒为单位
    • motionInterpolator 定义运动插值器
      • easeIn 缓入
      • easeOut 缓出
      • easeInOut 缓入缓出
      • linear 线性
      • bounce 弹跳效果
    • autoTransition 自动过渡
      • none 不运动
      • jumpToStart 直接跳到开始
      • jumpToEnd 直接跳到结束
      • animateToStart 执行动画到开始
      • animateToEnd 执行动画到结束

KeyFrameSet

默认情况下,运动会从初始状态进行到结束状态。通过使用 KeyFrameSet 您可以构建更复杂的动作,我们可以在动画运动过程中的某些点设置特定的约束或属性,MotionLayout 从起始状态到这些节点,再到结束状态,使视图平滑地过渡。
KeyFrameSet 中可以包含 KeyPosition、KeyAttribute、KeyCycle、KeyTimeCycle、KeyTrigger。

KeyFrameSet 可以实现更精确的动画控制

KeyPosition

指定运动序列中特定时刻的视图位置。此属性用于调整运动的默认路径。

用 KeyPosition 来实现曲线运动。

图中的菱形点就是我们用 KeyPosition 修改的点,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<OnClick motion:targetId="@id/box"/>
<KeyFrameSet>
<KeyPosition
motion:framePosition="50"
motion:motionTarget="@id/box"
motion:keyPositionType="parentRelative"
motion:percentY="0.3"
motion:percentX="0.8"/>
</KeyFrameSet>
</Transition>

  • framePosition 表示动画的进度,取值范围为[0,100]。上面的50就表示动画进度执行50%的地方
  • motionTarget 表示修改路径的视图 id
  • percentY 和 percentX 就是相对参考系的纵向和横向的比例
  • keyPositionType 表示坐标系类型,取值可以是 parentRelative、pathRelative、deltaRelative

重点介绍一下 keyPositionType

  • parentRelative

表示以MotionLayout 布局为参考系,布局左上角为(0,0),右下角为(1,1) 那么motion:percentX=”0.8”,motion:percentY=”0.3”就是(0.3,0.8)的位置,如下图所示:

按我们上面说的(0.3,0.8)应该是图1两条灰色的焦点,但图1和实际的点有些偏差。我们用KeyPosition 指定点时,是指定视图的中心点,图1是理想状态,如果我们的视图无限小,那就是图1 情况。事实上我们视图不可能无限小,真实的情况应该是图2 所示。

  • deltaRelative

此类型下,视图的起始点坐标为(0,0), 终点坐标为(1,1),如图所示:

1
2
3
4
5
6
7
8
9
<KeyFrameSet>
<KeyPosition
motion:framePosition="50"
motion:motionTarget="@id/box"
motion:keyPositionType="deltaRelative"
motion:percentY="0.5"
motion:percentX="0.8"/>
</KeyFrameSet>

  • pathRelative

起始点(0,0)还是视图开始位置,视图的终点位置是(1,0),如下图:

Y 轴正方向是,X 轴顺时针 90 度的方向。

KeyAttribute

keyAttribute 它可以让我们改变在动画的过程中某个时刻的属性。支持的属性如下 android:visibility, android:alpha, android:elevation, android:rotation, android:rotationX, android:rotationY, android:scaleX, android:scaleY, android:translationX, android:translationY, android:translationZ。如果是自定义的属性,可以使用 KeyAttibute 的子元素 CustomAttribute。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 <Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="2000">
<OnClick motion:targetId="@id/box"/>
<KeyFrameSet>
<KeyAttribute
motion:framePosition="50"
motion:motionTarget="@id/box"
android:rotationY="180"
android:alpha="0"/>
</KeyFrameSet>
</Transition>

上面我们在 KeyFrameSet 添加了一个 KeyAttribute,并指定了属性android:rotationY=”180” 和 android:alpha=”0”,其运行效果如下:

KeyCycle

可以让视图在动画的过程中按照一些周期函数,周期性的改变其属性值。

上图演示是在动画[0%,80%] 这个过程中以 sin 这个周期函数,周期性的改变 android:translationY 属性。

1
2
3
4
5
6
7
<KeyCycle
motion:framePosition="80"
motion:motionTarget="@+id/box2"
motion:wavePeriod="1"
motion:waveShape="sin"
motion:waveOffset="10"
android:translationY="50dp"/>

motion:framePosition=”80” 表示 KeyCycle 作用范围到动画的 80%,motion:wavePeriod 表示运动的周期数,motion:waveShape 表示周期的类型,这是指定的是 sin,就会按 sin 周期函数变化(可指定 sin、cos、bounce、square、triangle、sawtooth、reveresSawtooth)。motion:waveOffset 表示偏移量。

KeyTimeCycle

在关键帧上按照一些周期函数,周期性的改变其属性值,如下图所示:

与 KeyCycle 比较,KeyTimeCycle 在帧上做周期性,KeyCycle 是在动画过程中做周期性。

KeyTrigger

在动画的过程中可以触发视图中的函数。例如我们自定义一个 View,在这个类中我们定义两个公开的函数 who() 和 where()。

1
2
3
4
5
6
7
8
9
10
public class KeyTriggerTextView extends androidx.appcompat.widget.AppCompatTextView {

public void who() {
setText("我是谁");
}

public void where() {
setText("我在哪");
}
}

KeyTrigger 设置代码如下:

1
2
3
4
5
6
7
8
<KeyTrigger
motion:framePosition="20"
motion:motionTarget="@id/key_trigger_tv"
motion:onCross="who" />
<KeyTrigger
motion:framePosition="80"
motion:motionTarget="@id/key_trigger_tv"
motion:onCross="where" />

framePosition 动画的进度,motionTarget 作用的目标, motion:onCross 要触发的函数名(其中 onCross 可取值 onCross、onPositiveCross、onNegativeCros)。

onCross 是动画不管是正向还是反向,只要到达设置的 framePosition 就会执行函数,还有两个函数也会触发, onPositiveCross 只有正向执行动画是到达设置的 framePosition 才会执行,而 onNegativeCros 则反之。

Arc Motion

在 ConstraintLayout 2.0.0 alpha 2 中,引入了一种实现完美圆弧运动的新方案——而且它更加容易使用。你只需要添加 motion:pathMotionArc 属性,从而让默认的线形运动 (linear motion) 切换到弧线运动 (arc motion) 。

让我们来看一个简单的例子,开始状态是屏幕的右下,结束的位置是屏幕的顶部并且水平居中。添加下面这个属性就可以产生弧线运动:

motion:pathMotionArc=”startHorizontal”

如果把属性换成:

motion:pathMotionArc=”startVertical”

就会改变弧线的方向:

你仍然可以使用位置关键帧来创造更复杂的弧线路径。如下:

它是通过在动画中添加一个垂直居中的位置关键帧来实现的:

1
2
3
4
5
<KeyPosition
motion:keyPositionType="parentRelative"
motion:percentY="0.5"
motion:framePosition="50"
motion:motionTarget="@id/button"/>

通过设置 motion:pathMotionArc 属性,还可以在该场景中使用关键帧来更改圆弧的方向。属性可以是 flip (翻转当前的圆弧方向)、none (还原为线性运动),也可以是 startHorizontal 或 startVertical。

1
2
3
4
5
6
<KeyPosition
motion:keyPositionType="parentRelative"
motion:pathMotionArc="flip"
motion:percentY="0.5"
motion:framePosition="50"
motion:motionTarget="@id/button"/>

1
2
3
4
5
6
<KeyPosition
motion:keyPositionType="parentRelative"
motion:pathMotionArc="none"
motion:percentY="0.5"
motion:framePosition="50"
motion:motionTarget="@id/button"/>

Easing

时间模型(Easing)

你可以使用 motion:transitionEasing 属性来改变动画改变的速率。你可以将这个属性使用在 ConstraintSet 或关键帧,它接受这些值:standard, accelerate, decelerate, linear。

  • Standard easing

通常用于在非触摸驱动的动画中。它最适合于开始和结束都是静止状态的元素。

  • Accelerate easing

加速通常用于一个元素移出屏幕。

  • Decelerate easing

减速通常用于一个元素进入屏幕。

其它

ViewTransition

详情请参考官方文档:

ViewTransition

ViewTransition 译文

MotionEffect

详情请参考官方文档:

MotionEffect

MotionEffect 译文

详情请参考官方文档:

Carousel

Carousel 译文

MotionLabel

MotionLabel

MotionLayout 工作台

MotionLayout 是 ConstraintLayout 的子类,所以可以放心的将 ConstraintLayout 转换为 MotionLayout。

打开布局,在 Component Tree 栏选中选择 ConstraintLayout,右击在菜单栏中点击 Covert to MotionLayout 选项即可将 ConstraintLayout 转换为 MotionLayout。

通过此操作,会将 ConstraintLayout 重命名为 MotionLayout,并在布局下自动添加 app:layoutDescription=”@xml/xxx_scene” 并自动创建 xxx_scene.xml 文件(该文件为一个 MotionScene 文件,文件内容如下代码)。xxx 即为当前 xml 的名称。当然,您也可以手动重命名并且手动创建 MotionScene 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<MotionScene
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">

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

<ConstraintSet android:id="@+id/start">
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
</ConstraintSet>
</MotionScene>

Android studio 中 MotionLayout 的可视化的动画编辑工具,如下图所示:

  1. 普通状态,选中后预览视图显示原始的状态(即界面上所包含的所有控件)
  2. 表示 id=”start” 的 ConstraintSet,选中后预览视图显示此约束集的布局
  3. 表示 id=”end” 的 ConstraintSet,选中后预览视图显示此约束集的布局
  4. 表示从 start 约束集到 end 约束集的转场,对应 xml 中的 Transition 标签
  5. 创建一个新的 ConstraintSet
  6. 创建一个新的 Transition
  7. 创建触发转场的行为,是点击 click 还是滑动 swipe
  8. 点击后有个帮助教程
  9. 表示约束集的元素

点击上图中 4 这条转场线,还可以对 Transition 进行预览和编辑。

我们点击选中①,下面会出现一个时间轴的工具(可以逐帧拖动预览),点击②处播放按钮就可以直接预览动画效果(还有各种倍速可选),点击③处就可以看到添加 KeyFrameSet 的子元素的选项,可以进行添加 KeyPosition、KeyAttribute、KeyCycle、KeyTimeCycle、KeyTrigger。

MotionLayout 代码相关

场景切换

1
2
3
4
mMotionLayout.transitionToStart();
mMotionLayout.transitionToEnd();

mMotionLayout.transitionToState(R.id.end);

设置进度

1
mMotionLayout.setProgress(50);

设置动画时间

1
2
// 设置0无效,源码中最小值为8
mMotionLayout.setTransitionDuration(300);

设置 Listener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 设置监听,监听进度、状态
mMotionLayout.setTransitionListener(new MotionLayout.TransitionListener() {
@Override
public void onTransitionStarted(MotionLayout motionLayout, int i, int i1) {
Log.i(TAG, "onTransitionStarted");
}

@Override
public void onTransitionChange(MotionLayout motionLayout, int i, int i1, float v) {
Log.i(TAG, "onTransitionChange: " + v);
}

@Override
public void onTransitionCompleted(MotionLayout motionLayout, int i) {
Log.i(TAG, "onTransitionCompleted");
}

@Override
public void onTransitionTrigger(MotionLayout motionLayout, int i, boolean b, float v) {
Log.i(TAG, "onTransitionTrigger: " + v);
}
});

// 添加和移除监听
mMotionLayout.addTransitionListener();
mMotionLayout.removeTransitionListener();

几个主要的类

  • MotionLayout(androidx.constraintlayout.motion.widget.MotionLayout)
  • MotionScene (androidx.constraintlayout.motion.widget.MotionScene)
  • MotionController (androidx.constraintlayout.motion.widget.MotionController)

主要的入口方法

具体请看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 加载 LayoutDescription 
// androidx.constraintlayout.motion.widget.MotionLayout#loadLayoutDescription

public void loadLayoutDescription(int motionScene) {

}


// 创建、加载场景
// androidx.constraintlayout.motion.widget.MotionScene#MotionScene(android.content.Context, androidx.constraintlayout.motion.widget.MotionLayout, int)

// 加载、初始化
// androidx.constraintlayout.motion.widget.MotionLayout#setupMotionViews

// androidx.constraintlayout.motion.widget.MotionController#setup

// androidx.constraintlayout.motion.widget.MotionLayout.Model#build

// 调试模式下的绘制
// androidx.constraintlayout.motion.widget.MotionLayout.DevModeDraw

MotionLayout 使用相关问题

Transition 成对与切换相关问题

同一个 MotionScene 写多组 Transition,且这些 Transition 对应的 constraintSetStart 和 constraintSetEnd 是同一对场景(如均为 start 和 end),则多组 Transition 内的 OnClick 事件有效(但是因为是同一对场景,各个点击事件会互相影响,需要注意且不建议这么写–概括就是不要用多个点击事件控制同一组场景)。如果多组 Transition 对应的场景不是同一对场景(比如第一对 Transition 对应的是 start 和 end,第二对 Transition 对应的是 start2 和 end2,则非第一对 Transition 下的 OnClick 事件无效)。解决这种问题的方案是:不在 Transition 中定义 OnClick,使用代码定义点击事件,然后使用代码来切换场景)。【具体参见 demo 中的 MotionActivityArcMotion 使用】

可以使用代码来判断当前是哪个场景:

1
2
3
4
5
6
7
8
// 判断当前场景是否是 end2
boolean isEndState = mMotionLayout.getCurrentState() == R.id.end2;
if (!isEndState) {
// 如果不是 end2,比如可能是 end,则先切换到 start2,确保和 end2 成对
mMotionLayout.transitionToState(R.id.start2);
}
boolean isStartState = mMotionLayout.getCurrentState() == R.id.start2;
mMotionLayout.transitionToState(isStartState ? R.id.end2 : R.id.start2);

在一个 MotionScene 中定义多个[多组]场景时,需要在每个场景对每个控件进行充分约束,确保该场景下的控件位置是符合预期的,而不能省略对某个控件的约束,否则就会导致在切换某个场景时控件位置发生不符合预期的变化。

如果使用一个 MotionLayout 布局(一个 MotionScene)定义多组(一个开始和一个结束为一对,多对动画为多组)动画,一是需要注意在每个场景对每个控件的充分约束,二是需要注意每个场景切换对其它场景带来的影响。即便是这样,每个场景的状态仍是不可控的。比如现在有两个场景,场景1是有开始和结束两种状态的,场景2也是有开始和结束两种状态的,他们所有的状态组合是4种,如果我们只定义了默认的两种,显然是覆盖不全的,这就会造成状态的切换可能不符合预期,但是如果我们定义4种状态,再由代码去判断将会非常繁琐,同时场景越多,组合也会爆炸式增长(四个场景就有4X4=16种组合),显然是不符合实际也不能这么做。

所以,建议是:

  • 使用 ViewTransition
  • 使用多个 MotionLayout 布局,各自做各自的事情,互不干扰

使用 progress 调节进度及场景立即切换问题

我们知道,MotionLayout 是状态(场景)之间平滑过渡的布局,如果有多个场景,且在某个场景切换下我不想要平滑的过渡而是想要立即完成,该怎么设置呢?

首先运动布局有切换场景的方法:transitionToState() ,另外既然是动画,那我们再设置动画的时间为0不就可以实现立即切换场景了吗?

1
2
mMotionLayout.setTransitionDuration(0);
mMotionLayout.transitionToState(R.id.progress_end);

想法是美好的,但是设置后发现并不能立即切换到目标状态,还是有动画的。通过 setTransitionDuration() 的源码发现,里边最小的时间值为8,所以设置为0是无效的。那么我们就没办法设置了吗?

当然不是,既然场景的切换是平滑过渡的,同时也是有进度的,而且是可以通过设置进度来来控制场景状态的,那么直接设置进度为1(或者为0,根据需求)是不是就可以了?

1
2
mMotionLayout.transitionToState(R.id.progress_end);
mMotionLayout.setProgress(1);

是的,结果是符合我们预期效果的,所以可以使用 setProgress() 来实现场景状态的无动画切换