MotionLayout 基础教程
阅读说明:
- 本文假设读者已掌握如何使用
ConstraintLayout
。 - 本文是一篇
MotionLayout
基础教程,如您已了解如何使用MotionLayout
,本文可能对您帮助不大。 - 建议读者跟随本文一起动手操作,如您现在不方便,建议稍后阅读。
- 本文基于
ConstraintLayout 2.0.0-alpha4
版本编写,建议读者优先使用这一版本。 - 由于
MotionLayout
官方文档不全,有些知识点是根据笔者自己的理解总结的,如有错误,欢迎指正。
添加支持库:
dependencies {
...
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4'
}
MotionLayout
最低支持到 Android 4.3(API 18)
,还有就是 MotionLayout
是 ConstraintLayout 2.0
添加的,因此必须确保支持库的版本不低于 2.0
。
简介
MotionLayout
类继承自 ConstraintLayout
类,允许你为各种状态之间的布局设置过渡动画。由于 MotionLayout
继承了 ConstraintLayout
,因此可以直接在 XML
布局文件中使用 MotionLayout
替换 ConstraintLayout
。
MotionLayout
是完全声明式的,你可以完全在 XML
文件中描述一个复杂的过渡动画而 无需任何代码(如果您打算使用代码创建过渡动画,那建议您优先使用属性动画,而不是 MotionLayout
)。
开始使用
由于 MotionLayout
类继承自 ConstraintLayout
类,因此可以在布局中使用 MotionLayout
替换掉 ConstraintLayout
。
MotionLayout
与 ConstraintLayout
不同的是,MotionLayout
需要链接到一个 MotionScene
文件。使用 MotionLayout
的 app:layoutDescription
属性将 MotionLayout
链接到一个 MotionScene
文件。
例:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/scene_01">
<ImageView
android:id="@+id/image"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>
注意!必须为 MotionLayout
布局的所有直接子 View
都设置一个 Id
(允许不为非直接子 View
设置 Id
)。
创建 MotionScene 文件
MotionScene
文件描述了两个场景间的过渡动画,存放在 res/xml
目录下。
要使用 MotionLayout
创建过渡动画,你需要创建两个 layout
布局文件来描述两个不同场景的属性。当从一个场景切换到另一个场景时,MotionLayout
框架会自动检测这两个场景中具有相同 id
的 View
的属性差别,然后针对这些差别属性应用过渡动画(类似于 TransitionManger
)。
MotionLayout
框架支持的标准属性:
android:visibility
android:alpha
android:elevation
android:rotation
android:rotationX
android:rotationY
android:scaleX
android:scaleY
android:translationX
android:translationY
android:translationZ
MationLayout
除了支持上面列出的标准属性外,还支持全部的 ConstraintLayout
属性。
下面来看一个完整的例子,这个例子分为以下 3
步。
第 1
步:创建场景 1
的布局文件:
文件名:
activity_main_scene1.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/motionLayout"
app:layoutDescription="@xml/activity_main_motion_scene">
<ImageView
android:id="@+id/image"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>
场景 1
的布局预览如下图所示:
第 2
步:创建场景 2
的布局文件:
文件名:
activity_main_scene2.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/activity_main_motion_scene">
<ImageView
android:id="@+id/image"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/ic_launcher"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>
场景 2
的布局预览如下图所示:
说明:场景 1
与场景 2
中都有一个 id
值为 image
的 ImageView
,它们的差别是:场景 1
中的 image
是水平垂直居中放置的,而场景 2
中的 image
是水平居中,垂直对齐到父布局顶部的。因此当从场景 1
切换到场景 2
时,MotionLayout
将针对 image
的位置差别自动应用位移过渡动画。
第 3
步:创建 MotionScene
文件:
文件名:
activity_main_motion_scene.xml
,存放在res/xml
目录下
<?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:constraintSetStart="@layout/activity_main_scene1"
app:constraintSetEnd="@layout/activity_main_scene2"
app:duration="1000">
<OnClick
app:clickAction="toggle"
app:targetId="@id/image" />
</Transition>
</MotionScene>
编写完 MotionLayout
文件后就可以直接运行程序了。点击 image
即可进行场景切换。当进行场景切换时,MotionLayout
会自动计算出两个场景之间的差别,然后应用相应的过渡动画。
下面对 MotionLayout
文件进行说明:
如上例所示,MotionScene
文件的根元素是 <MotionScene>
。在 <MotionScene>
元素中使用 <Transition>
子元素来描述一个过渡,使用 <Transition>
元素的 app:constraintSetStart
属性指定起始场景的布局文件,使用 app:constraintSetEnd
指定结束场景的布局文件。在 <Transition>
元素中使用 <OnClick>
或者 <OnSwip>
子元素来描述过渡的触发条件。
<Transition>
元素的属性:
-
app:constraintSetStart
:设置为起始场景的布局文件Id
。 -
app:constraintSetEnd
:设置为结束场景的布局文件Id
。 -
app:duration
:过渡动画的持续时间。 -
app:motionInterpolator
:过渡动画的插值器。共有以下6
个可选值:-
linear
:线性 -
easeIn
:缓入 -
easeOut
:缓出 -
easeInOut
:缓入缓出 -
bounce
:弹簧 -
anticipate
:(功能未知,没有找到文档)
-
-
app:staggered
:【浮点类型】(功能未知,没有找到文档)
可以在 <Transition>
元素中使用一个 <OnClick>
或者 <OnSwipe>
子元素来描述过渡的触发条件。
<OnClick>
元素的属性:
-
app:targetId
:【id
值】设置用来触发过渡的那个View
的Id
(例如:@id/image
或@+id/image
)。
提示:
app:targetId
的值的前缀既可以是@+id/
也可以是@id/
,两者都可以。官方示例中使用的是@+id/
。不过,使用@id/
前缀似乎更加符合语义,因为@+id/
前缀在布局中常用来创建一个新的Id
,而@id/
前缀则常用来引用其他的Id
值。为了突出这里引用的是其他的Id
而不是新建了一个Id
,使用@id/
前缀要更加符合语义。
-
app:clickAction
:设置点击时执行的动作。该属性共有以下5
个可选的值:-
toggle
:在Start
场景和End
场景之间循环的切换。 -
transitionToEnd
:过渡到End
场景。 -
transitionToStart
:过渡到Start
场景。 -
jumpToEnd
:跳到End
场景(不执行过渡动画)。 -
jumpToStart
:跳到Start
场景(不执行过渡动画)。
-
<OnSwipe>
元素的属性:
-
app:touchAnchorId
:【id
值】设置需要追踪的对象(例如:@id/image
或@+id/image
)。 -
app:touchAnchorSide
:设置需要追踪你手指运动的对象边界,共有以下4
个可选值:top
left
right
bottom
-
app:dragDirection
:设置触发过渡动画的拖动方向。共有以下4
个可选值:-
dragUp
:手指从下往上拖动(↑)。 -
dragDown
:手指从上往下拖动(↓)。 -
dragLeft
:手指从右往左拖动(←)。 -
dragRight
:手指从左往右拖动(→)。
-
-
app:maxVelocity
:【浮点值】设置动画在拖动时的最大速度(单位:像素每秒px/s
)。 -
app:maxAcceleration
:【浮点值】设置动画在拖动时的最大加速度(单位:像素每二次方秒px/s^2
)。
可以同时设置 <OnClick>
与 <OnSwipe>
,或者都不设置,而是使用代码来触发过渡。
使用代码触发过渡动画
除了使用 <OnClick>
元素与 <OnSwipe>
元素来设置触发过渡动画的触发条件外,还可以使用代码来手动触发过渡动画。
下面对场景 1
的布局文件进行修改,在布局中添加 2
个按钮,预览如下图所示:
场景 1
修改后的布局文件内容为:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/activity_main_motion_scene">
<ImageView
android:id="@+id/image"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnToStartScene"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="To Start Scene"
android:textAllCaps="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/btnToEndScene" />
<Button
android:id="@+id/btnToEndScene"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="To End Scene"
android:textAllCaps="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/btnToStartScene"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>
场景 2
的布局文件不需要修改。
在 MainActivity
中添加如下代码来手动执行过渡动画:
public class MainActivity extends AppCompatActivity {
private MotionLayout mMotionLayout;
private Button btnToStartScene;
private Button btnToEndScene;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_scene1);
mMotionLayout = findViewById(R.id.motionLayout);
btnToStartScene = findViewById(R.id.btnToStartScene);
btnToEndScene = findViewById(R.id.btnToEndScene);
btnToStartScene.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 切换到 Start 场景
mMotionLayout.transitionToStart();
}
});
btnToEndScene.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 切换到 End 场景
mMotionLayout.transitionToEnd();
}
});
}
}
如上面代码中所示,调用 MotionLayout
的 transitionToStart()
方法可以切换到 Start
场景,调用 MotionLayout
的 transitionToStart()
方法可以切换到 End
场景。
效果如下所示:
image调整过渡动画的进度
MotionLayout
还支持手动调整过渡动画的播放进度。使用 MotionLayout
的 setProgress(float pos)
方法(pos
参数的取值范围为 [0.0 ~ 1.0]
)来调整过渡动画的播放进度。
下面对场景 1
的布局文件进行修改,移除两个按钮,加入一个 SeekBar
,修改后的布局代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/activity_main_motion_scene">
<ImageView
android:id="@+id/image"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<SeekBar
android:id="@+id/seekBar"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_marginBottom="56dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>
布局预览如下图所示:
image.png修改 MainActivity
中的代码:
public class MainActivity extends AppCompatActivity {
private MotionLayout mMotionLayout;
private SeekBar mSeekBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_scene1);
mMotionLayout = findViewById(R.id.motionLayout);
mSeekBar = findViewById(R.id.seekBar);
mSeekBar.setMax(0);
mSeekBar.setMax(100);
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
mMotionLayout.setProgress((float) (progress * 0.01));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
}
效果如下图所示:
image监听 MotionLayout 过渡
可以调用 MotionLayout
的 setTransitionListener()
方法向 MotionLayout
对象注册一个过渡动画监听器,这个监听器可以监听过渡动画的播放进度和结束事件。
public void setTransitionListener(MotionLayout.TransitionListener listener)
TransitionListener
监听器接口:
public interface TransitionListener {
// 过渡动画正在运行时调用
void onTransitionChange(MotionLayout motionLayout, int startId, int endId, float progress);
// 过渡动画结束时调用
void onTransitionCompleted(MotionLayout motionLayout, int currentId);
}
提示:
TransitionListener
接口在alpha
版本中有所改动,可多出了2
个回调方法:onTransitionStarted
和onTransitionTrigger
。由于MotionLayout
还处于alpha
版本,并未正式发布,因此有所改动也是正常。
例:
MotionLayout motionLayout = findViewById(R.id.motionLayout);
motionLayout.setTransitionListener(new MotionLayout.TransitionListener() {
@Override
public void onTransitionChange(MotionLayout motionLayout, int i, int i1, float v) {
Log.d("App", "onTransitionChange: " + v);
}
@Override
public void onTransitionCompleted(MotionLayout motionLayout, int i) {
Log.d("App", "onTransitionCompleted");
}
});
结语
本篇文章到此就结束了,你可能会觉得前面的例子不够炫酷,这里给出一个炫酷点的例子(这个例子很简单,建议读者动手尝试实现一下):
MotionLayout Cool Demo好了,在文章最后放上一个小小的福利,以下为小编自己在学习过程中整理出的一个学习思路及方向,从事互联网开发,最主要的是要学好技术,而学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯,更加需要准确的学习方向达到有效的学习效果。
由于内容较多就只放上一个大概的大纲,需要更及详细的学习思维导图的加群 Android IOC架构设计免费获取。
群内还有免费的高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术资料,并且还有技术大牛一起讨论交流解决问题。