如何为RecyclerView Item添加MotionLayout,实现滚动无卡顿的进度变化?
为RecyclerView列表项添加同步滚动的MotionLayout并保证流畅性
核心思路
要实现滚动时列表项MotionLayout进度同步变化,核心是监听RecyclerView滚动事件,计算可见item相对于滚动区域的位置比例,将其转化为MotionLayout的进度值(0~1),同时通过优化手段避免频繁重绘和计算导致的卡顿。
实现步骤
1. 定义列表项的MotionLayout布局
首先创建列表项的布局文件(res/layout/item_motion.xml),使用MotionLayout作为根布局,并配置对应的MotionScene来定义过渡状态:
<?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:id="@+id/item_motion_layout" android:layout_width="match_parent" android:layout_height="200dp" app:layoutDescription="@xml/motion_scene_item"> <!-- 示例View:标题和图片 --> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="列表项标题" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintMarginStart="16dp"/> <ImageView android:id="@+id/iv_image" android:layout_width="100dp" android:layout_height="100dp" android:src="@drawable/ic_launcher_background" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintMarginEnd="16dp"/> </androidx.constraintlayout.motion.widget.MotionLayout>
然后创建MotionScene文件(res/xml/motion_scene_item.xml),禁用自动动画(由滚动手动控制进度),定义起始和结束状态的约束:
<?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"> <Transition app:constraintSetStart="@id/state_start" app:constraintSetEnd="@id/state_end" app:duration="0" <!-- 禁用系统自动过渡动画 --> app:motionInterpolator="linear"> </Transition> <ConstraintSet android:id="@+id/state_start"> <Constraint android:id="@id/tv_title"> <layout app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintMarginStart="16dp"/> <property android:alpha="1"/> </Constraint> <Constraint android:id="@id/iv_image"> <layout app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintMarginEnd="16dp"/> <property android:scaleX="1"/> <property android:scaleY="1"/> </Constraint> </ConstraintSet> <ConstraintSet android:id="@+id/state_end"> <Constraint android:id="@id/tv_title"> <layout app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintMarginStart="32dp"/> <property android:alpha="0.5"/> </Constraint> <Constraint android:id="@id/iv_image"> <layout app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintMarginEnd="32dp"/> <property android:scaleX="0.8"/> <property android:scaleY="0.8"/> </Constraint> </ConstraintSet> </MotionScene>
2. 监听RecyclerView滚动,同步MotionLayout进度
在RecyclerView所在的页面(Activity/Fragment)或Adapter中,添加滚动监听器,计算每个可见item的进度并设置给MotionLayout:
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) val layoutManager = recyclerView.layoutManager as LinearLayoutManager val firstVisiblePos = layoutManager.findFirstVisibleItemPosition() val lastVisiblePos = layoutManager.findLastVisibleItemPosition() val recyclerViewVisibleHeight = recyclerView.height // 仅遍历可见item,减少计算量 for (pos in firstVisiblePos..lastVisiblePos) { val viewHolder = recyclerView.findViewHolderForAdapterPosition(pos) as? ItemViewHolder viewHolder?.let { holder -> // 直接使用ViewHolder缓存的引用,避免重复findViewById val progress = (holder.itemView.top.toFloat() / recyclerViewVisibleHeight).coerceIn(0f, 1f) holder.motionLayout.progress = progress } } } })
对应的Adapter ViewHolder示例:
class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { // 缓存MotionLayout引用,提升性能 val motionLayout = itemView.findViewById<MotionLayout>(R.id.item_motion_layout) }
性能优化(避免卡顿)
- 只处理可见item:遍历范围限制在
firstVisiblePos到lastVisiblePos,跳过不可见的item,减少不必要的计算。 - 禁用MotionLayout自动动画:在MotionScene中设置
app:duration="0",避免系统自动播放动画与手动进度控制冲突,减少额外的动画计算。 - 复用ViewHolder并缓存View引用:在ViewHolder中提前缓存MotionLayout等View,避免每次滚动都调用
findViewById。 - 使用轻量动画属性:优先使用
alpha、scale、translationX/Y等可硬件加速的属性,避免使用layout_margin、width/height等会触发布局重排的属性。 - 避免频繁触发重绘:确保MotionLayout的过渡不会导致过度的View重绘,可通过Android Studio的Profile工具监测绘制性能。
- 使用合适的LayoutManager:LinearLayoutManager的计算逻辑更简单,若使用GridLayoutManager,需调整进度计算逻辑(比如考虑列的高度)。
额外注意
- 若需要反向滚动时进度反向,可修改进度计算式为
1f - (holder.itemView.top.toFloat() / recyclerViewVisibleHeight).coerceIn(0f, 1f)。 - 测试不同屏幕尺寸和分辨率,确保进度计算在各种设备上表现一致。
内容的提问来源于stack exchange,提问作者George Samuel
相关产品推荐
相关产品推荐

