美文网首页jetpack
JetPack知识点实战系列十一:MotionLayout让动画

JetPack知识点实战系列十一:MotionLayout让动画

作者: chonglingliu | 来源:发表于2020-10-22 15:50 被阅读0次

    MotionLayoutConstraintLayout的子类,所以它是一种布局类型,但是它能够为布局属性添加动画效果,是开发者实现动画效果的另一个新的选择。

    MotionLayout基础

    让动画跑起来

    在入门练习的例子中,我们先利用MotionLayout实现一个View从左下(x25%,y75%)的位置移动到右上(x75%,y25%)的位置,如下图所示:

    位置移动
    • 添加依赖
    implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
    

    MotionLayoutconstraintlayout2.0以后才有的功能。

    • ConstraintLayout转为MotionLayout

    打开布局文件进入Design模式,选中ConstraintLayout右击弹出选项列表,点击Convert to MotionLayout,这样就进入了MotionLayout Editor界面。

    转换MotionLayout

    MotionLayout EditorAndroidStudio 4.0提供的功能。以前的版本是没有这个功能的。

    ConstraintLayout转换成MotionLayout后,我们会发现在res -> xml 下面多了一个MotionScene文件,动画的相关配置都是储存在这个文件中,而静态的布局依然在layout布局文件中。

    MotionScene

    MotionScene文件的代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <MotionScene 
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:motion="http://schemas.android.com/apk/res-auto">
    
        <Transition
            motion:constraintSetEnd="@+id/end"
            motion:constraintSetStart="@id/start"
            motion:duration="1000">
           <KeyFrameSet>
           </KeyFrameSet>
        </Transition>
    
        <ConstraintSet android:id="@+id/start">
        </ConstraintSet>
    
        <ConstraintSet android:id="@+id/end">
        </ConstraintSet>
    </MotionScene>
    

    我们目前只先看一看该文件的内容,主要包括Transition和两个ConstraintSet。至于他们分别代表的意义后面再做介绍。

    • 设置开始状态的位置

    我们回到MotionLayout Editor,在右上部分可以看到两个可以点击的区域,一个名字是start,一个是end。 他们代表的是动画效果两个状态---开始状态结束状态

    我们需要对imageButton进行动画,所以我们在start状态中勾选上imageButton

    设置方法: 点击start -> 选择imageButton -> 点击ConstraintSet( start ) 后面的 编辑按钮 -> 选择 Create Constraint

    start
    • 编辑结束状态的位置

    接下来我们编辑结束状态的位置,让imageButton位于右上(x75%,y25%)的位置。

    ConstraintSet

    开始状态我们不需要进行修改就是左下(x25%,y75%)的位置,

    修改结束状态的位置

    这时候我们点击startend 这两个ConstraintSet就能看出View位置的差别了。

    差别
    • Click触发动画

    MotionLayout动画可以点击OnClick触发,也可以滑动OnSwipe触发。

    点击startend 这两个ConstraintSet之间的Transition连线,然后勾选OnClick

    点击触发动画

    最后的效果如下:

    动画效果
    • 设置只能点击imageButton触发动画

    上面设置的是点击触发动画,我们可以设置成只有点击imageButton触发动画。

    设置方式:点击OnClick后面的+ -> 选择targetId,属性值为idimageButton

    点击imageButton触发

    MotionScene介绍

    经过一系列操作后,我们实现了imageButton的位移动画。
    这些是如何实现的呢?我们就回过来看看MotionScene文件的内容变化。

    <MotionScene 
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:motion="http://schemas.android.com/apk/res-auto">
    
        <Transition
            motion:constraintSetEnd="@+id/end"
            motion:constraintSetStart="@id/start"
            motion:duration="1000">
           <KeyFrameSet>
           </KeyFrameSet>
            <OnClick motion:targetId="@id/imageButton" />
        </Transition>
    
        <ConstraintSet android:id="@+id/start">
    
            <Constraint
                android:id="@+id/imageButton"
                motion:layout_constraintEnd_toStartOf="@+id/guideline"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                motion:layout_constraintBottom_toTopOf="@+id/guideline4"
                motion:layout_constraintTop_toTopOf="@+id/guideline4"
                motion:layout_constraintStart_toStartOf="@+id/guideline" />
        </ConstraintSet>
    
        <ConstraintSet android:id="@+id/end">
            <Constraint
                motion:layout_constraintVertical_bias="1.0"
                android:layout_height="wrap_content"
                motion:layout_constraintEnd_toStartOf="@id/guideline2"
                motion:layout_constraintStart_toStartOf="@id/guideline2"
                motion:layout_constraintTop_toTopOf="@id/guideline3"
                motion:layout_constraintBottom_toTopOf="@id/guideline3"
                motion:layout_constraintHorizontal_bias="0.0"
                android:layout_width="wrap_content"
                android:id="@+id/imageButton" />
        </ConstraintSet>
    </MotionScene>
    

    和我们刚才手动设置的对比,我们应该大概可以猜测出每个字段的含义了。

    1. Transition表示动画的过渡
    • constraintSetStart的值是开始的状态对应的id,表示从那个id对应的状态开始动画;
    • constraintSetEnd的值是结束的状态对应的id,表示过渡到那个id对应的状态;
    • duration表示的事动画的时长,单位是毫秒
    • 子标签OnClick 表示是点击触发。targetId表示点击对应的View触发。
    1. ConstraintSet表示约束数组,里面是一系列的View的约束值。代表的是动画的一个状态。
    2. KeyFrameSet表示的是关键帧的数组,设置的是动画中的某个关键帧的值,后面再做介绍。

    OnClick设置

    刚才我们点击imageButtonimageButtonstart状态和end状态来回切换,这是因为默认的motion:clickActiontoggle,即来回切换。它有5个值:

    • toggle:来回切换
    • jumpToStart:瞬间跳转到start状态,没有动画
    • jumpToEnd:瞬间跳转到end状态,没有动画
    • transitionToStart:动画过渡到start状态
    • transitionToEnd:动画过渡到end状态

    接下来我们设置为transitionToEnd,这样点击的效果就是:如果当前是start状态,会过渡到end状态,如果当前是end状态,点击没有效果。

    设置方法:点击OnClick后面的加号(+) -> 设置tools:clickAction的值为transitionToEnd

    transitionToEnd

    最后的效果如下:

    手机效果

    弧形运动 Arc Motion

    我们上面的运动轨迹是直线运动,我们也可以设置弧线运动。

    设置方法为给start状态的imageButton添加motion:pathMotionArc属性。它有几个常用的值:

    设置
    • startHorizontal:运动的前部分时间水平方向会移动更多的比例
    startHorizontal
    • startVertical:运动的前部分时间竖直方向会移动更多的比例
    startVertical
    • none:直线运动

    多个ConstraintLayout属性同时动画

    ConstraintSet能修改ConstraintLayout属性这个是很显然的,包括位置大小等。
    主要但不限于:

    • alpha
    • visibility
    • elevation
    • rotation
    • rotationX
    • rotationY
    • translationX
    • translationY
    • translationZ
    • scaleX
    • scaleY

    这些属性是可以同时设置动画的,譬如我们想位置移动的时候还进行旋转270度,然后透明度逐渐变为0。

    设置方法end状态的imageButton添加alpha属性和rotation属性。

    旋转加透明

    效果如下所示:

    多个属性

    自定义属性

    其实除了上面的属性,还能对任何自定义的属性进行动画。这里的自定义属性就是除了上面的属性之外的属性。

    如何找到能进行动画属性呢?其实很简单,找到对应Viewgetter/setter 方法,把get/set前缀去掉,把第一个字母小写就是对应的属性了。

    譬如setBackgroundColor()方法,找到对应的属性就是 backgroundColor

    设置方式:点击CustomAttributes后面的加号 -> 弹出的框中选择属性并输入属性值。

    设置自定义属性

    MotionLayout的一些开发属性

    我们目前还没研究MotionLayout如何和MotionScene关联起来的,我们来看看MotionLayout文件。

    MotionLayout提供了一些开发的属性,可以方便查看动画的一些属性。

    <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"
        tools:context=".Demo1Activity"
        app:layoutDescription="@xml/layout_activity_demo1_scene">
        ...
    </androidx.constraintlayout.motion.widget.MotionLayout>
    

    我们可以看到layoutDescription指向了MotionScene。此外,MotionLayout还有其他一些属性,可以帮助开发中方便查看动画的信息。

    • app:showPaths:显示动画的轨迹
    • app:motionProgress:设置动画进度的某个关键点
    • app:motionDebug:显示动画的信息

    如果我们设置app:motionDebug="SHOW_ALL"则我们可以看到一些辅助信息。

    <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"
        tools:context=".Demo1Activity"
        app:layoutDescription="@xml/layout_activity_demo1_scene"
        app:motionDebug="SHOW_ALL"
        >
        ...
    </androidx.constraintlayout.motion.widget.MotionLayout>
    
    debug

    图片变换

    MotionLayout提供了可以对显示的图片进行动画切换的类ImageFilterViewImageFilterButton

    设置方法

    1. ImageFilterView设置srcCompataltSrc, 他们分别代表开始显示的第一张图片和第二张图片。
    <androidx.constraintlayout.utils.widget.ImageFilterView
        android:id="@+id/image_filter_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/xixi"
        app:altSrc="@drawable/duoduo"
        />
    
    1. 显示第一张图片时自定义属性crossfade设置为0, 显示第一张图片时自定义属性crossfade设置为1.
    crossfade

    效果如下:

    过渡效果

    当然ImageFilterView还提供了其他一些自定义属性可以进行操作:

    • warmth:
    • saturation
    • contrast
    • round
    • roundPercent

    例如我们可以修改saturation,达到如下的效果:

    修改饱和度

    关键帧 Keyframes

    我把关键帧单独拎出来分析是因为我觉得它是动画的灵魂。为什么这么说呢?因为我们前面的内容只是定义了开始状态和结束状态,中间状态的修改就需要关键帧来实现了。

    如果有印象的话应该还记得MotionScene文件中有见到过KeyFrameSet,没错它就是用来存放关键帧的数组。

    接下来我们就来分别看下几种关键帧的使用。

    我们创建关键帧是点击Transition后面的那个带加号的计时器按钮,点击后会弹出来供选择一种关键帧。

    新建关键帧

    位置关键帧 Position Keyframes

    位置关键帧是在某一帧的位置修改View的位置,想想应该知道这个关键帧类型也就只能修改位置相关的属性了。

    改变位置就需要标定位置的坐标系,有三种坐标系:

    • parentRelative:x,y的坐标是相对父容器,值是从0.0-1.0
    parentRelative
    • deltaRelative:x,y的坐标系是相对于起始点位置,起点的坐标为(0, 0),终点的位置为(1, 1)。x为水平方向,y为垂直方向。
    deltaRelative
    • pathRelative:起点和终点的连线方向是X轴,的坐标是(0, 0),终点的坐标是(1.0, 0),然后和X轴垂直的方向为Y轴。
    pathRelative

    以上三个图片来源的参阅地址

    我们做个例子设置两个位置关键帧:

    在0.25的时候位于父坐标的(0.75, 0.75)

    在0.75的时候位于父坐标的(0.25, 0.25)

    设置关键帧

    设置完两个关键帧后的预览效果如下:

    gif1.gif

    哈哈,有没有感觉有问题,有点和基线没对齐哦~~ 目前我也不知道为什么,看有没有哪位大哥知道什么问题可以留言给我~~

    这两个点关键镇的点用deltaRelative坐标系去定位能够定位准确位置。所以如果你感觉位置不准确可以用deltaRelative坐标系试试。

    实际效果:

    位置关键帧效果

    这些位置的变换都是匀速的,可以设置transitionEasing的值改变变化速率。

    • Standard easing
    Standard easing
    • Accelerate easing
    Accelerate easing
    • Decelerate easing
    Decelerate easing

    以上三个图片来源的参阅地址

    属性关键帧 Attribute Keyframes

    属性关键帧是改变属性值,它既可以位置相关的属性,也可以改变自定义属性。

    我们做个例子在.5这个关键帧位置设置,scaleX1.5scaleY1.5alpha0.5,backgroundColor#FF00FF

    具体的设置方法和位置关键帧类似,这里不做过多介绍,只介绍下设置结果:

    设置

    代码如下:

    <KeyFrameSet>
        <KeyAttribute
            motion:motionTarget="@+id/imageButton"
            motion:framePosition="50"
            android:alpha="0.5"
            android:scaleX="1.5"
            android:scaleY="1.5">
            <CustomAttribute
                motion:attributeName="backgroundColor"
                motion:customColorValue="#FF00FF" />
        </KeyAttribute>
        <KeyPosition
            motion:motionTarget="@+id/imageButton"
            motion:framePosition="25"
            motion:keyPositionType="parentRelative"
            motion:percentX="0.75"
            motion:percentY="0.75" />
        <KeyPosition
            motion:motionTarget="@+id/imageButton"
            motion:framePosition="75"
            motion:keyPositionType="parentRelative"
            motion:percentX="0.25"
            motion:percentY="0.25" />
    </KeyFrameSet>
    
    组合效果

    关键触发关键帧 KeyTrigger Keyframes

    这个关键帧是当动画时间轴到达某个时间点后触发调用View的某个方法。当然也会有方向的考虑。

    我们做个例子,当正向动画时间轴超过90%以后imageButton替换图片;当反向动画时间轴超过90%以后imageButton恢复图片。

    注意:时间轴的时间都是按照正向计算的,正向和反向只是动画的内容,和时间轴的进度计算没有关系。

    实现这个功能需要自定义两个方法,所以我们来自定义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,并且实现了两个方法changeImagerevertImage

    • 将布局文件中的ImageButton替换成MyImageButton
    <com.johnny.motionlayoutdemo.MyImageButton
        android:id="@+id/imageButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/emoji"
        app:layout_constraintBottom_toTopOf="@+id/guideline4"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="@+id/guideline4" />
    
    • 添加KeyTrigger Keyframes

    添加KeyTrigger Keyframes的方法和上面类似,这里不做过多介绍,只介绍下设置结果:

    KeyTrigger

    MotionScene文件中对应的代码如下:

    <KeyFrameSet>
        <KeyTrigger
            motion:motionTarget="@+id/imageButton"
            motion:framePosition="90"
            motion:onPositiveCross="changeImage"
            />
        <KeyTrigger
            motion:motionTarget="@+id/imageButton"
            motion:framePosition="90"
            motion:onNegativeCross="revertImage"
            />
        <KeyTrigger
            motion:motionTarget="@+id/imageButton"
            motion:framePosition="90"
            motion:onNegativeCross="show"
            />
        ...
    </KeyFrameSet>
    

    最后的效果如下所示:

    KeyTrigger

    代码启动动画和监听动画

    我们可以通过ClickSwipe触发动画,也可以通过代码触发动画。

    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
                    }
                }
            )
    

    MotionLayout案例分析

    案例1:点赞和取消点赞

    效果图

    问题分析:

    • 替换图片

    (ImageFilterViewcrossfade组合实现)

    • 正向动画的时候有一些放大缩小的过度效果

    加一些属性关键帧(Attribute Keyframes),在几个关键帧加入ScaleXScaleY属性变化值

        <Transition
            motion:constraintSetEnd="@+id/end"
            motion:constraintSetStart="@id/start"
            motion:duration="500"
            android:id="@+id/forward"
            >
           <KeyFrameSet>
               <KeyAttribute
                   motion:motionTarget="@+id/praise_image_view"
                   motion:framePosition="20"
                   android:scaleX="0.3"
                   android:scaleY="0.3"
                   motion:transitionEasing="decelerate"
                   >
                   <CustomAttribute
                       motion:attributeName="crossfade"
                       motion:customFloatValue="1" />
               </KeyAttribute>
               <KeyAttribute
                   motion:motionTarget="@+id/praise_image_view"
                   motion:framePosition="80"
                   android:scaleX="1.2"
                   android:scaleY="1.2"/>
               <KeyAttribute
                   motion:motionTarget="@+id/praise_image_view"
                   motion:framePosition="90"
                   android:scaleX="0.9"
                   android:scaleY="0.9"/>
           </KeyFrameSet>
        </Transition>
    
    • 反向动画的时候没有过渡动画,只是替换了图片

    不能复用正向的过渡动画即使用(toogle),目前也不提供用代码将动画修改成jumpToEnd的方法,只能新建一个反向的Transition

    1. 新建Transition
    新建Transition
    1. 修改代码
    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) {
                }
            })
        }
    }
    

    案例2:循环旋转和文字切换

    效果图

    问题分析:

    • 文字切换

    自定义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)
        }
    
    }
    
               <KeyTrigger
                   motion:motionTarget="@+id/circle_tv"
                   motion:framePosition="35"
                   motion:onCross="changeTextToAdd" />
               <KeyTrigger
                   motion:motionTarget="@+id/circle_tv"
                   motion:framePosition="85"
                   motion:onCross="changeToM" />
    
    • 旋转

    加一些属性关键帧

           <KeyFrameSet>
               <KeyAttribute
                   motion:motionTarget="@+id/circle_iv"
                   motion:framePosition="20"
                   android:rotation="0" />
               <KeyAttribute
                   motion:motionTarget="@+id/circle_iv"
                   motion:framePosition="50"
                   android:rotation="360" />
               <KeyAttribute
                   motion:motionTarget="@+id/circle_iv"
                   motion:framePosition="70"
                   android:rotation="360" />
               <KeyAttribute
                   motion:motionTarget="@+id/circle_tv"
                   motion:framePosition="20"
                   android:scaleX="1.0"
                   android:scaleY="1.0"
                   />
               <KeyAttribute
                   motion:motionTarget="@+id/circle_tv"
                   motion:framePosition="35"
                   android:scaleX="0.1"
                   android:scaleY="0.1"
                   />
               <KeyAttribute
                   motion:motionTarget="@+id/circle_tv"
                   motion:framePosition="50"
                   android:scaleX="1.0"
                   android:scaleY="1.0"
                   />
               <KeyAttribute
                   motion:motionTarget="@+id/circle_tv"
                   motion:framePosition="70"
                   android:scaleX="1.0"
                   android:scaleY="1.0"
                   />
               <KeyAttribute
                   motion:motionTarget="@+id/circle_tv"
                   motion:framePosition="85"
                   android:scaleX="0.1"
                   android:scaleY="0.1"
                   />
    
    • 不断的循环

    监听动画完成后,让进度跳转调第一帧

    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) {
                }
            })
    
        }
    
    }
    

    相关文章

      网友评论

        本文标题:JetPack知识点实战系列十一:MotionLayout让动画

        本文链接:https://www.haomeiwen.com/subject/eixpmktx.html