利用MotionLayout实现RecyclerView折叠展开动画!
作者: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
主要由三部分组成: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
联动,实现复杂动画等。
读者如有兴趣可阅读下方的参考链接,及本文的所有代码 本文的所有相关代码
MotionLayoutRecyclerView实现:https://github.com/shenzhen2017/MotionLayoutRecyclerView
---END---
更文不易,点个“在看”支持一下👇