ConstraintLayout2.x之MotionLayout

MotionLayout的背景

简介

       MotionLayout 提供了一个丰富的动画系统来协调多个视图之间的动画效果。MotionLayout 基于 ConstraintLayout,并在其之上进行了扩展,允许你在多组约束 (或者 ConstraintSets) 之间进行动画的处理。你可以对视图的移动、滚动、缩放、旋转、淡入淡出等一系列动画行为进行自定义,甚至可以定义各个动画本身的自定义属性。它还可以处理手势操作所产生的物理移动效果,以及控制动画的速度。使用 MotionLayout 构建的动画是可追溯且可逆的,这意味着可以随意切换到动画过程中任意一个点,甚至可以倒着执行动画效果。

       Android Studio 集成了 Motion Editor (动作编辑器),可以利用它来操作 MotionLayout 对动画进行生成、预览和编辑等操作。这样一来,在协调多个视图的动画时,就可以做到对各个细节进行精细操控。

动画的演进

       目前的动画主要分为三种:View动画、属性动画、过渡动画。

       View动画 Animation 是在API 1很早就已经出现,但现在也基本比较少使用它们,因为使用属性动画也可以完全实现同样的效果。比如 AlphaAnimation、ScaleAnimation 等。

       属性动画是在API 11时出现,是目前最常见和使用的动画,可以很方便的实现控件的透明度、平移、缩放、旋转等动画效果。

       过渡动画是在API 18时引入,它主要是用于在两种场景或两种状态之间的切换。

属性动画与过渡动画的对比

       先用属性动画实现一个简单的例子,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher"
tools:ignore="ContentDescription" />
</FrameLayout>
1
2
3
4
5
6
7
8
9
final View root = findViewById(R.id.root);
final ImageView image = findViewById(R.id.image);
image.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int distance = root.getWidth() - image.getWidth();
image.animate().translationX(distance).start();
}
});

       效果如下所示:

       功能非常简单,点击时使用属性动画将ImageView从左边移动到右边,使用属性动画实现这个动画经过了两个步骤:计算图片从左边移动到右边的距离、创建属性动画执行动画。

       如果我们想直接通过改变ImageView的属性实现这个效果,代码和效果如下:

1
2
3
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) image.getLayoutParams();
lp.gravity = Gravity.END;
image.setLayoutParams(lp);

       修改了ImageView的 gravity 属性,但是这样是没有动画效果的,会直接闪现过去,再加上一句代码:

1
2
3
4
5
// 添加这句代码,在修改控件参数之前
TransitionManager.beginDelayedTransition((ViewGroup) image.getParent());
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) image.getLayoutParams();
lp.gravity = Gravity.END;
image.setLayoutParams(lp);

       运行程序可以发现实现效果和属性动画一样有了平移动画的效果。通过加上一句 TransitionManager.beginDelayedTransiton() 就实现了过渡动画的平移效果,从一个状态过渡到另一个状态。

       下面再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...>

<ImageView
android:id="@+id/image1"
... />

<ImageView
android:id="@+id/image2"
... />

<ImageView
android:id="@+id/image3"
... />
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
// 属性动画
final ImageView image = findViewById(R.id.image2);
image.animate()
.scaleX(2.0f)
.scaleY(2.0f)
.start();

// 过渡动画
TransitionManager.beginDelayedTransition((ViewGroup) image.getParent());
ViewGroup.LayoutParams lp = image.getLayoutParams();
lp.width *= 2;
lp.height *= 2;
image.setLayoutParams(lp);

       属性动画效果如下:

       过渡动画效果如下:

       过渡动画的实现本质可以简单分为两个步骤:

  1. 定义两个场景之间的过渡,有开始场景和结束场景,会记录两个场景控件的各种参数;
  2. 创建动画并执行动画,有了上一步记录的控件参数,就可以创建执行动画的参数。

过渡动画的限制

       看如下过渡动画的示例:

       相关代码如下:

       activity_film.xml:

1
2
3
4
5
6
7
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">

<include layout="@layout/film_start_scene" />
</androidx.constraintlayout.widget.ConstraintLayout>

       film_start_scene.xml:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/image_film_cover"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:src="@drawable/film_cover"
app:layout_constraintBottom_toBottomOf="@+id/background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/background" />

<TextView
android:id="@+id/text_film_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:paddingVertical="8dp"
android:textColor="@android:color/white"
android:textSize="14sp"
app:layout_constraintStart_toEndOf="@id/image_film_cover"
app:layout_constraintTop_toTopOf="@id/image_film_cover" />

<RatingBar
android:id="@+id/rating_film_rating"
style="?attr/ratingBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:numStars="5"
android:paddingVertical="8dp"
android:progressTint="#FFD600"
app:layout_constraintStart_toStartOf="@+id/text_film_title"
app:layout_constraintTop_toBottomOf="@id/text_film_title" />

<TextView
android:id="@+id/film_description_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@string/file_description_title"
android:textColor="@color/colorPrimary"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/background" />

<TextView
android:id="@+id/text_film_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/film_description_title" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_favourite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_bookmark"
android:tint="#FFD600"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="@id/background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/background" />
</merge>

       film_end_scene.xml:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<ImageView
android:id="@+id/image_film_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:src="@drawable/film_cover"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/text_film_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:paddingVertical="8dp"
android:textColor="@android:color/white"
android:textSize="14sp"
app:layout_constraintStart_toEndOf="@id/image_film_cover"
app:layout_constraintTop_toTopOf="@id/image_film_cover" />

<RatingBar
android:id="@+id/rating_film_rating"
style="?attr/ratingBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:numStars="5"
android:paddingVertical="8dp"
android:progressTint="#FFD600"
app:layout_constraintStart_toStartOf="@+id/text_film_title"
app:layout_constraintTop_toBottomOf="@id/text_film_title" />

<TextView
android:id="@+id/film_description_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@string/file_description_title"
android:textColor="@color/colorPrimary"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/background" />

<TextView
android:id="@+id/text_film_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/film_description_title" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_favourite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_bookmark"
android:tint="#FFD600"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</merge>
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
public class FilmActivity extends AppCompatActivity implements View.OnClickListener  {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_film);

bindData();
}

private void bindData() {
findViewById(R.id.image_film_cover).setOnClickListener(this);
((RatingBar) findViewById(R.id.rating_film_rating)).setRating(4.5f);
((TextView) findViewById(R.id.text_film_title)).setText(R.string.film_title);
((TextView) findViewById(R.id.text_film_description)).setText(R.string.film_description);
}

private boolean toggle = true;

@Override
public void onClick(View v) {
ViewGroup root = findViewById(R.id.root);
Scene startScene = Scene.getSceneForLayout(root, R.layout.film_start_scene, this);
Scene endScene = Scene.getSceneForLayout(root, R.layout.film_end_scene, this);
if (toggle) {
TransitionManager.go(endScene);
} else {
TransitionManager.go(startScene);
}

// 需要重新绑定数据,否则点击后会无法切换场景
// TransitionManager.go()切换场景后会将当前场景的控件全部移除替换为结束场景的控件对象
// 两个场景的对象不一样,所以需要重新绑定
// 虽然会重复的创建对象,但不需要过于担心性能问题
bindData();
toggle = !toggle;
}
}

       如果使用之前讲的 TransitionManager.beginDelayedTransition() 实现上面的处理将会是比较麻烦的事情,你需要通过代码对控件各个参数属性进行修改。

        这个示例将控件的开始场景和结束场景分别用 film_start_scene.xml 和 film_end_scene.xml 两个布局文件管理,再用 TransitionManager.go() 将场景装载。但缺点也比较明显:

  • 每次场景切换都会移除控件替换并需要重新绑定,调用 bindData();
  • 场景之间有一些控件参数并不需要但还是要加上。比如结束场景最终展示只需要 FloatinActionButton 、封面 ImageView 和一个背景 View,但还是得加上开始场景那些控件,否则会抛出异常。

        有没有一种办法可以实现:既可以在xml添加修改属性管理,又可以不重复绑定添加控件?使用 ConstraintLayout 作为根布局可以解决这个问题。

ConstraintLayout场景过渡

       相关代码如下:

       activity_start_scene.xml:

1
2
3
4
5
6
7
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">

<include layout="@layout/film_start_scene" />
</androidx.constraintlayout.widget.ConstraintLayout>

       activity_end_scene.xml:

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
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<ImageView
android:id="@+id/image_film_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:src="@drawable/film_cover"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_favourite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_bookmark"
android:tint="#FFD600"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//关键代码如下:
@Override
public void onClick(View v) {
ConstraintLayout root = findViewById(R.id.root);
TransitionManager.beginDelayedTransition(root);
ConstraintSet constraintSet = new ConstraintSet();
if (toggle) {
constraintSet.clone(this, R.layout.activity_end_scene);
} else {
constraintSet.clone(this, R.layout.activity_start_scene);
}
constraintSet.applyTo(root);
toggle = !toggle;
}

       实现的效果和上面使用 TransitionManager.go() 相同。

       在 ConstraintLayout 通过 ConstraintSet.clone() 后调用 ConstraintSet.applyTo() 就可以实现场景的切换。虽然解决了上面提出的问题,但这种方式实现场景过渡还是不够完美:

  • 不能停留在任意位置,applyTo() 设置了结束场景后就只能执行完成;
  • 不支持触摸反馈,根据手指的拖动伸缩;
  • 定义的两个xml文件有很多重复的控件属性,需要两个xml布局文件放在layout目录管理。

       根据上面探讨的问题,能解决这些问题的动画框架 MotionLayout 就诞生了。

MotionLayout场景过渡

       下面使用 MotionLayout 实现上面的效果:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<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/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/film_motion">

<View
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/image_film_cover"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:src="@drawable/film_cover"
app:layout_constraintBottom_toBottomOf="@+id/background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/background" />

<TextView
android:id="@+id/text_film_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:paddingVertical="8dp"
android:textColor="@android:color/white"
android:textSize="14sp"
app:layout_constraintStart_toEndOf="@id/image_film_cover"
app:layout_constraintTop_toTopOf="@id/image_film_cover" />

<RatingBar
android:id="@+id/rating_film_rating"
style="?attr/ratingBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:numStars="5"
android:paddingVertical="8dp"
android:progressTint="#FFD600"
app:layout_constraintStart_toStartOf="@+id/text_film_title"
app:layout_constraintTop_toBottomOf="@id/text_film_title" />

<TextView
android:id="@+id/film_description_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@string/file_description_title"
android:textColor="@color/colorPrimary"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/background" />

<TextView
android:id="@+id/text_film_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/film_description_title" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_favourite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_bookmark"
android:tint="#FFD600"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="@id/background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/background" />

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

       在 res/xml 目录定义 MotionLayout 使用的动画执行场景:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<Transition
app:constraintSetEnd="@id/end_scene"
app:constraintSetStart="@id/start_scene"
app:duration="200">

<!-- &lt;!&ndash; 设置点击封面执行过渡动画 &ndash;&gt;-->
<!-- <OnClick-->
<!-- app:clickAction="toggle"-->
<!-- app:targetId="@id/image_film_cover" />-->
<!-- 设置向右拖动封面执行过渡动画 -->
<OnSwipe
app:dragDirection="dragEnd"
app:onTouchUp="stop"
app:touchAnchorId="@id/image_film_cover" />
</Transition>

<!-- 因为开始场景已经默认设置在setContentView(),这里不设置也关系不大 -->
<ConstraintSet android:id="@+id/start_scene">

</ConstraintSet>

<ConstraintSet android:id="@+id/end_scene">
<!--
结束场景的控件,在MotionLayout只会认Constraint
布局相关属性也可以写在<Constraint>标签下的<Layout>
-->

<Constraint
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Constraint
android:id="@+id/image_film_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:src="@drawable/film_cover"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Constraint
android:id="@+id/fab_favourite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_bookmark"
android:tint="#FFD600"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

</ConstraintSet>

</MotionScene>

       效果如下:

第一个实例

       MotionLayout 布局必须要有一个MotionScene 文件,需要在 MotionLayout 标签中使用app:layoutDescription配置,如果你忘记配置,Android Sudio 会提示配置,按照提示会自动创建 MotionScene 文件并配置。

       选择创建后,会在res/xml文件夹下生成.xml(我的名字是activity_motionlayout_scene)文件。类似如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<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/widget" />
</ConstraintSet>

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

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

       根标签MotionScene有一个defaultDuration属性,表示所有未指定时间的动画的默认时间,默认为300毫秒。

       此时在布局文件中的MotionLayout标签会多一个layoutDescription=”@xml/aactivity_motionlayout_scene”属性。

       在布局文件中增加一个id 为view_start_status的正方形View。并在根标签MotionLayout添加showPaths=”true”属性,用来显示正方形运动的路径。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<androidx.constraintlayout.motion.widget.MotionLayout 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"
app:layoutDescription="@xml/activity_motionlayout_scene"
app:showPaths="true">

<View
android:id="@+id/view_start_status"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorPrimary" />

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

       将activity_motionlayout_scene.xml文件中Constraint标签的id值修改成正方形的id,即view_start_status。Constraint标签的id属性值需要与开始动画效果的View的id保持一致,这样Constraint标签的所有属性都会作用于该View。Constraint标签的属性与ConstraintLayout的属性是一致的,为此,给正方形开始状态增加一些属性,使其位置水平居中,距离顶部50dp。详细代码如下:

1
2
3
4
5
6
7
8
9
10
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/view_start_status"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="50dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

       因为id为start的ConstraintSet标签关联到Transition标签的constraintSetStart属性,所以它作为动画(目前只有一个动画)的起始状态。而id为end的ConstraintSet标签关联到Transition标签的constraintSetEnd属性,所以它将作为动画的结束状态。结束状态我们将正方形设置水平居中,距离底部50dp。代码如下:

1
2
3
4
5
6
7
8
9
10
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/view_start_status"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginBottom="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</ConstraintSet>

       给Transition标签增加onClick子标签,表示点击触发动画。onClick标签增加clickAction属性,值为toggle,表示重复点击时,动画循环效果;增加targetId属性,值为@id/view_start_status,表示点击正方形视图触发过渡动画。

       activity_motionlayout_scene.xml完整代码如下:

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
<?xml version="1.0" encoding="utf-8"?>
<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/view_start_status"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="50dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/view_start_status"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginBottom="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</ConstraintSet>

<Transition
app:constraintSetEnd="@id/end"
app:constraintSetStart="@+id/start">
<OnClick
app:clickAction="toggle"
app:targetId="@id/view_start_status" />
</Transition>
</MotionScene>

       运行结果:

标签与属性

Transition标签

       Transition标签主要用来指定Motion场景中一个或多个动画。即关联到动画对应的各种状态和用户交互动作,描述两个状态或约束集之间的过渡。

常用属性

  • id=”reference” Transition描述的ID;
  • constraintSetStart=”reference” 使用ConstraintSet描述的用作开始约束或布局文件作为开始约束(例如:@id/start(ConstraintSet描述)或者@layout/start(布局文件)),指定动画初始状态;
  • constraintSetEnd=”reference” 使用ConstraintSet描述的用作最终约束或布局文件作为最终约束(例如:@id/end(ConstraintSet描述)或者@layout/end(布局文件)),指定动画结束状态;
  • duration=”float” 执行过渡动画所需的时间;
  • staggered=”float” 交错移动物体的快速方法;
  • autoTransition=”none|jumpToStart|jumpToEnd|animateToStart|animateToEnd” 设置动画自动执行效果,无需用户触发(点击或移动)动画。
    • animateToStart 过渡到初始状态、执行过渡动画到开始约束的效果;
    • animateToEnd 过渡到结束状态、执行过渡动画到最终约束的效果;
    • jumpToEnd 跳到结束状态、直接到最终约束效果;
    • jumpToStart 跳到初始状态、直接到开始约束效果;
    • none 不开始状态。默认情况下是none,当设为其它值时,不用和用户交互即自动开启动画。
  • motionInterpolator=”easeInOut|easeIn|easeOut|linear|bounce” 插值器,取值有linear线性、bounce弹簧、easeIn淡入、easeOut淡出、easeInOut淡入淡出;
  • transitionDisable=”boolean” 允许动画功能;
  • layoutDuringTransition =”honorRequest|ignoreRequest” 动画过程中,MotionLayout子View调用reqeustLayout,是否做出响应。取值honorRequest响应、ignoreRequest忽略;

用户交互的子标签

       Transition标签通过一些子标签,实现与用户交互的行为。例如上文的OnClick子标签表示用户的点击行为。

OnClick标签

       点击场景中某个视图,开始动画效果,可选参数,增加了对触发处理的支持。

  • targetId=”reference” 设置用来触发过渡动画的 View 的 Id;
  • clickAction=”toggle|transitionToEnd|transitionToStart|jumpToEnd|jumpToStart” 点击时视图执行的动作;
    • toggle 在 Start 场景和 End 场景之间循环的切换;
    • transitionToEnd 过渡到 End 场景;
    • transitionToStart 过渡到 Start 场景;
    • jumpToEnd 不执行过渡动画跳到 End 场景;
    • jumpToStart 不执行过渡动画跳到 Start 场景;

OnSwipe标签

       表示在布局上滑动时要执行的操作。可选参数,增加了对触摸处理的支持,一个<Transition> 标签下可以包含多个<OnSwipe>

  • touchAnchorId=”reference” 视图的Id,滑动此视图外的区域,能响应滑动效果;
  • touchRegionId=”reference” 视图的Id,滑动此视图内的区域,能响应滑动效果;
  • touchAnchorSide=”top|left|right|bottom|middle|start|end” 用户滑动界面时MotionLayout将尝试在touchAnchorId指定的视图和手指之间保持一个恒定的距离,而此属性指定的是手指和View的哪一侧保持恒定的距离(left、right、top、buttom);
  • maxVelocity=”float” 目标视图的最大速度,单位为秒,当滑动一定速度,目标视图会按照惯性继续运作,进行先加速后减速运行(默认情况)。如果视图运动过程中加速到了我们设置的最大值,那么就是先加速,然后按最大速度匀速运动,最后再减速运动;
  • dragDirection=”dragUp|dragDown|dragLeft|dragRight|dragStart|dragEnd” 滑动的方向,设置之后只会在特定的方向生效;
    • dragUp(手指从下往上拖动(↑))
    • dragDown(手指从上往下拖动(↓))
    • dragLeft(手指从右往左拖动(←))
    • dragRight(手指从左往右拖动(→))
  • maxAcceleration=”float” 目标视图的最大加速度,如果想让视图运动更快,则加大其值。默认值1.2;
  • dragScale=”float” 控制视图相对于滑动长度的移动距离。默认值1.0(视图移动的距离应与滑动距离一致),当值小于1时,视图移动的距离会远远小于滑动距离(例如dragScale值为0.5 , 如果滑动了2dp,目标视图会移动1dp),当值大于1时,视图移动的距离会大于滑动距离(例如dragScale值为1.5 , 如果滑动了2dp,目标视图会移动3dp);
  • moveWhenScrollAtTop=”boolean” boolean类型,如果滑动是滚动的,并且View(例如RecyclerView或NestedScrollView)同时滚动和过渡;
  • onTouchUp=”decelerateAndComplete|decelerate|stop|autoCompleteToEnd|autoCompleteToStart|autoComplete”
    • decelerateAndComplete 减速直到最近的状态
    • decelerate 减数
    • stop 停止动画
    • autoCompleteToEnd 自动过渡到结束状态
    • autoCompleteToStart 自动过渡到开始状态
    • autoComplete 自动过渡到最近的状态
  • nestedScrollFlags=”disablePostScroll|disableScroll|none” 嵌套滚动标记;
  • limitBoundsTo 编制滑动边缘;
  • dragThreshold 触发滑动的门阀,即滑动多长距离才会触发动画;

关键帧子标签

       在上文中,默认情况下过渡动画Transition标签会关联一个开始状态和一个结束状态的TransitionSet标签。但Transition标签不仅可以创建初始状态和结束状态,还可以创建中间状态。这些中间状态则由关键帧来构成,以实现更复杂的动画效果。

       KeyFrameSet标签用来指定某个中间状态的位置和属性。其实和过渡动画的关键帧是一样的概念。KeyFrameSet标签含有KeyPosition和KeyAttribute两个子标签,这些共同构成过渡动画过程中某特殊状态的位置和属性。

位置关键帧

       KeyPosition标签用来定义整个运动动画中某个状态的位置,相比于静态的TransitionSet标签来说,更加灵活。

  • motionTarget=”reference” 修改路径的视图id,实现过渡动画的视图id;
  • framePosition=”integer” 表示动画的进度,当前关键帧的位置,把整个运动动画分成100个位置,取值0到100(可取负值)时,那么初始状态的位置就是0,结束状态就是100,30就表示动画进度执行30%的地方;
  • transitionEasing=”standard|accelerate|decelerate|linear” 使用的插值器,standard标准、accelerate加速、decelerate减速、linear线性;
  • pathMotionArc=”none|startVertical|startHorizontal|flip” 动画以弧形运行,需要在起始的 ConstraintSet 也要加入pathMotionArc属性,此时关键帧处加入pathMotionArc也能生效;
    • none 直线运行,还原为线性运动;
    • startVertical 纵向弧形,沿垂直方向开始弧形运动;
    • startHorizontal 横向弧形,沿水平方向开始弧形运动;
    • flip 当前弧形翻转,翻转当前的圆弧方向;

       正常不添加关键帧,只是控制View的约束位置,不设置其它参数,代码入下:

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
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:defaultDuration="1000">

<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/view_start_status"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="50dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Motion app:pathMotionArc="startHorizontal" />
</Constraint>
</ConstraintSet>

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

<Transition
app:constraintSetEnd="@id/end"
app:constraintSetStart="@+id/start">
<OnClick
app:clickAction="toggle"
app:targetId="@id/view_start_status" />
</Transition>
</MotionScene>

       执行效果如下直线效果:

       此时,在起始的 ConstraintSet 加入 pathMotionArc,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:defaultDuration="1000">

<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/view_start_status"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="50dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Motion app:pathMotionArc="startHorizontal" />
</Constraint>
</ConstraintSet>

...
</MotionScene>

       效果如下:

pathMotionArc="startHorizontal"
pathMotionArc="startVertical"

       此时加入一个关键帧,代码如下(keyPositionType属性后面说明):

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
42
43
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:defaultDuration="1000">

<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/view_start_status"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="50dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Motion app:pathMotionArc="startVertical" />
</Constraint>
</ConstraintSet>

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

<Transition
app:constraintSetEnd="@id/end"
app:constraintSetStart="@+id/start">
<OnClick
app:clickAction="toggle"
app:targetId="@id/view_start_status" />
<KeyFrameSet>
<KeyPosition
app:framePosition="50"
app:keyPositionType="parentRelative"
app:motionTarget="@id/view_start_status"
app:percentX="0.5"
app:percentY="0.5" />
</KeyFrameSet>

</Transition>
</MotionScene>

       效果如下:

       在关键帧处设置pathMotionArc=”startHorizontal”,代码入下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
...>

...

<Transition
app:constraintSetEnd="@id/end"
app:constraintSetStart="@+id/start">
...
<KeyPosition
app:pathMotionArc="startHorizontal"
app:framePosition="50"
app:keyPositionType="parentRelative"
app:motionTarget="@id/view_start_status"
app:percentX="0.5"
app:percentY="0.5" />
</KeyFrameSet>

</Transition>
</MotionScene>

       效果如下:

       在关键帧处设置pathMotionArc=”none”:

       在关键帧处设置pathMotionArc=”flip:

  • keyPositionType=”deltaRelative|pathRelative|parentRelative” 参考坐标系的选择,决定了percentX和percentY属性取值的结果。

       parentRelative

       相对父容器。表示坐标系基于父视图,看如下代码:

1
2
3
4
5
6
7
8
<KeyFrameSet>
<KeyPosition
app:motionTarget="@id/view_start_status"
app:framePosition="20"
app:keyPositionType="parentRelative"
app:percentX="0.3"
app:percentY="0.1" />
</KeyFrameSet>

       左图为相对与MotionLayout布局建立坐标系,可以看出实际的P点与坐标系中坐标相交点略微偏差。那是因为建立坐标系需要依赖视图本身的中心点为依据,如果你的视图足够小,小到为一个点,此时这种坐标系就是左图情况。而实际上视图不可能那样小,实际状况如右图,依赖控件本身的中心点建立坐标系,此时的P点正好为坐标系中的位置。

       deltaRelative

       三角定位。

1
2
3
4
5
6
7
8
<KeyFrameSet>
<KeyPosition
app:motionTarget="@id/view_start_status"
app:framePosition="20"
app:keyPositionType="deltaRelative"
app:percentX="0.3"
app:percentY="0.1" />
</KeyFrameSet>

        此方式是以动画起始状态视图中心点为坐标系(0,0)点,动画结束状态为(1,1)建立坐标系,p点为设置的percentX、percentY具体坐标。

       pathRelative

        相对路径。

1
2
3
4
5
6
7
8
<KeyFrameSet>
<KeyPosition
app:motionTarget="@id/view_start_status"
app:framePosition="20"
app:keyPositionType="pathRelative"
app:percentX="0.3"
app:percentY="0.1" />
</KeyFrameSet>

       此方式为以动画起始状态视图中心点(0,0),动画结束状态视图中心点(1,0)建立x轴,x顺时针90度建立y轴,y轴1.0长度与x轴相同,长度都等于路径的长度,此时y轴就有正负之分。p点为设置的percentX、percentY具体坐标。

  • percentX=”float” 相对参考系的横向的比例(0-1);
  • percentY=”float” 相对参考系的纵向的比例(0-1) ;
  • sizePercent=”float” 如果视图更改大小,则这将控制大小的增长方式。(对于固定大小的对象,请使用 scaleX / Y);

       例如设置一个约束起始宽度、高度为50dp ,终止宽度、高度为100dp,代码如下:

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
<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/view_start_status"
android:layout_width="50dip"
android:layout_height="50dip"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@id/view_start_status"
android:layout_width="100dip"
android:layout_height="100dip"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</ConstraintSet>
<Transition
app:constraintSetEnd="@id/end"
app:constraintSetStart="@+id/start">
<KeyFrameSet>
<KeyPosition
app:framePosition="50"
app:keyPositionType="deltaRelative"
app:motionTarget="@+id/view_start_status"
app:percentX="0.9"
app:percentY="0.5" />
</KeyFrameSet>
</Transition>
</MotionScene>

       如果不设置sizePercent比例效果如下:

        如果设置sizePercent为0.3,代码如下:

1
2
3
4
5
6
7
<KeyPosition
app:framePosition="50"
app:sizePercent="0.3"
app:keyPositionType="deltaRelative"
app:motionTarget="@+id/view_start_status"
app:percentX="0.9"
app:percentY="0.5" />

        效果如下:

        如果设置sizePercent为0.9,效果如下:

  • 宽度变化的百分比,如果宽度没有变化,则此属性无效。将会覆盖sizePercent。

       例如设置一个约束起始宽度为50dp ,终止宽度为100dp,代码如下:

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
<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/view_start_status"
android:layout_width="50dip"
android:layout_height="50dip"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@id/view_start_status"
android:layout_width="100dip"
android:layout_height="50dip"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</ConstraintSet>
<Transition
app:constraintSetEnd="@id/end"
app:constraintSetStart="@+id/start">
<KeyFrameSet>
<KeyPosition
app:framePosition="20"
app:keyPositionType="deltaRelative"
app:motionTarget="@+id/view_start_status"
app:percentX="0.3"
app:percentY="0.1" />
</KeyFrameSet>
</Transition>
</MotionScene>

       如果不设置percentWidth比例效果如下:

       如果设置percentWidth为0.3,代码如下:

1
2
3
4
5
6
7
<KeyPosition
app:framePosition="20"
app:percentWidth="0.3"
app:keyPositionType="deltaRelative"
app:motionTarget="@+id/view_start_status"
app:percentX="0.3"
app:percentY="0.1" />

       效果如下:

       如果设置percentWidth为0.9,效果如下:

  • percentHeight=”float” 高度变化的百分比,如果高度没有变化,则此属性无效。percentWidth和percentHeight属性都会导致sizePercent属性失效。具体效果同percentWidth。
  • curveFit=”spline|linear” 设置动画运动路径,spline(曲线,默认)、linear(直线)

       如果不设置curveFit属性或设置curveFit=”spline”(它将是曲线运行)

       代码如下:

1
2
3
4
5
6
<KeyPosition
app:framePosition="50"
app:keyPositionType="deltaRelative"
app:motionTarget="@+id/view_start_status"
app:percentX="0.9"
app:percentY="0.5" />

       效果如下:

       如果设置curveFit=“linear”(它将是直线运行)

       代码如下:

1
2
3
4
5
6
7
<KeyPosition
app:framePosition="50"
app:keyPositionType="deltaRelative"
app:curveFit="linear"
app:motionTarget="@+id/view_start_status"
app:percentX="0.9"
app:percentY="0.5" />

       效果如下:

  • drawPath=”none|path|pathRelative|deltaRelative|asConfigured|rectangles” 调试使用,绘制布局动画路径及参考线。

       例如设置drawPath=”pathRelative”代码如下:

1
2
3
4
5
6
7
8
<KeyPosition
app:framePosition="50"
app:sizePercent="0.9"
app:drawPath="pathRelative"
app:keyPositionType="deltaRelative"
app:motionTarget="@+id/view_start_status"
app:percentX="0.9"
app:percentY="0.5" />

       效果如下:

       设置drawPath=”rectangles”效果如下:

       设置drawPath=”deltaRelative”效果如下:

属性关键帧

       KeyAttribute相对于位置关键帧,属性关键帧更注重的是属性,在动画期间控制布局属性,例如实现View在移动过程中自身的缩放、透明度、旋转等属性的改变。

基本属性

       如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<KeyFrameSet>
<KeyAttribute
android:scaleX="0.1"
android:scaleY="0.1"
app:framePosition="50"
app:motionTarget="@id/view_start_status" />
<KeyAttribute
android:alpha="0.1"
app:framePosition="50"
app:motionTarget="@id/view_start_status" />
<KeyAttribute
android:rotation="-45"
app:framePosition="50"
app:motionTarget="@id/view_start_status" />
</KeyFrameSet>

       效果如下:

  • motionTarget=”reference” 修改属性的视图id;
  • framePosition=”integer” 表示动画的进度;
  • transitionEasing 动画速度;
  • curveFit 选择基于直线的路径或基于单一速率的路径;
  • motionProgress 设置动画进度;
  • scaleX=”float” 视图的宽度变化比例,X轴缩放;
  • scaleY=”float” 视图的高度变化比例,Y轴缩放;
  • rotation=”float” 视图旋转的角度(以度为单位);
  • rotationX 视图绕x轴旋转的角度(以度为单位);
  • rotationY 视图绕y轴旋转的角度(以度为单位);
  • transformPivotX 旋转或缩放的中心点X坐标;
  • transformPivotY 旋转或缩放的中心点Y坐标;
  • alpha=”float” 视图的透明度变化(0-1)
  • elevation=”dimension” 视图基于Z轴的高度;
  • translationX=”dimension” 视图在X轴上位移的距离;
  • translationY=”dimension” 视图在Y轴上位移的距离;
  • translationZ=”dimension” 视图在Z轴上位移的距离。
自定义属性

       CustomAttribute标签必须通过attributeName属性指定一个属性名,通过反射调用设置的“名称”方法。

       例如在关键帧处改变背景色,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<KeyFrameSet>
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/view_start_status">
<CustomAttribute
app:attributeName="BackgroundColor"
app:customColorValue="#00ffff" />
</KeyAttribute>
<KeyAttribute
app:framePosition="50"
app:motionTarget="@id/view_start_status">
<CustomAttribute
app:attributeName="BackgroundColor"
app:customColorValue="#00ff00" />
</KeyAttribute>
</KeyFrameSet>

       效果如下:

  • attributeName=”string” 属性的名称,区分大小写(BackgroundColor将寻找方法setBackgroundColor(…));
  • customColorValue=”color” 反射的属性为一个颜色值的属性;
  • customColorDrawableValue 颜色值的Drawable类型;
  • customIntegerValue=”integer” 反射的属性为一个integer值的属性;
  • customFloatValue=”float” 反射的属性为一个float值的属性;
  • customStringValue=”string” 反射的属性为一个string值的属性;
  • customDimension=”dimension” 反射的属性为一个dimension值的属性;
  • customPixelDimension Pixel尺寸类型;
  • customBoolean=”boolean” 反射的属性为一个boolean(true/false)值的属性;

KeyCycle

       控制动画过程中做周期性。

       例如实现View在移动动画使用正弦函数模式进行属性改变:

1
2
3
4
5
6
7
8
9
<KeyFrameSet>
<KeyCycle
android:rotation="45"
app:framePosition="50"
app:motionTarget="@id/view_start_status"
app:waveOffset="0"
app:wavePeriod="1"
app:waveShape="sin" />
</KeyFrameSet>

       效果如下:

sin正弦波
  • motionTarget=”reference” 修改属性的视图id;
  • framePosition=”integer” 表示动画的进度,当前关键帧的位置,把整个运动动画分成100个位置,取值0到100(可取负值)时,那么初始状态的位置就是0,结束状态就是100,30就表示动画进度执行30%的地方;
  • waveOffset=”float” 偏移值已添加到属性;
  • wavePeriod=”float” 在该区域附近循环的循环数;
  • waveShape=”sin|square|triangle|sawtooth|reverseSawtooth|cos|bounce” 产生设置的波的形状,如上面的正弦波;

       下面是其它的波形效果图:

square方形波
triangle三角波
sawtooth锯齿波
reverseSawtooth反向锯齿波
cos余弦波
bounce反弹波

       常用相关属性参数及<CustomAttribute> 参考 <KeyAttribute> 即可。

KeyTimeCycle

       控制动画在帧上做周期性,例如在View未移动时也显示周期性动画,当View移动时同时也执行周期性动画,代码如下:

1
2
3
4
5
6
7
8
<KeyFrameSet>
<KeyTimeCycle
android:rotation="30"
app:wavePeriod="1"
app:framePosition="50"
app:motionTarget="@id/view_start_status"
/>
</KeyFrameSet>

       效果如下:

  • rotation=”float” 视图旋转的角度(以度为单位);
  • wavePeriod=”float” 在该区域附近循环的循环数;
  • motionTarget=”reference” 修改属性的视图id;
  • framePosition=”integer” 表示动画的进度,取值范围为[0,100]。50就表示动画进度执行50%的地方;

       ​ 其它参数同<KeyCycle>

KeyTrigger

       在动画过程中的固定点触发回调到代码中,例如在动画执行过程中,监听动画执行进度,类似如下效果:

       实现步骤:

       1、定义KeyTrigger:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<KeyFrameSet>
<KeyTrigger
app:framePosition="20"
app:motionTarget="@id/tv_text"
app:onCross="p0" />
<KeyTrigger
app:framePosition="50"
app:motionTarget="@id/tv_text"
app:onCross="p1" />
<KeyTrigger
app:framePosition="80"
app:motionTarget="@id/tv_text"
app:onCross="p2" />
</KeyFrameSet>
  • motionTarget=”reference” 目标视图id(这里是自定义视图,因为方法写在了自定义视图里);
  • framePosition=”integer” 表示动画的进度,取值范围为[0,100]。50就表示动画进度执行50%的地方
  • onCross=”string” 方法名称,与自定义视图中方法名一一对应。不管动画是正向还是反向,只要到达设置的framePosition 就会执行函数;
  • onPositiveCross=”string” 方法名称,与自定义视图中方法名一一对应。只有正向执行动画是到达设置的framePosition 才会执行函数;
  • onNegativeCross=”string” 方法名称,与自定义视图中方法名一一对应。只有反向执行动画是到达设置的framePosition 才会执行函数;

       三者效果如下所示:

  • triggerSlack=”float” 如果动画位置未离开framePosition触发点,则不会重复调用触发器(值越大重复率越低);
  • triggerId=”reference” 使用此ID回调TransitionListener监听中的onTransitionTrigger中方法;

       2、自定义视图:

       代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyText extends AppCompatTextView {

public MyText(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

public void p0() {
setText("20%");
}

public void p1() {
setText("50%");
}

public void p2() {
setText("80%");
}
}

       3、使用自定义视图:

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
<androidx.constraintlayout.motion.widget.MotionLayout 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"
app:layoutDescription="@xml/activity_motion_scene6"
app:showPaths="true">

<View
android:id="@+id/view_start_status"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@android:color/black" />

<com.wy521angel.constrainlayout2.MyText
android:id="@+id/tv_text"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_marginBottom="200dp"
android:background="@android:color/black"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />

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

ConstraintSet标签

       描述约束集,约束集主要用来定义多个属性集合,并通过id被Transition标签引用,作为运动动画过程的起始或结束状态。

Constraint标签

       在 MotionLayout 配置文件中,可以将 Constraint 理解为在对应 ConstraintSet 场景下的View控件,只是在配置文件中是不会认ImageView或TextView等等,都用 Constraint 代替,类似如下代码:

1
2
3
4
5
6
7
8
9
10
<MotionScene>
<ConstraintSet>
<Constraint
android:id="@+id/view"
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
</MotionScene>

Layout标签

       Layout 是 Constraint 的子节点,主要是声明控件的布局属性。

1
2
3
4
5
6
7
8
9
10
11
<MotionScene>
<ConstraintSet>
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</Constraint>
</ConstraintSet>
</MotionScene>

       相比直接将布局属性写在 Constraint 节点,使用 Layout 会更加合理。

Motion标签

       Motion 是 Constraint 的子节点,可以指定控件的运动轨迹,默认是直线运动轨迹,通过它可以很方便的将运动轨迹修改为曲线。

1
2
3
4
5
6
7
8
9
<MotionScene>
<ConstraintSet>
<Constraint>
<Motion
app:pathMotionArc="startVertical"
app:transitionEasing="decelerate" />
</Constraint>
</ConstraintSet>
</MotionScene>

       相关属性详见位置关键帧一节。

CustomAttribute标签

       CustomAttribute 是 Constraint 的子节点,在该节点声明控件背景、文本颜色等其他自定义属性。

1
2
3
4
5
6
7
8
9
<MotionScene>
<ConstraintSet>
<Constraint>
<CustomAttribute
app:attributeName="backgroundColor"
app:customColorValue="@color/colorAccent" />
</Constraint>
</ConstraintSet>
</MotionScene>

       相关属性详见自定义属性一节。

Transform标签

       Transform 是 Constraint 的子节点,可以指定从开始场景到结束场景之间添加动画效果,比如缩放、旋转等。

1
2
3
4
5
6
7
8
9
10
<MotionScene>
<ConstraintSet>
<Constraint>
<Transform
android:rotation="360"
android:scaleX="2"
android:scaleY="2" />
</Constraint>
</ConstraintSet>
</MotionScene>

       相关属性详见属性关键帧中的基本属性。

       代码如下:

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
42
43
44
45
46
47
48
49
50
51
<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/view_start_status">
<Layout
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="50dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Motion
app:pathMotionArc="startVertical"
app:transitionEasing="decelerate" />
<CustomAttribute
app:attributeName="backgroundColor"
app:customColorValue="@color/colorAccent" />
</Constraint>

</ConstraintSet>

<ConstraintSet android:id="@+id/end">
<Constraint android:id="@+id/view_start_status">
<Layout
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginBottom="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

<Transform
android:rotation="360"
android:scaleX="1.33"
android:scaleY="1.33"
android:translationZ="12dp" />

<CustomAttribute
app:attributeName="backgroundColor"
app:customColorValue="@color/colorPrimary" />
</Constraint>

</ConstraintSet>

<Transition
app:constraintSetEnd="@id/end"
app:constraintSetStart="@+id/start">
<OnClick
app:clickAction="toggle"
app:targetId="@id/view_start_status" />
</Transition>
</MotionScene>

       效果如下:

MotionLayout常用API

  • setDebugMode(int debugMode) 设置运动进行时是否显示运动路径,用来调试动画,与MotionLayout xml中app:motionDebug对应,在xml 也可以使用app:showPaths=”true”来控制是否显示运动路径;

       代码中设置可选参数如下:

1
2
3
public static final int DEBUG_SHOW_NONE = 0;//不显示路径及进度
public static final int DEBUG_SHOW_PROGRESS = 1;//只显示进度
public static final int DEBUG_SHOW_PATH = 2;//只显示路径

       xml设置可选参数如下:

1
2
3
4
<enum name="NO_DEBUG" value="0"/>//不显示路径及进度
<enum name="SHOW_PROGRESS" value="1"/>//只显示进度
<enum name="SHOW_PATH" value="2"/>//只显示路径
<enum name="SHOW_ALL" value="3"/>//既显示路径也显示进度
  • loadLayoutDescription(int motionScene) 通过代码加载MotionScene,对应的xml中属性为app:layoutDescription;
  • transitionToStart() 切换到动画start状态,默认有过渡效果,如果不需要过渡效果,可以通过setProgress(0);
  • transitionToEnd() 切换到动画end状态,默认有过渡效果,如果不需要过渡效果,可以通过setProgress(1);
  • setProgress(float pos) 设置动画运动进度(0-1);
  • transitionToState(int id) 切换到动画某个状态,可以是start或end状态,参数id指的是ConstraintSet标签定义的id;
  • setTransitionListener(MotionLayout.TransitionListener listener) 监听MotionLayout动画执行过程,如下代码所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface TransitionListener {
//开始动画时回调
void onTransitionStarted(MotionLayout motionLayout,//当前MotionLayout视图
int startId,//开始状态的ID 如果未知,则为-1
int endId //结束状态的ID 如果未知,则为-1
);
//动画改变状态时回调
void onTransitionChange(MotionLayout motionLayout,//当前MotionLayout视图
int startId,//开始状态的ID 如果未知,则为-1
int endId,//结束状态的ID 如果未知,则为-1
float progress //当前动画进度(0-1
);
//完成动画时回调
void onTransitionCompleted(MotionLayout motionLayout,//当前MotionLayout视图
int currentId //到达状态的ID
);
//使用<KeyTrigger>中定义了 triggerId,会回调到这里
void onTransitionTrigger(MotionLayout motionLayout,//当前MotionLayout视图
int triggerId,//使用triggerId设置的ID
boolean positive,//正向运动(start-->end)经过此处返回true,反向运动(end-->start)经过此处 返回false
float progress //当前动画进度(0-1
);
}

使用AndroidStudio中Motion Editor

预览面板

       Motion Editor(Android Studio 4.0 +) 是一款专门针对 MotionLayout 布局类型所构建的可视化编辑器,通过它可以轻松地创建和预览动画效果。当你在一个包含 MotionLayout 的 XML 文件中选择 Design 或 Split 视图时,AndroidStudio 会自动打开 Motion Editor。你可以使用已在布局编辑器中所熟知的交互方式来编辑布局和 Motion Scene 文件,并可以直接在 Android Studio 预览界面中对动画效果进行预览。

       预览面板的加入使得在处理动画效果时,能够实现快速编辑并立即获取反馈,当你对动画进行细微调整之后,不用再去重新编译和部署,也能直接预览最终的动画效果。

概览面板

       MotionLayout 可以对布局的变化做动画处理,在编辑器中该动画可被指定为 ConstraintSets 中的 Transition 效果,Motion Editor 可以通过概览面板将这些状态的转变可视化(预览面板),要编辑 ConstraintSet 中的约束,点击概览面板中相应的选项即可。

       图中start、end是两个<ConstraintSet> ,它们之间有一个<Transition> 效果,可以通过选择start、end来修改视图的状态。

选择面板

       选择面板会根据概览面板中的状态显示相应的控件信息,它有三种显示模式。

       选中概览面板中 MotionLayout 时的模式:Motion Editor 支持编辑基本的 MotionLayout,当在概览面板中选中 MotionLayout模式之后,你可以选择相应的组件来查看它的约束是否配置正确。

       选中概览面板中 ConstraintSet 时的模式:当在概览面板中选中 ConstraintSet 时,选择面板会以列表的形式列出所有组件,组件旁边的选中图标意味着该组件被当前的 ConstraintSet 所约束(下图选中的是start,也可以选end)。

        选中概览面板中 Transition 时的模式:当在概览面板中选择 Transition 时,你可以通过动画工具栏来控制动画的播放。当选中某个动画后,点击时间轴上的 ▶按钮,可以预览动画效果。

        当在概览面板中选择 Transition 时,你可以通过工具栏中添加关键帧来添加关键帧约束:

属性面板

       这里的属性面板同 Layout Editor 的属性面板类似,可以在这里对Constraint 的可视化效果进行预览,对Motion Scene 文件中视图的所有属性效果进行修改和添加。

       当选择概览面板的 ConstraintSet 时,选择面板会以列表的形式列出所有组件,选择具体组件,这时属性面板会展示组件基础可选修改选项供修改或添加。

       当选择概览面板的 Transition 时,此时属性面板展示<Transition>基础可选属性选项,供修改或添加,下方的选择面板会以列表的形式列出所有动画,选择具体动画,这时属性面板会展示动画基础可选属性选项,供修改或添加。

       代码详见ConstraintLayoutTest

参考资料:
谷歌开发者 Constraint Layout 2.0 用法详解
新小梦 Constraintlayout 2.0:你们要的更新来了
zping0808 ConstraintLayout2.x使用详解
VincentWei95 MotionLayout

Fork me on GitHub