在了解 MotionLayout
之前,建议先了解 ConstriantLayout
的一些基本使用,因为 MotionLayout
是 ConstraintLayout
的子类,参考文章:ConstraintLayout的基本使用。
MotionLayout
作为 ConstraintLayout
的子类,既然是子类自然就是对父类功能的一些增强和优化,ConstraintLyout
已经将布局等相关的工作已经做完了,那么 MotionLayout
是干嘛的?顾名思义,Motion
是运动的意思,ConstriantLayout
是静态布局,而 MotionLayout
自然就是可以让布局“运动”起来的布局。
MotionLayout
可以控制布局控件之间实现动画联动,它是一个动画框架,所使用的动画为过渡动画。过渡动画又是什么?接下来慢慢细说。
目前的动画主要分为三种:View动画、属性动画、过渡动画。
View动画 Animation
是在API 1很早就已经出现,但现在也基本比较少使用它们,因为使用属性动画也可以完全实现同样的效果。比如 AlphaAnimation
、ScaleAnimation
等。
属性动画是在API 11时出现,是目前最常见和使用的动画,可以很方便的实现控件的透明度、平移、缩放、旋转等动画效果。
过渡动画是在API 18时引入,它主要是用于在两种场景或两种状态之间的切换。
先用属性动画实现一个简单的例子:
<?xml version="1.0" encoding="utf-8"?>
<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>
public class ObjectAnimatorActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_object_animator);
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的属性实现这个效果会是怎样的?
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) image.getLayoutParams();
lp.gravity = Gravity.END;
image.setLayoutParams(lp);
我们修改了ImageView的 gravity
属性,但是这样是没有动画效果会直接闪现过去,我们再加上一句代码:
// 添加这句代码,在修改控件参数之前
TransitionManager.beginDelayedTransition((ViewGroup) image.getParent());
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) image.getLayoutParams();
lp.gravity = Gravity.END;
image.setLayoutParams(lp);
运行程序可以发现实现效果和属性动画一样有了平移动画的效果。通过加上一句 TransitionManager.beginDelayedTransiton()
就实现了过渡动画的平移效果,从一个状态过渡到另一个状态。
如果单从这个例子说明过渡动画更方便,说服力还是不够,属性动画能实现而且也可以做更多复杂的动画,还是比过渡动画好用。那过渡动画还有哪些是属性动画不具备的?过渡动画可以改变控件的属性,属性动画不能。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/image1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@mipmap/ic_launcher"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/image2"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="5dp"
android:background="@mipmap/ic_launcher"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/image3"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@mipmap/ic_launcher"
tools:ignore="ContentDescription" />
</LinearLayout>
// 属性动画
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);
过渡动画的实现本质可以简单分为两个步骤:
定义两个场景之间的过渡:肯定有开始场景和结束场景,会记录两个场景控件的各种参数
创建动画并执行动画:有了上一步记录的控件参数,就可以创建执行动画的参数
还是先上一个过渡动画的示例:
具体示例代码如下:
activity_film.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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" />
</android.support.constraint.ConstraintLayout>
film_start_scene.xml
<?xml version="1.0" encoding="utf-8"?>
<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" />
<android.support.design.widget.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
<?xml version="1.0" encoding="utf-8"?>
<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" />
<android.support.design.widget.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>
ic_bookmark.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z" />
</vector>
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;
}
}
如果你的程序运行的是support而不是androidx,在使用 ic_bookmark
资源可能会编译失败,需要在 build.gradle
添加支持:
android {
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
}
如果使用上面讲解的 TransitionManager.beginDelayedTransition()
实现上面的处理将会是比较麻烦的事情,你需要在代码对控件各个参数属性进行修改。
这个示例将控件的开始场景和结束场景分别用 film_start_scene.xml
和 film_end_scene.xml
两个布局文件管理,再用 TransitionManager.go()
将场景装载。但缺点也比较明显:
每次场景切换都会移除控件替换并需要重新绑定调用 bindData()
场景之间有一些控件参数并不需要但还是要加上。比如结束场景最终展示只需要 FloatinActionButton
、封面 ImageView
和一个背景 View
,但还是得加上开始场景那些控件,否则会抛出异常
有没有一种办法可以实现:既可以在xml添加修改属性管理,又可以不重复绑定添加控件?使用 ConstraintLayout
作为根布局可以解决这个问题。
activity_start_scene.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="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" />
<android.support.design.widget.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_24dp"
android:tint="#FFD600"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="@id/background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/background" />
</android.support.constraint.ConstraintLayout>
activity_end_scene.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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" />
<android.support.design.widget.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_24dp"
android:tint="#FFD600"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</android.support.constraint.ConstraintLayout>
public class FilmActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_start_scene);
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) {
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
实现上面的效果。
activity_motion_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.motion.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" />
<android.support.design.widget.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_24dp"
android:tint="#FFD600"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="@id/background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/background" />
</android.support.constraint.motion.MotionLayout>
在 res/xml
目录定义 MotionLayout
使用的动画执行场景:
<?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">
<Transition
app:constraintSetEnd="@id/end_scene"
app:constraintSetStart="@id/start_scene"
app:duration="200">
<!-- 设置点击封面执行过渡动画 -->
<OnClick
app:clickAction="toggle"
app:targetId="@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_24dp"
android:tint="#FFD600"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</ConstraintSet>
</MotionScene>
除了可以通过点击外,还可以实现触摸反馈处理场景切换:
<!-- 设置向右拖动封面执行过渡动画 -->
<OnSwipe
app:dragDirection="dragEnd"
app:onTouchUp="autoComplete"
app:touchAnchorId="@id/image_film_cover" />
跟随手指拖动停止:
<OnSwipe
app:dragDirection="dragEnd"
app:onTouchUp="stop"
app:touchAnchorId="@id/image_film_cover" />
MotionLayout
作为 ConstraintLayout
的子类,需要修改 ConstraintLayout
的依赖版本为2.0或以上才能使用:
implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta7'
关于 MotionLayout
这篇文章会讲解比较常用的一些属性参数,具体其他参数的使用和示例可以参考官网:MotionLayout、MotionLayout示例
MotionScene
定义 MotionLayout
布局动画执行场景等具体参数,作为xml文件的根元素,可以定义三个子元素:
、
、
。
<MotionScene>
<Transition/>
<ConstraintSet/>
<StateSet/>
</MotionScene>
Transition
定义一组过渡动画,支持修改动画执行期间布局控件的运动轨迹等。
<MotionScene>
<Transition
app:constraintSetStart="@+id/start"
app:constrantSetEnd="@+id/end"
app:duration="1000">
<OnClick/>
<OnSwipe/>
<KeyFrameSet>
<KeyAttribure/>
<KeyCycle/>
<KeyPosition/>
<KeyTimeCycle/>
</KeyFrameSet>
</Transition>
</MotionScene>
属性 | 描述 |
---|---|
constraintSetStart | 指定开始场景 ConstraintSet 的id或布局xml,即开始时的控件布局状态 |
constraintSetEnd | 指定结束场景 ConstraintSet 的id或布局xml,即结束时的控件布局状态 |
duration | 动画执行时间 |
motionInterpolator | 设置动画执行插值器 |
<?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">
<!-- bounce实现回弹效果 -->
<Transition
app:constraintSetEnd="@id/end_scene"
app:constraintSetStart="@id/start_scene"
app:duration="1000"
app:motionInterpolator="bounce">
<OnClick
app:clickAction="toggle"
app:targetId="@id/view" />
</Transition>
<ConstraintSet android:id="@+id/start_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Motion app:pathMotionArc="startHorizontal" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end_scene">
<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>
OnClick
设置在点击时触发执行过渡动画。
<MotionScene>
<Transition>
<OnClick
app:clickAction="toggle"
app:targetId="@id/btn_toggle" />
</Transition>
</MotionScene>
属性 | 描述 |
---|---|
clickAction | 设置点击按钮时动画的执行方式,toggle 、transitionToEnd 、transitionToStart 、 jumpToEnd 、jumpToStart |
targetId | 设置点击执行动画的控件id |
注意:如果一个控件已经定义了
就不能去响应
。
OnSwipe
设置在触摸拖拽滑动时执行过渡动画。
<MotionScene>
<Transition>
<OnSwipe
app:dragDirection="dragStart"
app:touchAnchorId="@+id/anchor_id"
app:onTouchUp="autoComplete" />
</Transition>
</MotionScene>
属性 | 描述 |
---|---|
dragDirection | 设置执行过渡动画的触摸拖拽方向,dragUp 、dragDown 、dragLeft/dragStart 、dragRight/dragEnd |
touchAnchorId | 设置启动触摸拖拽动画的控件id |
onTouchUp | 设置触摸松手后执行的场景状态,autoComplete 、autoCompleteToStart 、autoCompleteToEnd 、stop 、decelarate 、decelarateToComplete ;当值为 autoComplete 时,如果触摸拖动松手时的状态靠近开始场景就会往开始场景的状态执行,否则就执行结束场景的状态;当值为 stop 表示跟随手指拖动的位置停在某个位置 |
注意:如果一个控件已经定义了
就不能去响应
。
KeyFrameSet
定义控件在动画执行期间具体可以怎样移动,通过属性
、
、
、
设置。
<MotionScene>
<Transition>
<KeyFrameSet>
<KeyAttribute/>
<KeyCycle/>
<KeyPosition/>
<KeyTimeCycle/>
</KeyFrameSet>
</Transition>
</MotionScene>
KeyAttribute
控制动画执行期间在关键帧再处理特定的动画属性,也可以称为属性关键帧。
<MotionScene>
<Transition>
<KeyAttribute
app:framePosition="50"
app:motionTarget="@id/view"
app:rotation="180">
<CustomAttribute
app:attributeName="backgroundColor"
app:customColorValue="@android:color/black" />
</KeyAttribute>
</Transition>
</MotionScene>
属性 | 描述 |
---|---|
framePosition | 动画关键帧百分比,取值范围0-100。比如值为50,表示动画执行到50%的位置 |
motionTarget | 被处理的控件id |
rotation | 设置view的旋转角度 |
rotationX | 设置view横坐标的旋转角度 |
rotationY | 设置view纵坐标的旋转角度 |
scaleX | 设置view水平方向的缩放 |
scaleY | 设置view垂直方向的缩放 |
translationX | 设置viewX轴平移 |
translationY | 设置viewY轴平移 |
translationZ | 设置viewZ轴平移 |
KeyAttribute
也支持在对应关键帧动态设置 CustomAttribute
修改控件属性,例如控件背景、文本颜色等等,这在后续节点说明该属性。
<?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">
<Transition
app:constraintSetEnd="@id/end_scene"
app:constraintSetStart="@id/start_scene"
app:duration="1000">
<OnClick
app:clickAction="toggle"
app:targetId="@id/view" />
<KeyFrameSet>
<!-- 实现动画执行到50%的时候旋转180度并且放大,然后恢复原样 -->
<KeyAttribute
android:rotation="180"
android:scaleX="2"
android:scaleY="2"
app:framePosition="50"
app:motionTarget="@id/view" />
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Motion app:pathMotionArc="startHorizontal" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end_scene">
<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>
KeyPosition
处理动画执行期间在某个关键帧移动位置,可以称为位置关键帧。
<MotionScene>
<Transition>
<KeyPosition
app:framePosition="50"
app:keyPositionType="deltaRelative"
app:motionTarget="@id/view"
app:percentX="1"
app:percentY="0" />
</Transition>
</MotionScene>
属性 | 描述 |
---|---|
motionTarget | 被处理的控件id |
framePosition | 动画关键帧百分比,取值范围0-100 |
keyPositionType | 指定关键帧使用的坐标系,有三种坐标系:parentRelative 、deltaRelative 和 pathRelative |
percentX | 指定关键帧运动轨迹的水平相对位置,取值范围0-1;keyPositionType 选择不同的坐标系运动轨迹位置也有所不同 |
percentY | 指定关键帧运动轨迹的垂直相对位置,取值范围0-1;keyPositionType 选择不同的坐标系运动轨迹位置也有所不同 |
pathMotionArc | 修改所在关键帧的运动轨迹,startHorizontal 表示起始方向是水平方向;startVertical 表示起始方向是垂直方向,最终会让运动轨迹形成弧形;none 表示不处理运动轨迹为直线;flip 表示设置往后的运动轨迹与之前的相反 |
<?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">
<Transition
app:constraintSetEnd="@id/end_scene"
app:constraintSetStart="@id/start_scene"
app:duration="1000">
<OnClick
app:clickAction="toggle"
app:targetId="@id/view" />
<KeyFrameSet>
<!--
动画执行到50%时移动到相对于parent的中间位置、指定运动轨迹和之前的相反
需要注意的是,pathMotionArc要生效,控件的起始场景Constraint也必须设置pathMotionArc,否则会失效默认为直线
-->
<KeyPosition
app:framePosition="50"
app:keyPositionType="parentRelative"
app:motionTarget="@id/view"
app:pathMotionArc="flip"
app:percentX="0.5"
app:percentY="0.5" />
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 起始场景设置pathMotionArc,否则位置关键帧的pathMotionArc不生效 -->
<Motion app:pathMotionArc="startHorizontal" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end_scene">
<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>
keyPositionType
指定不同的坐标系统,每一个关键帧 KeyPosition
都是独立的可以按需指定自己使用的坐标系统
该坐标系是让控件的位置关键帧相对于父容器,比如上面的例子就是相对于父容器 MotionLayout
:
该坐标系是让控件的位置关键帧相对于开始位置和结束位置,开始位置的中心坐标是控件当前关键帧的中心点,结束位置的中心坐标是控件当前关键帧的中心点。
如果你想要控件在水平方向或垂直方向上移动,这个坐标系会非常有用,因为坐标系是相对于自身位置的,所以可以根据自身位置很方便拿到位置信息。
为了能够清晰看清坐标系的差别,将上面例子的结束位置调整到中间位置:
简单可以理解为该坐标系是通过开始位置和结束位置的长度定义到y轴的长度。
使用三种坐标系演示一个示例:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.motion.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/motion_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/motion_scene_test">
<TextView
android:id="@+id/a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="A"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="B"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/c"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="C"
android:textSize="24sp"
android:textStyle="bold" />
<Button
android:id="@+id/btn_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="toggle"
android:textAllCaps="false" />
<Button
android:id="@+id/btn_show_debug"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="showPath"
android:textAllCaps="false" />
</android.support.constraint.motion.MotionLayout>
final MotionLayout motionLayout = findViewById(R.id.motion_layout);
findViewById(R.id.btn_show_debug).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
motionLayout.setDebugMode(MotionLayout.DEBUG_SHOW_PATH);
}
});
<?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">
<Transition
app:constraintSetEnd="@+id/end"
app:constraintSetStart="@+id/start"
app:duration="1000">
<OnClick
app:clickAction="toggle"
app:targetId="@id/btn_toggle" />
<KeyFrameSet>
<KeyPosition
app:framePosition="50"
app:keyPositionType="deltaRelative"
app:motionTarget="@id/a"
app:percentX="1"
app:percentY="0" />
<KeyPosition
app:framePosition="50"
app:keyPositionType="pathRelative"
app:motionTarget="@id/b"
app:percentX="0.5"
app:percentY="-0.5" />
<KeyPosition
app:framePosition="50"
app:keyPositionType="parentRelative"
app:motionTarget="@id/c"
app:percentX="0.5"
app:percentY="0.25" />
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="A"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="textColor"
app:customColorValue="@android:color/darker_gray" />
</Constraint>
<Constraint
android:id="@+id/b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="B"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/c"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/a">
<CustomAttribute
app:attributeName="textColor"
app:customColorValue="@android:color/darker_gray" />
</Constraint>
<Constraint
android:id="@+id/c"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="C"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/b">
<CustomAttribute
app:attributeName="textColor"
app:customColorValue="@android:color/darker_gray" />
</Constraint>
<Constraint
android:id="@+id/btn_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/btn_show_debug"
app:layout_constraintRight_toRightOf="parent" />
<Constraint
android:id="@+id/btn_show_debug"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/btn_toggle"
app:layout_constraintRight_toRightOf="parent" />
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="A"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toLeftOf="@+id/b"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="textColor"
app:customColorValue="@color/colorPrimary" />
</Constraint>
<Constraint
android:id="@+id/b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="B"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="textColor"
app:customColorValue="@color/colorAccent" />
</Constraint>
<Constraint
android:id="@+id/c"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="C"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/b"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="textColor"
app:customColorValue="@android:color/black" />
</Constraint>
<Constraint
android:id="@+id/btn_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/btn_show_debug" />
<Constraint
android:id="@+id/btn_show_debug"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/btn_toggle"
app:layout_constraintRight_toRightOf="parent" />
</ConstraintSet>
</MotionScene>
KeyCycle
属性可以指定在关键帧范围内执行重复动画。
属性 | 描述 |
---|---|
motionTarget | 被处理的控件id |
framePosition | 动画关键帧百分比,取值范围0-100 |
wavePeriod | 循环次数,在指定的 framePosition 执行重复动画次数 |
waveShape | 振幅循环模型,比如 sin 、cos |
rotation | 设置view的旋转角度 |
rotationX | 设置view横坐标的旋转角度 |
rotationY | 设置view纵坐标的旋转角度 |
scaleX | 设置view水平方向的缩放 |
scaleY | 设置view垂直方向的缩放 |
translationX | 设置viewX轴平移 |
translationY | 设置viewY轴平移 |
translationZ | 设置viewZ轴平移 |
比如现在有一个需求:拖动一个View在执行到一半时左右各旋转45°。可以用 KeyAttribute
属性关键帧实现:
<?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">
<Transition
app:constraintSetEnd="@id/end_scene"
app:constraintSetStart="@id/start_scene"
app:duration="1000">
<OnSwipe
app:dragDirection="dragRight"
app:touchAnchorId="@id/view" />
<KeyFrameSet>
<KeyAttribute
android:rotation="-45"
app:framePosition="45"
app:motionTarget="@id/view" />
<KeyAttribute
android:rotation="0"
app:framePosition="50"
app:motionTarget="@id/view"/>
<KeyAttribute
android:rotation="45"
app:framePosition="55"
app:motionTarget="@id/view"/>
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</Constraint>
</ConstraintSet>
</MotionScene>
上面需求比较简单,如果后续要求在执行到一半时循环旋转45°各10次,那就要写很多的 KeyAttribute
。针对这种需求,完全可以通过 KeyCycle
实现:
<?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">
<Transition
app:constraintSetEnd="@id/end_scene"
app:constraintSetStart="@id/start_scene"
app:duration="1000">
<OnSwipe
app:dragDirection="dragRight"
app:touchAnchorId="@id/view" />
<KeyFrameSet>
<!--
KeyCycle定义的是一个区间
第一个KeyCycle和最后一个KeyCycle是定义区间范围的
中间的KeyCycle可以随意定义振幅循环次数
-->
<KeyCycle
android:rotation="0"
app:framePosition="0"
app:motionTarget="@id/view"
app:wavePeriod="0"
app:waveShape="sin" />
<KeyCycle
android:rotation="45"
app:framePosition="50"
app:motionTarget="@id/view"
app:wavePeriod="1"
app:waveShape="sin" />
<KeyCycle
android:rotation="0"
app:framePosition="100"
app:motionTarget="@id/view"
app:wavePeriod="0"
app:waveShape="sin" />
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</Constraint>
</ConstraintSet>
</MotionScene>
使用 KeyCycle
指定 wavePeriod
就可以指定在对应关键帧动画循环次数。
KeyTimeCycle
和 KeyCycle
一样也是处理重复动画,但不同在于它是以时间为度量来处理的。
<?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">
<Transition
app:constraintSetEnd="@id/end_scene"
app:constraintSetStart="@id/start_scene"
app:duration="1000">
<OnSwipe
app:dragDirection="dragRight"
app:touchAnchorId="@id/view" />
<KeyFrameSet>
<KeyTimeCycle
android:rotation="0"
app:framePosition="0"
app:motionTarget="@id/view"
app:wavePeriod="0"
app:waveShape="sin" />
<KeyTimeCycle
android:rotation="45"
app:framePosition="50"
app:motionTarget="@id/view"
app:wavePeriod="0.5"
app:waveShape="sin" />
<KeyTimeCycle
android:rotation="0"
app:framePosition="100"
app:motionTarget="@id/view"
app:wavePeriod="0"
app:waveShape="sin" />
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</Constraint>
</ConstraintSet>
</MotionScene>
ConstraintSet
指定开始场景和结束场景,对 ConstriantSet
指定id后在
通过 constraintSetStart
和 constraintSetEnd
设置一组过渡动画的场景切换。
在 MotionLayout
配置文件中,可以将 Constraint
理解为在对应 ConstraintSet
场景下的View控件,只是在配置文件中是不会认ImageView或TextView等等,都用 Constraint
代替。
<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
是 Constraint
的子节点,主要是声明控件的布局属性。
<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
是 Constraint
的子节点,可以指定控件的运动轨迹,默认是直线运动轨迹,通过它可以很方便的将运动轨迹修改为曲线。
<MotionScene>
<ConstraintSet>
<Constraint>
<Motion
app:pathMotionArc="startVertical"
app:transitionEasing="decelerate" />
</Constraint>
</ConstraintSet>
</MotionScene>
属性 | 描述 |
---|---|
pathMotionArc | 指定曲线运动轨迹方向,startHorizontal 表示起始方向是水平方向,startVertical 表示起始方向是垂直方向,最终会让运动轨迹形成弧形 |
transitionEasing | 指定动画运动的插值器,支持 accelarate 、decelarate 、linear 、standard |
CustomAttribute
是 Constraint
的子节点,在该节点声明控件背景、文本颜色等其他自定义属性。
<MotionScene>
<ConstraintSet>
<Constraint>
<CustomAttribute
app:attributeName="backgroundColor"
app:customColorValue="@color/colorAccent" />
</Constraint>
</ConstraintSet>
</MotionScene>
属性 | 描述 |
---|---|
attributeName | 设置要修改的控件属性,比如要调用控件的 setTextColor ,则执行属性名 textColor |
customColorValue | 修改颜色属性值 |
customIntegerValue | 修改int属性值 |
customFloatValue | 修改float属性值 |
customStringValue | 修改string属性值 |
customDimension | 修改dimension属性值 |
customBoolean | 修改boolean属性值 |
<?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">
<Transition
app:constraintSetEnd="@id/end_scene"
app:constraintSetStart="@id/start_scene"
app:duration="1000">
<OnClick
app:clickAction="toggle"
app:targetId="@id/view" />
</Transition>
<ConstraintSet android:id="@+id/start_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintLeft_toLeftOf="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_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<CustomAttribute
app:attributeName="backgroundColor"
app:customColorValue="@color/colorPrimary" />
</Constraint>
</ConstraintSet>
</MotionScene>
Transform
是 Constraint
的子节点,可以指定从开始场景到结束场景之间添加动画效果,比如缩放、旋转等。
<MotionScene>
<ConstraintSet>
<Constraint>
<Transform
android:rotation="360"
android:scaleX="2"
android:scaleY="2" />
</Constraint>
</ConstraintSet>
</MotionScene>
属性 | 描述 |
---|---|
rotation | 设置view的旋转角度 |
rotationX | 设置view横坐标的旋转角度 |
rotationY | 设置view纵坐标的旋转角度 |
scaleX | 设置view水平方向的缩放 |
scaleY | 设置view垂直方向的缩放 |
translationX | 设置viewX轴平移 |
translationY | 设置viewY轴平移 |
translationZ | 设置viewZ轴平移 |
<?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">
<Transition
app:constraintSetEnd="@id/end_scene"
app:constraintSetStart="@id/start_scene"
app:duration="1000">
<OnClick
app:clickAction="toggle"
app:targetId="@id/view" />
</Transition>
<ConstraintSet android:id="@+id/start_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintLeft_toLeftOf="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_scene">
<Constraint android:id="@+id/view">
<Layout
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="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>
</MotionScene>