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.set(0, 0, 0, 0); // mTempRect 默认上下左右都是 0
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState); // 通过 getItemOffsets() 给 mTempRect 赋值
        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,如下图所示,itemView 是绿色的区域,outRect 是红色的区域:

如果还有第二个 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 的上方的原因。

相关推荐
luoluoal2 小时前
Java项目之基于ssm的个性化旅游攻略定制系统(源码+文档)
java·mysql·mybatis·ssm·源码
淘源码d17 小时前
如何运用C#.NET快速开发一套掌上医院系统?
开发语言·c#·.net·源码·掌上医院
工业互联网专业1 天前
基于springboot+vue的二手车交易系统
java·vue.js·spring boot·毕业设计·源码·课程设计·二手车交易系统
EnzoRay1 天前
RecyclerView——ItemTouchHelper
源码
luoluoal1 天前
Java项目之基于ssm的怀旧唱片售卖系统(源码+文档)
java·mysql·mybatis·ssm·源码
大叔_爱编程1 天前
wx206基于ssm+vue+uniapp的优购电商小程序
vue.js·小程序·uni-app·毕业设计·ssm·源码·课程设计
congregalis2 天前
跳出框架:一步步实现简易Deep Search Agent
人工智能·程序员·源码
Jinuss3 天前
源码分析之Leaflet图层控制控件Control.Layers实现原理
源码·leaflet
7675604794 天前
useDateFormat源码解析
前端·源码