RecyclerView—ItemDecoration

ItemDecoration 最常用的功能是给 RecyclerView 的 child 之间添加间隔,代码如下:

kotlin 复制代码
// 自定义 ItemDecoration
class CustomItemDecoration : RecyclerView.ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        // 配置 outRect 的 bottom 为 5dp
        outRect.bottom = ScreenUtils.dip2px(5).toInt()
    }
}
kotlin 复制代码
// 调用 RecyclerView 的 addItemDecoration() 添加自定义的 ItemDecoration
recyclerView.addItemDecoration(CustomItemDecoration())

这样就在纵向的 RecyclerView 的每个 child 之间添加了 5dp 的间隔,用起来很简单,但是具体是怎么实现的呢?我们通过源码来分析,本文源码基于 androidx.recyclerview:recyclerview:1.2.1。

RecyclerView 测量 child 的宽高的时候会调用 LayoutManager 的 measureChildWithMargins() 方法:

java 复制代码
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    widthUsed += insets.left + insets.right; // 把 insets 的 left 和 right 添加给 widthUsed
    heightUsed += insets.top + insets.bottom; // 把 insets 的 top 和 bottom 添加给 heightUsed

    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
            getPaddingLeft() + getPaddingRight()
                    + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
            canScrollHorizontally());
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
            getPaddingTop() + getPaddingBottom()
                    + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
            canScrollVertically());
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}

可以看到测量的时候不只会考虑 RecyclerView 的 padding 和 child 的 margin,还会把 insets 的上下左右的值计算在内,insets 来自 RecyclerView 的 getItemDecorInsetsForChild() 方法:

java 复制代码
Rect getItemDecorInsetsForChild(View child) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    ...
    final Rect insets = lp.mDecorInsets;
    final int decorCount = mItemDecorations.size();
    insets.set(0, 0, 0, 0);
    for (int i = 0; i < decorCount; i++) {
        // mTempRect 默认上下左右都是 0
        mTempRect.set(0, 0, 0, 0); 
        // 通过 getItemOffsets() 给 mTempRect 赋值
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState); 
        insets.left += mTempRect.left;
        insets.top += mTempRect.top;
        insets.right += mTempRect.right;
        insets.bottom += mTempRect.bottom;
    }
    ...
    return insets;
}

在这里会遍历 mItemDecorations 并把每个 ItemDecoration 的 mTempRect 的上下左右的值累加给 insets,mItemDecorations 是通过 RecyclerView 的 addItemDecoration() 添加的,insets 来自 child 的 LayoutParams。mTempRect 的上下左右可以通过 ItemDecoration 的 getItemOffsets() 来进行赋值,其默认上下左右的值都是 0。

所以其实 outRect 是 itemView 外面的一个框,类似一个相框,把 itemView 围了起来,其实就类似于给当前 itemView 外面添加了 margin,如下图所示:

如果还有第二个 ItemDecoration,就在现在 outRect 外面再围一个框,依此类推。

再来看看 RecyclerView 是怎么布局 child 的,在 LayoutManager 的 layoutDecoratedWithMargins() 方法中:

java 复制代码
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
                                       int bottom) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Rect insets = lp.mDecorInsets;
    child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
            right - insets.right - lp.rightMargin,
            bottom - insets.bottom - lp.bottomMargin);
}

可以看到布局 child 的时候也会把 insets 的 上下左右考虑进去。

ItemDecoration 是 RecyclerView 的抽象静态内部类,除了 getItemOffsets() 方法,里面还有 2 个方法需要关注:

  • onDraw(),用于在提供给 RecyclerView 的画布上绘制装饰,通过此方法绘制的任何内容都将在 itemView 之前绘制,因此将显示在 itemView 下方。
  • onDrawOver(),用于在提供给 RecyclerView 的画布上绘制装饰,通过此方法绘制的任何内容都将在 itemView 之后绘制,因此将显示在 itemView 上方。

这两个方法会在 RecyclerView 的绘制过程中调用:

java 复制代码
@Override
public void draw(Canvas c) {
    super.draw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    ...
}

@Override
public void onDraw(Canvas c) {
    super.onDraw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

这里 draw() 方法会先调用 super.draw(c),super.draw(c) 即 View 中的 draw() 方法,这里会依次调用 onDraw() 和 dispatchDraw() 方法,dispatchDraw() 方法用于绘制 child,这就是 onDraw() 方法绘制的内容显示在 itemView 的下方,而 onDrawOver() 绘制的的内容显示在 itemView 的上方的原因。

相关推荐
爱笑的源码基地20 小时前
小微企业ERP源码,采用SpringBoot+Vue+ElementUI+UniAPP技术架构,支持二次开发及商用授权
java·源码·二次开发·erp·源代码·mrp生产计划
不简说3 天前
前端可视化打印设计器sv-print,一口气更新了30版
前端·源码·产品
坐吃山猪3 天前
【Nanobot】README04_LEVEL2 提供商系统设计
python·源码·agent·nanobot
坐吃山猪3 天前
【Nanobot】README09_LEVEL4 添加新聊天渠道
开发语言·网络·python·源码·nanobot
坐吃山猪3 天前
【Nanobot】README03_LEVEL2_工具系统架构
python·源码·agent·nanobot
深入云栈5 天前
第一篇:Nacos 2.x 架构全览——为什么从HTTP转向gRPC?
源码
爱笑的源码基地5 天前
拿来即用:基于Spring Cloud+UniApp的智慧工地源码,架构清晰易扩展
java·云计算·源码·智慧工地·程序·开箱即用·数字工地
冬奇Lab5 天前
RAG 系列(十七):Agentic RAG——让 Agent 主导检索过程
人工智能·llm·源码
谙弆悕博士5 天前
【附C++源码】从零开始实现 2048 游戏
java·c++·游戏·源码·项目实战·2048
幽络源小助理6 天前
最新轻量美化表白墙系统源码v2.0_带后台版_附搭建教程
前端·开源·源码·php源码