MotionLayout 学习笔记3 - 关键帧基础

270 阅读4分钟

MotionLayout 学习笔记 - 关键帧基础

起始代码

activity_key_frame_basic.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/activity_key_frame_basic_scene"
    app:motionDebug="SHOW_ALL"
    tools:context=".KeyFrameBasicActivity">

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/view"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#3dda84"
        android:padding="10dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_android_48dp" />

</androidx.constraintlayout.motion.widget.MotionLayout>

activity_key_frame_basic_scene.xml

<?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="2000">
        <KeyFrameSet></KeyFrameSet>
        <OnClick motion:targetId="@+id/view" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@id/view">
            <Layout
                android:layout_width="80dp"
                android:layout_height="80dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintEnd_toEndOf="parent" />
        </Constraint>
    </ConstraintSet>
</MotionScene>
KeyFrame_起始代码.gif

属性关键帧

我们来添加一个属性关键帧 <KeyAttribute> ,所谓属性关键帧,就是让我们的控件在执行的动画过程中,某个(某些)属性的值从初始状态值逐渐变化到关键帧所设置的值,我们来试一试

<KeyFrameSet>
	<KeyAttribute
    	android:rotation="180"
    	android:scaleX="2"
	    android:scaleY="2"
    	motion:framePosition="50"
    	motion:motionTarget="@+id/view" />
</KeyFrameSet>

motionTarget 指定关键帧作用在哪个 view,motion:framePosition="50" 表示将这个关键帧打在动画执行到 50% 的时刻,scaleXscaleYrotation 则是指定相关的属性值。

KeyFrame_属性关键帧.gif

可以看到,progress=50 的时候,view 被放大到 2 倍,旋转角度为 180。

位置关键帧

顾名思义,位置关键帧,就是在指定的动画进度,控件移动到指定的位置。

<KeyFrameSet>
	<KeyPosition
    	motion:framePosition="50"
	    motion:keyPositionType="parentRelative"
    	motion:motionTarget="@+id/view"
	    motion:percentX="0.6"
    	motion:percentY="0.15" />
</KeyFrameSet>

keyPositionType 指定坐标的类型,parentRelativekeyPositionType 使用与屏幕相同的坐标系。它会将 (0, 0) 定义为整个 MotionLayout 的左上角,并将 (1, 1) 定义为右下角。

parentRelative.png



KeyFrame_位置关键帧_1.gif

现在,动画的前半部分将 view 移动到屏幕水平的 60%,垂直的 15% 的位置,后半部分再将其移动到最终位置。

关键帧也可以指定 pathMotionArc,但是要注意,要使关键帧的 pathMotionArc 生效,必须在开始场景里也配置 pathMotionArc

</MotionScene>
	<Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="2000">
        <KeyFrameSet>
            <KeyPosition
            	motion:pathMotionArc="startVertical"
                 motion:framePosition="50"
                 motion:keyPositionType="parentRelative"
                 motion:motionTarget="@+id/view"
                 motion:percentX="0.6"
                 motion:percentY="0.15" />
        </KeyFrameSet>
        ...
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@id/view">
        	<Motion motion:pathMotionArc="startVertical" />
        </Constraint>
    </ConstraintSet>
	...
</MotionScene>
KeyFrame_关键帧_pathMotionArc.gif

再来添加一个位置关键帧,将水平位置设置为 percentX="1"

<MotionScene
    ...
    <Transition
        ...
        <KeyFrameSet>
            <KeyPosition
                motion:framePosition="50"
                motion:keyPositionType="parentRelative"
                motion:motionTarget="@+id/view"
                motion:pathMotionArc="startVertical"
                motion:percentX="0.6"
                motion:percentY="0.15" />

            <KeyPosition
                motion:framePosition="75"
                motion:keyPositionType="parentRelative"
                motion:motionTarget="@+id/view"
                motion:percentX="1"
                motion:percentY="0.5" />
        </KeyFrameSet>
        ...
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@id/view">
            <Motion motion:pathMotionArc="startVertical" />
        </Constraint>
    </ConstraintSet>
    ...
</MotionScene>
KeyFrame_关键帧_坐标边界问题.gif

不知道你是否有这么一个疑问?我不是设置了 percentX="1" 吗?以 view 的中心点来定位的话,那么 view 应该会有一半跑到屏幕外啊?这就要看看 keyPositionType="parentRelative" 了,为什么是 parentRelative 不是 parent 呢,其实 MotionLayout 会根据控件的宽高对坐标系做一定的偏移,相对于父布局偏移,所以名字 parentRelative 可以说是很合理了

parentRelative坐标.png

keyPositionType

MotionLayout 坐标系属于笛卡尔坐标系

MotionLayout 中的所有坐标系在 X 轴和 Y 轴上使用的值都介于 0.01.0 之间。它们允许使用负值和大于 1.0 的值。

parentRelative

parentRelative 前面已经说了,就不再重复了

parentRelative.png
deltaRelative

增量是一个关于变化的数学术语,deltaRelative 是表示“相对变化”的一种方式。在 deltaRelative 坐标中,(0,0) 是视图的起始位置,(1,1) 是结束位置。X 轴和 Y 轴与屏幕对齐。

注意看上面和下面的图片,上面 parentRelative 的坐标系原点就是屏幕(白色部分)的左上角,而 deltaRelative 的坐标系原点在控件的起始位置。

deltaRelative.png

parentRelative 比较,主要区别在于这种坐标仅仅描述控件移动将要经过的部分屏幕。

deltaRelative 很适合用于单独控制水平或垂直平移。比如,我的控件开始位置是在相对父布局的左上角,结束位置相当于右移 250dp,当动画到 1/4 (时间完成度 25%)的时候,我想让它右移的动画完成一半(125dp),用 parentRelative 能做吗?做不了,因为 250dp 的一半(125dp)指定不了,屏幕宽 * ? = 125dp,但是 deltaRelative 就可以

delta_relative_坐标系.png
<?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="30000"
        motion:motionInterpolator="linear">
        <OnClick
            motion:clickAction="transitionToEnd|jumpToStart"
            motion:targetId="@id/view" />
        <KeyFrameSet>
            <KeyPosition
                motion:framePosition="25"
                motion:keyPositionType="deltaRelative"
                motion:motionTarget="@id/view"
                motion:percentX="0.5"
                motion:percentY="0.0" />
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start"></ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="100dp"
                android:layout_height="100dp"
                motion:layout_constraintLeft_toLeftOf="@+id/guideline_vertical_250dp"
                motion:layout_constraintTop_toTopOf="parent" />
        </Constraint>
    </ConstraintSet>
</MotionScene>

可以看到页面底部,progress = 25% 的时候,动画完成度确实完成了一半(也就是右移 125dp)

delta_relative.gif

deltaRelative 的坐标会根据视图在 X 和 Y 方向上移动的距离而增大或缩小。

如果控件在水平/垂直方向上没有移动太远距离,或在该方向上根本没有移动,deltaRelative 就不会在沿该方向创建有用的坐标系。例如一个 view 从左侧水平地移到右侧,deltaRelative 中就不会有有用的 Y 轴。

pathRelative
pathRelative.png

pathRelative 和前两个坐标系差异巨大,在起始位置和结束位置连一条直线,这条直线的距离就是 X 轴,而且这条线的长度就是 1,也就是说 (0,0) 是起始位置,(1,0) 是结束位置。

官方说这个 pathRelative 主要有 3 个作用:

  1. 在部分动画内容播放期间让某个视图加速、减速或停止
  2. 向路径添加细微的弧线
  3. deltaRelative不起作用的情况下添加第二个维度

我们举例来看看第三种情况

同样是一个 view,起始位置在左上角,起始位置往右 250dp 就是最终位置,现在我要打一个位置关键帧,在时间完成度 1/4 的时候,它的位置在起始位置的斜下方,相当于在起始位置右移 250dp 的一半 + 下移 250dp 的一半

pathRelative草图.png

用 deltaRelative ? 可是起始位置和最终位置所构成的坐标系是一维的,它没有 Y 轴,因为最终位置相对于起始位置只是在 X 轴上平移,所以这个位置关键帧如果用 deltaRelative,在垂直方向上无法定位。

pathRelative草图_2.png

使用 pathRelative 则完美解决这个问题

pathRelative草图_3.png
<?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="30000"
        motion:motionInterpolator="linear">
        <OnClick
            motion:clickAction="transitionToEnd|jumpToStart"
            motion:targetId="@id/view" />
        <KeyFrameSet>
            <KeyPosition
                motion:framePosition="25"
                motion:keyPositionType="pathRelative"
                motion:motionTarget="@id/view"
                motion:percentX="0.5"
                motion:percentY="0.5" />
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start"></ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@+id/view">
            <Layout
                android:layout_width="100dp"
                android:layout_height="100dp"
                motion:layout_constraintLeft_toLeftOf="@+id/guideline_vertical_250dp"
                motion:layout_constraintTop_toTopOf="parent" />
        </Constraint>
    </ConstraintSet>
</MotionScene>
pathRelative.gif