8、RecyclerView进阶:LayoutManager、Adapter、ViewHolder、ItemDecoration、点击事件处理

RecyclerView 这东西,说实话,是 Android 开发里绕不开的一个坎。基础用法大家都会,但真要把它玩明白,还是得花点功夫。今天我就把 RecyclerView 进阶的几个核心点掰开揉碎了讲一讲。

我个人习惯把 RecyclerView 比作一个「乐高积木系统」。它本身只负责「回收」和「复用」,真正决定长什么样的,是 LayoutManager、Adapter、ViewHolder 这些零件。你想想看,是不是这个理?

核心要点:RecyclerView 的灵活性来自于它的组件化设计。每个组件各司其职,组合起来就能应对各种复杂场景。

8.1 LayoutManager:布局的指挥官

LayoutManager 决定了 item 在屏幕上怎么排列。系统提供了三个现成的:LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager。但说实话,实际项目中经常需要自定义。

我在项目中遇到过这样一个需求:做一个类似「时间轴」的列表,左边是时间线,右边是内容。用 LinearLayoutManager 显然不行,因为每个 item 的左边都要对齐时间线。最后我写了个自定义 LayoutManager,重写了 onLayoutChildren 方法,手动计算每个 item 的位置。

class TimelineLayoutManager : RecyclerView.LayoutManager() {
    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
        return RecyclerView.LayoutParams(
            RecyclerView.LayoutParams.MATCH_PARENT,
            RecyclerView.LayoutParams.WRAP_CONTENT
        )
    }

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
        detachAndScrapAttachedViews(recycler)
        val parentWidth = width
        val parentHeight = height
        var top = paddingTop

        for (i in 0 until itemCount) {
            val view = recycler.getViewForPosition(i)
            addView(view)
            measureChildWithMargins(view, 0, 0)

            val left = paddingLeft
            val right = left + getDecoratedMeasuredWidth(view)
            val bottom = top + getDecoratedMeasuredHeight(view)

            layoutDecoratedWithMargins(view, left, top, right, bottom)
            top = bottom
        }
    }
}

嗯,这里要注意:自定义 LayoutManager 时,一定要处理好 detachAndScrapAttachedViewsrecycler.getViewForPosition 的配合。否则很容易出现 item 显示错乱的问题。

小技巧:如果你只是想让列表「横着走」,直接用 LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) 就行,别自己造轮子。

8.2 Adapter 与 ViewHolder:数据与视图的桥梁

Adapter 负责把数据变成 View,ViewHolder 负责缓存这些 View。说白了,Adapter 是「生产车间」,ViewHolder 是「仓库」。

我建议你把 ViewHolder 写得「胖」一点。什么意思?就是把 findViewById、setText、setOnClickListener 这些操作都封装在 ViewHolder 里。Adapter 只负责传数据,不负责 UI 操作。

class MyAdapter(private val dataList: List<String>) : 
    RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_layout, parent, false)
        return MyViewHolder(view)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bind(dataList[position])
    }

    override fun getItemCount(): Int = dataList.size

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val textView: TextView = itemView.findViewById(R.id.text_view)

        fun bind(text: String) {
            textView.text = text
        }
    }
}

你看,Adapter 里干干净净,所有 UI 逻辑都在 ViewHolder 里。这样后期改样式,只需要改 ViewHolder,不用动 Adapter。

注意:不要在 onBindViewHolder 里做耗时操作。这个方法会被频繁调用,任何卡顿都会直接体现在列表滑动上。我曾经就因为在这里加载图片,导致列表卡成 PPT。

8.3 ItemDecoration:给列表加点「装饰」

ItemDecoration 是个好东西,但很多人只用它来画分割线。其实它能做的事情远不止这些。

我举个例子:你想在列表的每个 item 之间加一个「间距」,很多人会直接在 item 布局里写 margin。但这样有个问题——第一个 item 的顶部也会多出 margin。用 ItemDecoration 就能完美解决。

class SpaceItemDecoration(private val space: Int) : RecyclerView.ItemDecoration() {
    override fun getItemOffsets(outRect: Rect, view: View, 
        parent: RecyclerView, state: RecyclerView.State) {
        // 第一个 item 不添加顶部间距
        if (parent.getChildAdapterPosition(view) != 0) {
            outRect.top = space
        }
    }
}

除了间距,你还可以用它画「分组标题」、「水印」、「选中高亮」等等。说白了,只要跟 item 位置相关的绘制,都可以交给 ItemDecoration。

提示:ItemDecoration 的 onDraw 方法是在 item 内容之下绘制的,onDrawOver 是在 item 内容之上绘制的。想清楚你要的效果,再决定用哪个。

8.4 点击事件处理:让列表「活」起来

RecyclerView 没有提供现成的点击事件接口,需要自己实现。常见的做法有两种:

  1. 在 ViewHolder 里设置点击监听——简单直接,适合 item 内部有多个可点击控件的情况。
  2. 在 Adapter 里设置回调接口——解耦性好,适合把点击事件传递给 Activity 或 Fragment。

我个人更推荐第二种。为什么呢?因为第一种方式会让 ViewHolder 持有 Activity 的引用,容易造成内存泄漏。

class MyAdapter(
    private val dataList: List<String>,
    private val onItemClick: (position: Int) -> Unit
) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bind(dataList[position])
        holder.itemView.setOnClickListener {
            onItemClick(position)
        }
    }
}

这样写,点击事件的处理逻辑完全由调用方决定,Adapter 只负责「通知」。你想想看,是不是更灵活?

避坑指南:我曾经在 onBindViewHolder 里直接 new 了一个 OnClickListener,结果列表滑动时频繁创建对象,导致 GC 频繁触发。后来改成在 ViewHolder 里统一设置监听,问题就解决了。

8.5 知识体系总览

下面这张图是我自己整理的 RecyclerView 进阶知识体系,你可以对照着看看自己掌握了多少。

RecyclerView 进阶 LayoutManager LinearLayoutManager GridLayoutManager StaggeredGridLayoutManager 自定义 LayoutManager Adapter & ViewHolder onCreateViewHolder onBindViewHolder getItemCount ViewHolder 封装 UI 逻辑 ItemDecoration getItemOffsets onDraw onDrawOver 分割线 / 间距 / 水印 点击事件处理 ViewHolder 内设置监听 Adapter 回调接口 避免内存泄漏 性能优化 DiffUtil 局部刷新 setHasFixedSize 预加载 / 异步加载

这张图把 RecyclerView 进阶的五个核心模块都串起来了。你仔细看看,每个模块之间其实是有联系的。比如 ItemDecoration 的绘制依赖于 LayoutManager 的布局结果,点击事件的处理又依赖于 ViewHolder 的绑定时机。

好了,关于 RecyclerView 进阶的内容就讲到这里。这些知识点我在实际项目中反复用过,每一个都是踩过坑才总结出来的。希望你能少走一些弯路。