查看原文
其他

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

RicardoMJiang 技术最TOP 2022-08-26

作者:RicardoMJiang 链接:https://juejin.cn/post/6908327119055650824

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">

.....
</android.support.constraint.motion.MotionLayout>

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

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

MotionScene

什么是 MotionScene?结合上图 MotionScene 主要由三部分组成:StateSetConstraintSetTransition

实现RecyclerView展开折叠效果,主要用到了 ConstarintSetTransition

首先来看看布局文件

<?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的动画起始位置以及结束位置的约束信息(仅包含少量必要信息,如:widthheightmargin以及位置属性等)。

显而易见,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 我们可以通过MotionLayoutprogress判断当前是在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联动,实现复杂动画等。

读者如有兴趣可阅读下方的参考链接,及本文的所有代码 本文的所有相关代码

MotionLayoutRecyclerView实现:https://github.com/shenzhen2017/MotionLayoutRecyclerView


---END---

推荐阅读:
硬核!关于PDD员工发帖溯源联想到的相关技术与实现
虎牙二面:说说你对 Java “零拷贝”的理解?
全球最大色情网站宣布:封杀特朗普
2021年1月程序员薪资报告已出炉!升了!升了!
Kotlin开发团队惹上麻烦了!
使用 Jetpack DataStore 进行数据存储
Android UI优化全解析
你还在使用 try-catch-finally 关闭资源?不太优雅~
2020年总结!翻过这座山,他们就会听到你的故事!


更文不易,点个“在看”支持一下👇


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存