You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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)
}

性能优化(避免卡顿)

  1. 只处理可见item:遍历范围限制在firstVisiblePos到lastVisiblePos,跳过不可见的item,减少不必要的计算。
  2. 禁用MotionLayout自动动画:在MotionScene中设置app:duration="0",避免系统自动播放动画与手动进度控制冲突,减少额外的动画计算。
  3. 复用ViewHolder并缓存View引用:在ViewHolder中提前缓存MotionLayout等View,避免每次滚动都调用findViewById。
  4. 使用轻量动画属性:优先使用alpha、scale、translationX/Y等可硬件加速的属性,避免使用layout_margin、width/height等会触发布局重排的属性。
  5. 避免频繁触发重绘:确保MotionLayout的过渡不会导致过度的View重绘,可通过Android Studio的Profile工具监测绘制性能。
  6. 使用合适的LayoutManager:LinearLayoutManager的计算逻辑更简单,若使用GridLayoutManager,需调整进度计算逻辑(比如考虑列的高度)。

额外注意

  • 若需要反向滚动时进度反向,可修改进度计算式为1f - (holder.itemView.top.toFloat() / recyclerViewVisibleHeight).coerceIn(0f, 1f)。
  • 测试不同屏幕尺寸和分辨率,确保进度计算在各种设备上表现一致。

内容的提问来源于stack exchange,提问作者George Samuel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 13:20:19