阅读说明:php
ConstraintLayout
。MotionLayout
基础教程,如您已了解如何使用 MotionLayout
,本文可能对您帮助不大。ConstraintLayout 2.0.0-alpha4
版本编写,建议读者优先使用这一版本。MotionLayout
官方文档不全,有些知识点是根据笔者本身的理解总结的,若有错误,欢迎指正。添加支持库:html
dependencies {
...
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4'
}
复制代码
MotionLayout
最低支持到 Android 4.3(API 18)
,还有就是 MotionLayout
是 ConstraintLayout 2.0
添加的,所以必须确保支持库的版本不低于 2.0
。java
MotionLayout
类继承自 ConstraintLayout
类,容许你为各类状态之间的布局设置过渡动画。因为 MotionLayout
继承了 ConstraintLayout
,所以能够直接在 XML
布局文件中使用 MotionLayout
替换 ConstraintLayout
。android
MotionLayout
是彻底声明式的,你能够彻底在 XML
文件中描述一个复杂的过渡动画而 无需任何代码(若是您打算使用代码建立过渡动画,那建议您优先使用属性动画,而不是 MotionLayout
)。app
因为 MotionLayout
类继承自 ConstraintLayout
类,所以能够在布局中使用 MotionLayout
替换掉 ConstraintLayout
。框架
MotionLayout
与 ConstraintLayout
不一样的是,MotionLayout
须要连接到一个 MotionScene
文件。使用 MotionLayout
的 app:layoutDescription
属性将 MotionLayout
连接到一个 MotionScene
文件。ide
例:布局
<?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
)。post
MotionScene
文件描述了两个场景间的过渡动画,存放在 res/xml
目录下。gradle
要使用 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
值】设置拖动操做要关联到的对象,让触摸操做看起来像是在拖动这个对象的由 app:touchAnchorSide
属性指定的那个边。app:touchAnchorSide
:设置触摸操做将会拖动对象的哪一边,共有如下 4
个可选值:
top
left
right
bottom
app:dragDirection
:设置拖动的方向(注意,只有设置了 app:touchAnchorId
属性后该属性才有效)。共有如下 4
个可选值:
dragUp
:手指从下往上拖动(↑)。dragDown
:手指从上往下拖动(↓)。dragLeft
:手指从右往左拖动(←)。dragRight
:手指从左往右拖动(→)。app:maxVelocity
:【浮点值】设置动画在拖动时的最大速度(单位:像素每秒 px/s
)。app:maxAcceleration
:【浮点值】设置动画在拖动时的最大加速度(单位:像素每二次方秒 px/s^2
)。能够同时设置 <OnClick>
与 <OnSwipe>
,或者都不设置,而是使用代码来触发过渡。
还能够在 <Transition>
元素中设置多个 <OnClick>
,每一个 <OnClick>
均可以关联到一个不一样的控件上。虽然 <Transition>
元素中也能够设置多个 <OnSwipe>
,可是后面的 <OnSwipe>
会替换掉前面的 <OnSwipe>
,最终使用的是最后一个 <OnSwipe>
。
<OnSwipe>
拖动操做因为 <OnSwipe>
拖动操做涉及的交互较为复杂,这里单独对它的如下 3
个属性进行说明:
app:touchAnchorId
app:dragDirection
app:touchAnchorSide
首先是 app:touchAnchorId
属性与 app:dragDirection
属性。app:touchAnchorId
属性用于设置拖动操做要关联到的对象;app:dragDirection
属性用于指定拖动方向。
默认状况下,由上往下
拖动时会运行过渡动画,此时 <OnSwipe/>
元素不须要设置任何属性,只要在 <Transition>
中加一个 <OnSwipe/>
标签便可。
例:
<Transition ...>
<OnSwipe/>
</Transition>
复制代码
可是,若是你要支持 由下往上
(↑)或者 由左往右
(→)或者 由右往左
(←),那么至少应该设置好 app:touchAnchorId
与 app:dragDirection
属性。
app:dragDirection
属性设置的拖动方向与 app:touchAnchorId
属性关联到的对象在 Start
场景和 End
场景中的位置是息息相关的。例以下图 a
中,End
场景中的 Widget
位于 Start
场景中的 Widget
的上方,那么应该设置 app:dragDirection="dragUp"
。再看图 b
中,End
场景中的 Widget
位于 Start
场景中的 Widget
的右边,那么应该设置 app:dragDirection="dragRight"
:
若是 End
场景中的 Widget
相对于 Start
场景中的 Widget
是倾斜的(以下图所示),将会有两个可选的方向,下图中的可选方向是 dragUp
、dragRight
,具体使用哪一个方向,由你本身决定。
设置一个正确的拖动方向是很是重要的,不然拖动时,过渡动画将表现不佳。
提示:
MotionLayout
将使用app:touchAnchorId
关联到的对象在app:dragDirection
方向上的拖动进度(progress
)做为整个过渡动画的进度,当关联对象在app:dragDirection
方向上的拖动完成时,也就意味着整个过渡动画完成了。
例:实现拖动效果
删除 <Transition>
元素元素的 <OnClick>
标签,并加入一个 <OnSwipe>
标签,修改后的 MotionScene
文件内容以下所示:
<?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,加入 OnSwipe -->
<OnSwipe app:touchAnchorId="@id/image" app:dragDirection="dragUp"/>
</Transition>
</MotionScene>
复制代码
注意:若是将 <OnClick>
和 <OnSwipe>
关联到了同一个控件,或者 <OnSwipe>
关联到的那个控件是可点击的,点击事件将会影响到拖动,你将没法按住控件进行拖动,只能按住控件的外面才能拖动。
例:
在 <Transition>
标签中加入 <OnClick>
,修改后的 MotionScene
文件内容以下所示:
<?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">
<OnSwipe app:touchAnchorId="@id/image" app:dragDirection="dragUp"/>
<!-- 加入 OnClick -->
<OnClick app:targetId="@id/image" app:clickAction="toggle"/>
</Transition>
</MotionScene>
复制代码
效果以下所示:
app:touchAnchorSide
属性:
app:touchAnchorSide
属性的功能是 “设置触摸操做将会拖动对象的哪一边”,该属性可用于实现可折叠效果,例如可折叠标题栏。
例:在底部实现一个向上拉的折叠效果。
1. 修改 acticity_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" />
<!-- 增长如下代码 -->
<FrameLayout android:id="@+id/bottomBar" android:layout_width="match_parent" android:layout_height="0dp" android:background="@color/colorPrimary" app:layout_constraintTop_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent">
<ImageView android:layout_gravity="center" android:src="@mipmap/ic_launcher" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</FrameLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>
复制代码
2. 修改 acticity_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" />
<!-- 增长如下代码 -->
<FrameLayout android:id="@+id/bottomBar" android:layout_width="match_parent" android:layout_height="120dp" android:background="@color/colorPrimary" app:layout_constraintBottom_toBottomOf="parent">
<ImageView android:layout_gravity="center" android:src="@mipmap/ic_launcher" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</FrameLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>
复制代码
3. 修改 MotionScene
文件(文件名:activity_main_motion_scene.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">
<!-- 关联到 bottomBar 上-->
<OnSwipe app:touchAnchorId="@id/bottomBar" app:touchAnchorSide="top" app:dragDirection="dragUp"/>
<OnClick app:targetId="@id/image" app:clickAction="toggle"/>
</Transition>
</MotionScene>
复制代码
效果以下所示:
提示:其实
<OnSwipe>
能够不关联到bottomBar
上,在由于在前面的例子中咱们已经把<OnSwipe>
关联到了image
上,且拖动方向也设置正确(drageUp
),这样其实已经能够正常拖动了。可是因为bottomBar
是可折叠的,把<OnSwipe>
拖动关联到它上面更加合适,这样能够设置app:touchAnchorSide="top"
,告诉MotionLayout
控件bottomBar
的上边界是可拖动的,这样更符合语义。
除了使用 <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
场景。
效果以下所示:
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>
复制代码
布局预览以下图所示:
修改 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) {
}
});
}
}
复制代码
效果以下图所示:
能够调用 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");
}
});
复制代码
本篇文章到此就结束了,你可能会以为前面的例子不够炫酷,这里给出一个炫酷点的例子(这个例子很简单,建议读者动手尝试实现一下):
后续文章:
参考文章: