MotionLayout是ConstraintLayout的子类,所以它是一种布局类型,但是它能够为布局属性添加动画效果,是开发者实现动画效果的另一个新的选择。
在入门练习的例子中,我们先利用MotionLayout实现一个View从左下(x25%,y75%)
的位置移动到右上(x75%,y25%)
的位置,如下图所示:
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
MotionLayout是constraintlayout2.0以后才有的功能。
打开布局文件进入Design模式,选中ConstraintLayout右击弹出选项列表,点击Convert to MotionLayout,这样就进入了MotionLayout Editor界面。
MotionLayout Editor是AndroidStudio 4.0提供的功能。以前的版本是没有这个功能的。
将ConstraintLayout转换成MotionLayout后,我们会发现在res -> xml 下面多了一个MotionScene文件,动画的相关配置都是储存在这个文件中,而静态的布局依然在layout布局文件中。
该MotionScene文件的代码如下:
我们目前只先看一看该文件的内容,主要包括Transition
和两个ConstraintSet
。至于他们分别代表的意义后面再做介绍。
我们回到MotionLayout Editor,在右上部分可以看到两个可以点击的区域,一个名字是start,一个是end。 他们代表的是动画效果两个状态—开始状态和结束状态。
我们需要对imageButton
进行动画,所以我们在start状态中勾选上imageButton
。
设置方法: 点击start -> 选择imageButton -> 点击ConstraintSet( start ) 后面的 编辑按钮 -> 选择 Create Constraint。
接下来我们编辑结束状态的位置,让imageButton
位于右上(x75%,y25%)
的位置。
开始状态我们不需要进行修改就是左下(x25%,y75%)
的位置,
这时候我们点击start和end 这两个ConstraintSet就能看出View位置的差别了。
MotionLayout动画可以点击OnClick触发,也可以滑动OnSwipe触发。
点击start和end 这两个ConstraintSet之间的Transition连线,然后勾选OnClick。
最后的效果如下:
上面设置的是点击触发动画,我们可以设置成只有点击imageButton触发动画。
设置方式:点击OnClick后面的**+** -> 选择targetId,属性值为id即imageButton。
经过一系列操作后,我们实现了imageButton
的位移动画。
这些是如何实现的呢?我们就回过来看看MotionScene文件的内容变化。
和我们刚才手动设置的对比,我们应该大概可以猜测出每个字段的含义了。
constraintSetStart
的值是开始的状态对应的id,表示从那个id对应的状态开始动画;constraintSetEnd
的值是结束的状态对应的id,表示过渡到那个id对应的状态;duration
表示的事动画的时长,单位是毫秒OnClick
表示是点击触发。targetId
表示点击对应的View触发。刚才我们点击imageButton
,imageButton在start状态和end状态来回切换,这是因为默认的motion:clickAction
是toggle,即来回切换。它有5个值:
接下来我们设置为transitionToEnd,这样点击的效果就是:如果当前是start状态,会过渡到end状态,如果当前是end状态,点击没有效果。
设置方法:点击OnClick后面的加号(+) -> 设置tools:clickAction
的值为transitionToEnd。
最后的效果如下:
我们上面的运动轨迹是直线运动,我们也可以设置弧线运动。
设置方法为给start状态的imageButton添加motion:pathMotionArc属性。它有几个常用的值:
ConstraintSet能修改ConstraintLayout属性这个是很显然的,包括位置大小等。
主要但不限于:
这些属性是可以同时设置动画的,譬如我们想位置移动的时候还进行旋转270度,然后透明度逐渐变为0。
设置方法:end状态的imageButton添加alpha属性和rotation属性。
效果如下所示:
其实除了上面的属性,还能对任何自定义的属性进行动画。这里的自定义属性就是除了上面的属性之外的属性。
如何找到能进行动画属性呢?其实很简单,找到对应View的getter/setter 方法,把get/set前缀去掉,把第一个字母小写就是对应的属性了。
譬如setBackgroundColor()
方法,找到对应的属性就是 backgroundColor。
设置方式:点击CustomAttributes后面的加号 -> 弹出的框中选择属性并输入属性值。
我们目前还没研究MotionLayout如何和MotionScene关联起来的,我们来看看MotionLayout文件。
MotionLayout提供了一些开发的属性,可以方便查看动画的一些属性。
...
我们可以看到layoutDescription
指向了MotionScene。此外,MotionLayout还有其他一些属性,可以帮助开发中方便查看动画的信息。
如果我们设置app:motionDebug="SHOW_ALL"
则我们可以看到一些辅助信息。
...
MotionLayout提供了可以对显示的图片进行动画切换的类ImageFilterView和ImageFilterButton,
设置方法:
srcCompat
和altSrc
, 他们分别代表开始显示的第一张图片和第二张图片。
crossfade
设置为0, 显示第一张图片时自定义属性crossfade
设置为1.效果如下:
当然ImageFilterView还提供了其他一些自定义属性可以进行操作:
例如我们可以修改saturation,达到如下的效果:
我把关键帧单独拎出来分析是因为我觉得它是动画的灵魂。为什么这么说呢?因为我们前面的内容只是定义了开始状态和结束状态,中间状态的修改就需要关键帧来实现了。
如果有印象的话应该还记得MotionScene文件中有见到过KeyFrameSet,没错它就是用来存放关键帧的数组。
接下来我们就来分别看下几种关键帧的使用。
我们创建关键帧是点击Transition后面的那个带加号的计时器按钮,点击后会弹出来供选择一种关键帧。
位置关键帧是在某一帧的位置修改View的位置,想想应该知道这个关键帧类型也就只能修改位置相关的属性了。
改变位置就需要标定位置的坐标系,有三种坐标系:
以上三个图片来源的参阅地址
我们做个例子设置两个位置关键帧:
在0.25的时候位于父坐标的(0.75, 0.75)
在0.75的时候位于父坐标的(0.25, 0.25)
设置完两个关键帧后的预览效果如下:
哈哈,有没有感觉有问题,有点和基线没对齐哦~~ 目前我也不知道为什么,看有没有哪位大哥知道什么问题可以留言给我~~
这两个点关键镇的点用deltaRelative坐标系去定位能够定位准确位置。所以如果你感觉位置不准确可以用deltaRelative坐标系试试。
实际效果:
这些位置的变换都是匀速的,可以设置transitionEasing
的值改变变化速率。
以上三个图片来源的参阅地址
属性关键帧是改变属性值,它既可以位置相关的属性,也可以改变自定义属性。
我们做个例子在.5这个关键帧位置设置,scaleX
为1.5,scaleY
为1.5,alpha
为0.5,backgroundColor
为**#FF00FF**
具体的设置方法和位置关键帧类似,这里不做过多介绍,只介绍下设置结果:
代码如下:
这个关键帧是当动画时间轴到达某个时间点后触发调用View的某个方法。当然也会有方向的考虑。
我们做个例子,当正向动画时间轴超过90%以后imageButton
替换图片;当反向动画时间轴超过90%以后imageButton
恢复图片。
注意:时间轴的时间都是按照正向计算的,正向和反向只是动画的内容,和时间轴的进度计算没有关系。
实现这个功能需要自定义两个方法,所以我们来自定义ImageButton
class MyImageButton @JvmOverloads constructor (context: Context, set: AttributeSet? = null, style: Int = 0) : AppCompatImageButton(context, set, style) {
// 替换图片
fun changeImage() {
this.setImageDrawable(resources.getDrawable(R.drawable.duoduo))
}
// 恢复图片
fun revertImage() {
this.setImageDrawable(resources.getDrawable(R.drawable.emoji))
}
}
我们自定义了MyImageButton,并且实现了两个方法changeImage和revertImage。
添加KeyTrigger Keyframes的方法和上面类似,这里不做过多介绍,只介绍下设置结果:
MotionScene文件中对应的代码如下:
...
最后的效果如下所示:
我们可以通过Click和Swipe触发动画,也可以通过代码触发动画。
motionLayout.setTransitionDuration(3000)
motionLayout.transitionToState(R.id.end)
我们通过以上代码先修改动画时长,然后开始动画。
提示:
motionLayout
就是MotionLayout
我们有可以通过代码监听动画的进程
motionLayout.setTransitionListener(
object: MotionLayout.TransitionListener {
override fun onTransitionTrigger(motionLayout: MotionLayout?,
triggerId: Int, positive: Boolean, progress: Float) {
// Called when a trigger keyframe threshold is crossed
}
override fun onTransitionStarted(motionLayout: MotionLayout?,
startId: Int, endId: Int) {
// Called when the transition starts
}
override fun onTransitionChange(motionLayout: MotionLayout?,
startId: Int, endId: Int, progress: Float) {
// Called each time a property changes. Track progress value to find
// current position
}
override fun onTransitionCompleted(motionLayout: MotionLayout?,
currentId: Int) {
// Called when the transition is complete
}
}
)
问题分析:
(ImageFilterView和crossfade组合实现)
加一些属性关键帧(Attribute Keyframes),在几个关键帧加入ScaleX和ScaleY属性变化值
不能复用正向的过渡动画即使用(toogle),目前也不提供用代码将动画修改成jumpToEnd的方法,只能新建一个反向的Transition
- 新建Transition
- 修改代码
class PraiseActivity : AppCompatActivity() { private var praised = false private var isAnimation = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_praise) // 1. 点击触发动画 praise_image_view.setOnClickListener { if (isAnimation) return@setOnClickListener isAnimation = true if (praised) { // 2. 设置动画的Transition然后开始动画 motionLayout.setTransition(R.id.revert) motionLayout.transitionToEnd() } else { motionLayout.setTransition(R.id.forward) motionLayout.transitionToEnd() } } // 3. 监听动画的完成后记录状态值 motionLayout.setTransitionListener(object : MotionLayout.TransitionListener{ override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) { } override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) { } override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) { praised = !praised isAnimation = false } override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { } }) } }
问题分析:
自定义TextView然后添加两个方法,在关键帧触发
class MyTextView@JvmOverloads constructor (context: Context, set: AttributeSet? = null, style: Int = 0) : AppCompatTextView(context, set, style) { // 文字变成加号 fun changeTextToAdd() { this.text = "+" this.setTextSize(TypedValue.COMPLEX_UNIT_SP, 80.0F) } // 文字变成M fun changeToM() { this.text = "M" this.setTextSize(TypedValue.COMPLEX_UNIT_SP, 50.0F) } }
加一些属性关键帧
监听动画完成后,让进度跳转调第一帧
class CycleCircleActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_cycler_circle) // 进入页面开始动画 motionLayout.post { motionLayout.setTransition(R.id.start, R.id.end) motionLayout.transitionToEnd() } motionLayout.setTransitionListener(object : MotionLayout.TransitionListener { override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) { p0?.isInteractionEnabled = false } override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) { } override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) { p0?.isInteractionEnabled = true // 跳转到第一帧 p0?.progress = 0.0F // 开始动画 p0?.transitionToEnd() } override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { } }) } }