利用MotionLayout实现RecyclerView折叠展开动画

RecyclerView的展开与折叠是一种常见的动画
主要有两种方式可以实现
1.通过添加与移除元素
notifyInsert,notifyRemoved,这种方式涉及到元素的加减,动画效果不太流畅
2.通过给RecyclerView的item添加动画
这种情况需要考虑一个item添加动画时,对其他的item的影响。而利用MotionLayout可以方便的实现这一点。

先来看看效果

 

1.支持流畅的展开折叠
2.支持多类型item
3.支持只能同时展开一个

下面来看下具体实现

引入MotionLayout库

dependencies { implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta2' } 

在布局文件中使用

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:id="@+id/motionContainer"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white"
    app:layoutDescription="@xml/motion_list_rv_item_scene">
.....
</androidx.constraintlayout.motion.widget.MotionLayout>

由于 MotionLayout 作为 ConstraintLayout 的子类,那么就自然而然地可以像 ConstraintLayout 那样使用去“约束”子视图了,不过这可就有点“大材小用了”,MotionLayout 的用处可远不止这些。我们先来看看 MotionLayout 的构成:

由上图可知,MotionLayout 可分为 和 两个部分。 部分可简单理解为一个 ConstraintLayout,至于 其实就是我们的“动画层”了。MotionLayout 为我们提供了 layoutDescription 属性,我们需要为它传入一个 MotionScene 包裹的 XML 文件,想要实现动画交互,就必须通过这个“媒介”来连接。

MotionScene

什么是 MotionScene?结合上图 MotionScene 主要由三部分组成:StateSet、ConstraintSet 和 Transition
实现RecyclerView展开折叠效果,主要用到了 ConstarintSet 和 Transition

首先来看看布局文件

<?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:id="@+id/motionContainer"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white"
    app:layoutDescription="@xml/motion_list_rv_item_scene">

    <LinearLayout
        android:id="@+id/box_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="86dp">
            ....
            </LinearLayout>
        </LinearLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="@color/blue_magic" />
    </LinearLayout>

    <View
        android:id="@+id/view2"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_below="@id/box_content"
        android:background="#eaeaef" />
</androidx.constraintlayout.motion.widget.MotionLayout>

布局文件很简单,只不过你可能会注意到,我们对 LinearLayout并没有添加任何约束,原因在于:我们会在 MotionScene 中声明 ConstraintSet,里面将包含该 LinearLayout 的“运动”起始点和终点的约束信息。

当然你也可以在布局文件中对其加以约束,但 MotionScene 中对于控件约束的优先级会高于布局文件中的设定。这里我们通过 layoutDescription 来为 MotionLayout 设置它的 MotionScene 为 motion_list_rv_item_scene,接下来就让我们一睹 MotionScene 的芳容:

动画文件

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@id/box_content"
            android:layout_width="0dp"
            android:layout_height="86dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@id/box_content"
            android:layout_width="0dp"
            android:layout_height="186dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

    <Transition
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@+id/start"
        app:duration="500"
        app:motionInterpolator="easeInOut">
    </Transition>
</MotionScene>

首先,可以发现我们定义了两个 分别描述了RecyclerView中的item的动画起始位置以及结束位置的约束信息(仅包含少量必要信息,如:width、height、margin以及位置属性等)。
显而易见,itemView起始高度为86dp,结束高度186dp.

那么问题来了,如何让它动起来呢?
这就要依靠我们的 元素了。

事实上,我们都知道,动画都是有开始位置和结束位置的,而 MotionLayout 正是利用这一客观事实,将首尾位置和动画过程分离,两个点位置和距离虽然是固定的,但是它们之间的 Path 是无限的,可以是“一马平川”,也可以是"蜿蜒曲折"的。

我们只需要为 Transition 设置起始位置和结束位置的 ConstraintSet 并设置动画时间即可,剩下的都交给 MotionLayout 自动去帮我们完成。

当然你也可以通过 onClick 点击事件来触发动画,绑定目标控件的 id 以及通过 clickAction 属性来设置点击事件的类型。

OnClick有多种类型

  • 1.toggle,如果布局当前处于开始状态,请将动画效果切换为结束状态;否则,请将动画效果切换为开始状态。
  • 2.transitionToStart,为从当前布局到 元素的 motion::constraintSetStart 属性指定的布局添加动画效果。
  • 3.transitionToEnd,为从当前布局到 元素的 motion:constraintSetEnd 属性指定的布局添加动画效果。

只能同时展开一个item实现

因为我们需要在展开一个item时,折叠其他item,因此不在xml中指定点击事件,去adapter中指定
实现展开一个时折叠其他item 我们可以通过MotionLayout的progress判断当前是在start状态还是end状态。

下面的代码主要有几点需要注意的
1.如果是start状态则展开,否则则折叠
2.利用payload局部刷新达到折叠其他itemView的效果。
3.在RecyclerView滚动时会复用,所以需要在onBindViewHolder时初始化item的状态,即progress,不然会发生错位现象

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is MotionViewHolder) {
            val motionBox = holder.itemView.findViewById<MotionLayout>(R.id.motionContainer)
            if (expandList[position]){
                motionBox.progress = 1.0f
            }else{
                motionBox.progress = 0f
            }

            holder.itemView.setOnClickListener {
                expandList.fill(false)
                if (motionBox.progress == 1.0f) {
                    motionBox.transitionToStart()
                } else if (motionBox.progress == 0.0f) {
                    motionBox.transitionToEnd()
                    expandList[position] = true
                }
                for (i in 0 until itemCount) {
                    if (i != position) {
                        notifyItemChanged(i, "collapse")
                    }
                }
            }  
        }
    }

    override fun onBindViewHolder(
        holder: RecyclerView.ViewHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {
        if (payloads.isNullOrEmpty()) {
            super.onBindViewHolder(holder, position, payloads)
        } else {
            if (holder is MotionViewHolder) {
                val motionBox = holder.itemView.findViewById<MotionLayout>(R.id.motionContainer)
                motionBox.transitionToStart()
            }
        }
    }

总结

通过以上步骤,即利用MotionLayout比较简单的实现了RecyclerView的item展开折叠效果
1.支持流畅的展开折叠
2.支持多类型item
3.支持只能同时展开一个

MotionLayout还有很多更强大的功能,比如与AppBarLayout联动,与Lottie联动,实现复杂动画等。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
RecyclerViewAndroid中常用的列表控件,用于展示大量数据。要实现RecyclerView的item动画展开折叠效果,可以通过自定义动画和布局参数的方式来实现。 首先,需要为RecyclerView的item布局添加动画效果。可以使用属性动画或者动画资源文件来实现。例如可以使用属性动画将item的宽度从0变为原始宽度,实现展开效果;或者使用动画资源文件定义透明度的变化,实现淡入淡出效果。这样,在item被添加到RecyclerView中时,会播放动画效果,实现展开折叠的效果。 其次,为了在item展开时使其占用更多的空间,需要设置item的布局参数。可以通过修改item的宽度、高度、权重等属性,来实现展开折叠的效果。例如,在展开时,可以将item的宽度设置为match_parent,高度设为固定值或者根据内容自适应的值,从而让item占据更多的空间,显示更多的内容;在折叠时,可以将item的宽度设置为wrap_content,高度设置为固定值或者较小的值,从而让item占据较少的空间,显示较少的内容。 最后,需要在RecyclerView的适配器中编写代码来控制item的展开折叠。可以使用标志位或者数据集合的状态来记录item的展开折叠状态,在适配器的ViewHolder中根据状态来设置item的动画和布局参数。例如,在item展开时,可以将展开状态设置为true,并为item添加展开动画和修改布局参数的代码;在item折叠时,将展开状态设置为false,并为item添加折叠动画和恢复原始布局参数的代码。 总之,要实现RecyclerView的item动画展开折叠效果,需要为item添加动画效果,修改item的布局参数,并在适配器中控制item的展开折叠状态以及动画和布局参数的设置。这样就可以实现一个具有动画效果的可展开折叠RecyclerView列表。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

金戈鐡馬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值