Android 通过MotionLayot实现点赞动画

语言: CN / TW / HK

在之前的文章Android 一种点赞动画的实现中,通过AnimationAnimator实现了一种点赞动画效果,有知友评论用MotionLayout是否会比较简单。我之前还没有使用过MotionLayout,刚好通过实现这个点赞动画来学习一下MotionLayout的使用。

MotionLayout

MotionLayoutConstraintLayout的子类,包含在ConstraintLayout库中,在ConstraintLayout的基础上,增加了管理控件动画的功能。

官方文档

添加库

如果之前没有使用ConstraintLayout,那么需要在app module下的build.gradle中添加代码,如下:

``` dependencies { // 项目中使用AndroidX implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1'

// 项目中未使用AndroidX
implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta1'

} ```

点赞效果的实现

尝试使用MotionLayout来实现之前的点赞动画,最终实现了缩放以及发散效果。

布局中添加MotionLayout

```

<!--根节点改为使用MotionLayout-->
<!--layoutDescription 配置MotionScene配置文件-->
<!--showPaths设置是否显示动画的轨迹-->
<androidx.constraintlayout.motion.widget.MotionLayout
    android:id="@+id/motion_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    motion:layoutDescription="@xml/example_motion_scene"
    tools:showPaths="true">

    <include
        android:id="@+id/include_title"
        layout="@layout/layout_title" />

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/iv_thumb_up"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        android:src="@drawable/icon_thumb_up"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/iv_thumb_up1"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        android:src="@drawable/icon_thumb_up"
        android:visibility="gone"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/iv_thumb_up2"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        android:src="@drawable/icon_thumb_up"
        android:visibility="gone"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/iv_thumb_up3"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        android:src="@drawable/icon_thumb_up"
        android:visibility="gone"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/iv_thumb_up4"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        android:src="@drawable/icon_thumb_up"
        android:visibility="gone"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/iv_thumb_up5"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        android:src="@drawable/icon_thumb_up"
        android:visibility="gone"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>

```

创建MotionScene配置文件

```

<!--配置动画的属性-->
<!--duration 配置动画的持续时间-->
<!--constraintSetStart 配置动画开始时,控件集的状态-->
<!--constraintSetEnd 配置动画结束时,控件集的状态-->
<!--motionInterpolator 配置动画的插值器,-->
<Transition
    android:id="@+id/transition_thumb"
    android:duration="1500"
    motion:constraintSetEnd="@id/thumb_end"
    motion:constraintSetStart="@id/thumb_start"
    motion:motionInterpolator="linear">

    <!--点击时触发动画-->
    <!--targetId 配置触发事件的控件id-->
    <!--clickAction 配置点击触发的效果-->
    <!--clickAction toggle 当前控件集为开始状态,则播放动画切换至结束状态,反之亦然-->
    <!--clickAction transitionToEnd 播放控件集开始到结束的动画-->
    <!--clickAction transitionToStart 播放控件集结束到开始的动画-->
    <!--clickAction jumpToEnd 不播放动画,控件集直接切换至结束状态-->
    <!--clickAction jumpToStart 不播放动画,控件集直接切换至开始状态-->
    <OnClick
        motion:clickAction="transitionToEnd"
        motion:targetId="@id/iv_thumb_up" />

    <!--关键帧集合,用于实现缩放效果-->
    <KeyFrameSet>

        <!--修改属性-->
        <!--framePosition 取值范围为0-100-->
        <!--motionTarget 设置修改的对象-->
        <!--scaleX 设置x轴缩放大小-->
        <!--scaleY 设置y轴缩放大小-->
        <KeyAttribute
            android:scaleX="1.6"
            android:scaleY="1.6"
            motion:framePosition="25"
            motion:motionTarget="@id/iv_thumb_up" />

        <KeyAttribute
            android:scaleX="1"
            android:scaleY="1"
            motion:framePosition="50"
            motion:motionTarget="@id/iv_thumb_up" />
    </KeyFrameSet>
</Transition>

<!--控件集 动画开始时的状态-->
<ConstraintSet android:id="@+id/thumb_start">

    <!--与layout文件中的控件对应-->
    <!--visibilityMode 如果需要改变控件的可见性,需要将此字段配置为ignore-->
    <Constraint
        android:id="@+id/iv_thumb_up"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore" />

    <Constraint
        android:id="@+id/iv_thumb_up1"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore">

        <!--改变控件的属性-->
        <!--attributeName 属性名-->
        <!--customFloatValue Float类型属性值-->
        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="1" />
    </Constraint>

    <Constraint
        android:id="@+id/iv_thumb_up2"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore">

        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="1" />
    </Constraint>

    <Constraint
        android:id="@+id/iv_thumb_up3"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore">

        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="1" />
    </Constraint>

    <Constraint
        android:id="@+id/iv_thumb_up4"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore">

        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="1" />
    </Constraint>

    <Constraint
        android:id="@+id/iv_thumb_up5"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore">

        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="1" />
    </Constraint>
</ConstraintSet>

<!--控件集 动画结束时的状态-->
<ConstraintSet android:id="@+id/thumb_end">

    <Constraint
        android:id="@+id/iv_thumb_up"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="2dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore" />

    <Constraint
        android:id="@+id/iv_thumb_up1"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginTop="110dp"
        android:layout_marginEnd="90dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore">

        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="0.5" />
    </Constraint>

    <Constraint
        android:id="@+id/iv_thumb_up2"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginStart="70dp"
        android:layout_marginTop="95dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore">

        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="0.4" />
    </Constraint>

    <Constraint
        android:id="@+id/iv_thumb_up3"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginEnd="85dp"
        android:layout_marginBottom="140dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore">

        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="0.6" />
    </Constraint>

    <Constraint
        android:id="@+id/iv_thumb_up4"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginStart="60dp"
        android:layout_marginBottom="120dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore">

        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="0.2" />
    </Constraint>

    <Constraint
        android:id="@+id/iv_thumb_up5"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginEnd="20dp"
        android:layout_marginBottom="60dp"
        motion:layout_constraintBottom_toBottomOf="parent"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent"
        motion:visibilityMode="ignore">

        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="0" />
    </Constraint>
</ConstraintSet>

```

监听动画状态

``` class MotionLayoutExampleActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding: LayoutMotionLayoutExampleActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_motion_layout_example_activity)
    binding.motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
        override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {
            // 动画开始
            // 把发散的按钮显示出来
            binding.ivThumbUp1.visibility = View.VISIBLE
            binding.ivThumbUp2.visibility = View.VISIBLE
            binding.ivThumbUp3.visibility = View.VISIBLE
            binding.ivThumbUp4.visibility = View.VISIBLE
            binding.ivThumbUp5.visibility = View.VISIBLE
        }

        override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {
            // 动画进行中
        }

        override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
            // 动画完成
            // 隐藏发散的按钮,将状态还原
            binding.root.postDelayed({
                binding.ivThumbUp1.visibility = View.GONE
                binding.ivThumbUp2.visibility = View.GONE
                binding.ivThumbUp3.visibility = View.GONE
                binding.ivThumbUp4.visibility = View.GONE
                binding.ivThumbUp5.visibility = View.GONE
                binding.motionLayout.progress = 0f
            }, 200)
        }

        override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {

        }
    })
}

} ```

示例

已整合到demo中。

ExampleDemo github

ExampleDemo gitee

效果如图:

device-2023-02-04-103100.gif

大致还原了之前的动画效果,MotionLayout实现起来确实不复杂,但是目前还没有找到如何设置动画开始前的延时,因此点击完之后按钮的缩放效果与发散效果之间的间隔、发散出去的按钮之间的间隔无法完全复原。