Android MotionLayout动画之KeyFrameSet关键帧详解(二)

346 阅读12分钟

MotionLayout系列文章

Android | MotionLayout入门级使用教程(一):juejin.cn/post/736642…

关键帧KeyFrameSet

motionScene

在 MotionScene 中,KeyFrameSet 是一个用于定义动画关键帧的容器,它可以在动画的特定时间点上对属性进行插值或触发事件,从而控制动画的行为和效果。通过关键帧,可以定义精确的动画轨迹和行为,而不是仅仅依赖 MotionLayout 的默认插值。

keyframe

KeyFrameSet 包含多个关键帧类型,每种类型可以控制不同的属性或触发不同的事件。这些关键帧通过动画的时间轴 (framePosition) 定位。

KeyPosition

image.png KeyPosition 是一个关键帧类型,用于定义视图在动画路径上的位置和大小变化。它允许通过百分比指定位置、大小、角度等属性,以便在特定的动画帧上对元素进行更精细的控制。以下是常用属性及其含义:

  • framePosition:定义动画帧的位置,表示在动画中的百分比位置。范围为 0-100,例如 framePosition="50" 表示动画的中点。
  • motionTarget:指定该关键帧所应用的目标视图,通过 ID 进行引用。
  • percentX / percentY:是指在 framePosition 按多大百分比来修改路径(值介于 0.0 到 1.0 之间,允许使用负数值和大于 1 的值)。可以简单理解为:在framePosition,修改motionTarget 的路径,即根据 keyPositionType 确定的坐标移动percentX 或percentY 。
  • keyPositionType:定义关键帧位置的计算方式,keyPositionType 属性用于确定如何根据 percentX 或 percentY 来修改路径。它可以是 parentRelative、pathRelative 或 deltaRelative。

    parentRelative: 相对于父容器的位置来计算关键帧,左上角和右下角分别为(0,0)和(1,1)。 在这里插入图片描述

deltaRelative: 基于相对于起始点和结束点的相对偏移量来计算路径,起始坐标和终点坐标分别为(0,0)和(1,1)。
deltaRelative

pathRelative: 基于运动路径的百分比位置,从起点连接到终点的方向是X轴,Y轴的正方向是X轴顺时针转90度的方向。 pathRelative

  • percentWidth:表示在宽度上的百分比变化量。若宽度无变化,设置此属性无效,可用来实现逐帧增宽的效果。
  • percentHeight:表示在高度上的百分比变化量。同 percentWidth,若高度无变化则无效。
  • sizePercent:定义元素的整体大小百分比变化(宽度和高度比例一致),是 percentWidth 和 percentHeight 的简化形式。
  • transitionEasing:用于定义在关键帧上应用的缓动效果。可以选择 easeIn, easeOut 等内置类型或自定义曲线。
  • pathMotionArc:定义路径的弧度,如 startVertical, startHorizontal 等。用于创建更加平滑的曲线运动。
  • curveFit:用于曲线拟合,指定如何连接关键帧。通常用于处理更复杂的曲线路径,取值有:spline样条插值、linear线性插值。

视频介绍:www.youtube.com/watch?v=3HQ…

KeyAttribute

KeyAttribute 是在特定时间点上动态修改 UI 元素属性的关键帧类型。通过KeyAttribute,可以在动画的某些时刻(关键帧)对元素的多个属性进行变换,例如透明度、旋转、缩放、平移等,从而实现复杂的动画效果。以下是 KeyAttribute 内各字段的含义:

属性详解

1、framePosition:定义了关键帧在动画进度中的位置,以百分比(0 到 100)表示。例如,framePosition=50 表示动画进行到一半时触发该关键帧,framePosition="25"表示该关键帧会在动画的 25% 处生效。

2、motionTarget:指定关键帧的目标视图或组件,通常使用视图 ID 引用。如:motionTarget="@+id/my_view" 用于指定应用此关键帧的视图。

3、transitionEasing:控制该关键帧的动画缓动效果,例如线性或非线性变化。示例:transitionEasing="easeInOut" 表示从平滑启动到渐渐加速,最后减速结束。 4、curveFit:定义了属性在动画中的插值方式,如线性或样条插值。示例:curveFit="spline" 表示使用样条插值。

5、motionProgress:用于控制 MotionLayout 内的嵌套动画进度,范围从 0 到 1。示例:在某个关键帧时,可以通过 motionProgress="0.5" 将进度条设为一半。

除上面的属性之外,还可以设置一些View相关的属性,从而对视图的外观和变换进行调整。

android:alpha:设置透明度。android:alpha="0.5" 表示视图半透明。

android:elevation:控制视图的高度,以增加视图的层次感。示例:android:elevation="10dp" 表示将视图抬升 10dp。

android:rotation、android:rotationX、android:rotationY:分别控制视图的二维和三维旋转角度。示例:android:rotation="45" 将视图顺时针旋转 45 度。

android:transformPivotX、android:transformPivotY:控制旋转和缩放的中心点。示例:android:transformPivotX="50dp" 表示将 X 轴的旋转中心设置在 50dp 处。

transformPivotTarget:指定其他视图作为旋转和缩放的中心点。

transitionPathRotate:视图沿动画路径旋转的角度。示例:transitionPathRotate="30" 将视图沿路径旋转 30 度。

android:scaleX、android:scaleY:分别设置视图的 X 和 Y 轴缩放。示例:android:scaleX="1.5" 将视图宽度放大 1.5 倍。

android:translationX、android:translationY:控制视图的 X、Y 方向偏移。示例:android:translationX="20dp" 表示将视图向右平移 20dp。

android:translationZ:控制视图在 Z 轴上的偏移。

KeyAttribute 提供了灵活的方式去设置动画中的多个关键帧,从而实现复杂且流畅的动画效果。视频介绍:www.youtube.com/watch?v=jUm…

KeyCycle

KeyCycle在动画过程中做周期性波形来实现动画效果。通过 KeyCycle,可以让属性值以波动的方式进行变化(例如像正弦波或方波),用于产生类似于脉冲或振荡的效果。

属性解释

1、motionTarget:指定应用波动效果的目标视图,通常使用视图的 ID。

2、curveFit:插值方式,决定波形曲线的平滑程度。示例:curveFit="spline" 表示使用样条插值。

3、framePosition:定义关键帧在动画进度中的位置,以 0 到 100 的百分比表示。示例:framePosition="50" 表示在动画进度的一半时触发此波动效果。

4、transitionEasing:缓动函数,控制波动动画的速度和流畅度。示例:transitionEasing="easeInOut"。

5、motionProgress:控制 MotionLayout 内嵌套动画的进度,值从 0 到 1。

6、waveShape:定义波形的形状,可选值包括: • sin:正弦波 • square:方波 • triangle:三角波 • sawtooth:锯齿波 • reverseSawtooth:反向锯齿波 • cos:余弦 • bounce:弹跳

7、wavePhase:波的初始相位,单位为度数。它控制波形的起始位置。示例:wavePhase="90" 表示波形从 90 度开始。

8、wavePeriod:波形周期,表示波形重复的频率。值越小,频率越高。示例:wavePeriod="2" 表示波形每 2 秒完成一个周期。

9、waveOffset:波的偏移量,控制波动效果的初始位置。示例:waveOffset="0、2" 表示从 0、2 的位置开始波动。

10、waveVariesBy:定义波动变化的方向,可以是 x, y 或 path。示例:waveVariesBy="x"。

11、 transitionPathRotate:视图沿路径旋转的角度,主要在路径动画中使用。示例:transitionPathRotate="45" 使视图沿路径旋转 45 度。

其他视图属性,这些属性允许在 KeyCycle 中对视图的多个变换进行波动效果的设置,例如透明度、旋转、缩放和位移等。

android:alpha:透明度变化。
android:elevation:视图的高度。
android:rotation、android:rotationX、android:rotationY:控制二维和三维旋转。
android:scaleX、android:scaleY:控制 X 和 Y 轴缩放。
android:translationX、android:translationY:控制 X 和 Y 方向位移。
android:translationZ:控制 Z 轴方向位移。

视频介绍:www.youtube.com/watch?v=qWm…

KeyTimeCycle

KeyTimeCycle 用于实现基于时间的周期性动画。通过该关键帧,可以对视图的属性(如透明度、旋转、位移等)产生随时间波动的效果,常用于制作振荡或周期性的动画。

属性详解

  1. framePosition:动画进度中的位置,以百分比表示(0-100)。确定波动效果的触发时间点。
  2. motionTarget:指定动画目标视图,通过视图 ID 关联。
  3. transitionEasing:控制缓动效果,定义波形的流畅性。
  4. curveFit:曲线拟合方法,用于控制插值的平滑度和效果(如 spline)。
  5. waveShape:波形的形状,定义周期变化的方式。常用的值包括: • sin:正弦波 • square:方波 • triangle:三角波 • sawtooth:锯齿波 • reverseSawtooth:反向锯齿波 • cos:余弦 • bounce:弹跳 波形形状
  6. wavePeriod:波形周期,控制波形的频率,值越大表示频率越高。
  7. motionProgress:控制嵌套动画的进度,范围为 0 到 1。
  8. waveOffset:波的偏移量,用于设置初始波动位置。
  9. wavePhase:波的相位,定义波的起始角度,以度数表示。
  10. waveDecay:波的衰减时间,单位为毫秒。控制波动效果随时间逐渐减弱,直到小于 1/256。

除上述属性之外,还可以对视图属性添加波动效果,包括透明度、缩放、旋转和位移等:

android:alpha:透明度变化。
android:elevation:视图的高度。
android:rotation、android:rotationX、android:rotationY:控制二维和三维旋转。
android:scaleX、android:scaleY:控制 X 和 Y 轴缩放。
android:translationX、android:translationY:控制 X 和 Y 方向位移。
android:translationZ:控制 Z 轴方向位移。

视频介绍:www.youtube.com/watch?v=us0…

KeyTrigger

KeyTrigger 用于在动画到达特定位置时触发事件函数。通过设置触发条件,响应动画进度的某一特定时刻执行特定操作。

  1. framePosition:触发事件的位置,以动画进度的百分比表示。范围是 0 到 100。
  2. motionTarget:指定触发事件的目标视图,通过视图 ID 进行关联。
  3. triggerReceiver:指定接收触发事件的视图,可以是另一个视图的 ID。
  4. onNegativeCross、onPositiveCross、onCross:这些属性指定了事件的回调函数,分别在以下条件下调用: • onNegativeCross:动画经过 framePosition 向负方向(回退)触发。 • onPositiveCross:动画经过 framePosition 向正方向(前进)触发。 • onCross:动画经过 framePosition 时(无论方向)触发。
  5. viewTransitionOnNegativeCross、viewTransitionOnPositiveCross、viewTransitionOnCross:这些属性指定了动画过度操作的 ID,对应 onNegativeCross、onPositiveCross 和 onCross 的触发条件。
  6. triggerSlack:触发的容差,表示触发事件前后的范围,用于避免重复触发。
  7. triggerId:触发事件的唯一 ID,用于在回调中识别触发事件。
  8. motion_postLayoutCollision:布尔值,设置为 true 时将在布局后检测碰撞触发。
  9. motion_triggerOnCollision:指定目标视图的 ID,用于检测 motionTarget 和目标视图的碰撞,发生碰撞时触发事件。

使用示例:

class TriggerText @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : TextView(context, attrs, defStyleAttr) {

    fun forwardText() {
        text = "我要前进"
    }

    fun retreatText() {
        text = "我要后退"
    }
}

自定义了TextView并声明了两个方法用以改变文本,XML布局文件如下:

<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"
    app:layoutDescription="@xml/fragment_cl_img_filter_scene"
    app:showPaths="true">

    <androidx.constraintlayout.utils.widget.ImageFilterView
        android:id="@+id/iv_filter_bottom"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginBottom="30dp"
        android:scaleType="centerCrop"
        android:src="@drawable/icon_flower"
        app:altSrc="@drawable/icon_cat_h"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <org.ninetripods.mq.study.widget.TriggerText
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/red"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="@+id/iv_filter_bottom"
        app:layout_constraintStart_toStartOf="@+id/iv_filter_bottom"
        app:layout_constraintTop_toBottomOf="@+id/iv_filter_bottom" />
</androidx.constraintlayout.motion.widget.MotionLayout>

MotionScene 动画核心代码:

<Transition
    motion:constraintSetEnd="@id/end"
    motion:constraintSetStart="@+id/start"
    motion:duration="1000"
    motion:motionInterpolator="linear">
    <KeyFrameSet>
        <!--在动画特定位置执行事件函数-->
        <KeyTrigger
            motion:framePosition="10"
            motion:motionTarget="@+id/tv_content"
            motion:onPositiveCross="forwardText"
            motion:viewTransitionOnCross="@color/red"
            motion:triggerSlack="10"/>
        <KeyTrigger
            motion:framePosition="90"
            motion:motionTarget="@+id/tv_content"
            motion:onNegativeCross="retreatText" />
    </KeyFrameSet>
    <OnSwipe
        motion:dragDirection="dragRight"
        motion:touchAnchorId="@+id/iv_filter_bottom"
        motion:touchAnchorSide="right" />
</Transition>

执行效果:

trigger

可以看到第一个 KeyTrigger 在动画进行到 10% 时(前进时)触发 forwardText 方法;第二个 KeyTrigger 在动画进行到 90% 时(后退时)触发 retreatText 方法。

KeyTrigger 可用于动态响应动画进度,或当视图达到某个位置时执行特定逻辑,比如播放音效、显示提示或修改视图属性等。相关视频:www.youtube.com/watch?v=Rqf…

示例

官方的一个简单综合示例:

示例

XML代码(layout_motion_moon):

<?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:background="@drawable/background"
    app:layoutDescription="@xml/layout_moon_scene"
    app:showPaths="true">

    <ImageView
        android:id="@+id/moon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_moon" />

    <TextView
        android:id="@+id/credits"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Virginia P @ Google"
        android:textSize="24sp" />

    <TextView
        android:id="@+id/swipe_hint"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Swipe towards the right" />

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

对应的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="3000">

        <OnSwipe
            motion:dragDirection="dragRight"
            motion:touchAnchorId="@id/moon"
            motion:touchAnchorSide="bottom" />

        <KeyFrameSet>
            <KeyPosition
                motion:framePosition="25"
                motion:keyPositionType="parentRelative"
                motion:motionTarget="@id/moon"
                motion:percentY="0.6" />
            <KeyPosition
                motion:framePosition="50"
                motion:keyPositionType="parentRelative"
                motion:motionTarget="@id/moon"
                motion:percentY="0.5" />
            <KeyPosition
                motion:framePosition="75"
                motion:keyPositionType="parentRelative"
                motion:motionTarget="@id/moon"
                motion:percentY="0.6" />

            <KeyAttribute
                android:rotation="-360"
                android:scaleX="2.0"
                android:scaleY="2.0"
                motion:framePosition="50"
                motion:motionTarget="@id/moon" />

            <KeyAttribute
                android:rotation="-720"
                android:scaleX="1.0"
                android:scaleY="1.0"
                motion:framePosition="100"
                motion:motionTarget="@id/moon" />

            <KeyAttribute
                motion:framePosition="0"
                motion:motionTarget="@id/moon">
                <CustomAttribute
                    motion:attributeName="colorFilter"
                    motion:customColorValue="#FFFFFF" />
            </KeyAttribute>

            <KeyAttribute
                motion:framePosition="50"
                motion:motionTarget="@id/moon">
                <CustomAttribute
                    motion:attributeName="colorFilter"
                    motion:customColorValue="@color/yellow" />
            </KeyAttribute>
            <KeyAttribute
                motion:framePosition="100"
                motion:motionTarget="@id/moon">
                <CustomAttribute
                    motion:attributeName="colorFilter"
                    motion:customColorValue="#FFFFFF" />
            </KeyAttribute>

            <KeyAttribute
                android:alpha="0.0"
                motion:framePosition="85"
                motion:motionTarget="@id/credits" />

            <KeyAttribute
                android:alpha="0"
                motion:framePosition="40"
                motion:motionTarget="@id/swipe_hint"
                motion:transitionEasing="accelerate" />
        </KeyFrameSet>

    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/moon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="100dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent" />

        <Constraint
            android:id="@id/credits"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:alpha="0.0"
            motion:layout_constraintBottom_toBottomOf="@id/moon"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="@id/moon" />
        <Constraint
            android:id="@id/swipe_hint"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:visibility="visible"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/moon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="100dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent" />

        <Constraint
            android:id="@id/credits"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:alpha="1.0"
            motion:layout_constraintBottom_toBottomOf="@id/moon"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="@id/moon" />
        <Constraint
            android:id="@id/swipe_hint"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="invisible"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>
</MotionScene>

大致解释下上述代码的功能:

  • ConstraintSet 定义了 start 和 end 状态,描述了动画开始和结束时视图的位置与属性。moon 视图从父布局左下移动到右下,同时改变大小、旋转、颜色等;
  • 定义了 OnSwipe 手势控制,允许用户通过向右滑动 moon 的底部触发动画。
  • 使用 KeyFrameSet 描述了动画过程中多个关键帧的效果,包括:

位置控制 (KeyPosition):通过 Y 轴的百分比,创建了平滑的移动路径。
属性变化 (KeyAttribute):定义旋转角度、缩放比例、透明度等属性。
自定义属性 (CustomAttribute):改变视图的颜色滤镜。

资料

1、视频介绍:www.youtube.com/results?sea…
2、CodeLabs:codelabs.developers.google.com/codelabs/mo…