RecyclerView源码剖析: ItemDecoration的绘制

880 阅读3分钟

本文是RecyclerView源码分析系列的第三篇,这篇文章我们来分析ItemDecoration。

本文结合RecyclerView源码剖析: 基本显示一起看,效果更好。

添加ItemDecoration

public void addItemDecoration(@NonNull ItemDecoration decor) {
    addItemDecoration(decor, -1);
}

public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
    // ...
    
    // 1.保存ItemDecoration  
    if (index < 0) {
        mItemDecorations.add(decor);
    } else {
        mItemDecorations.add(index, decor);
    }
    // 2.标记ItemDecoration区域为dirty
    markItemDecorInsetsDirty();
    // 3.请求重新布局
    requestLayout();
}

RecyclerView首先用ArrayList mItemDecorations保存ItemDecoation,然后标记ItemDecoraction的区域需要刷新,最后请求重新布局。

测量

这里的测量不是指的ItemDecoration的测量,而是指的是带有ItemDecoration的子View测量。因为RecyclerView需要知道子View的ItemDecoration占据的空间,从而来测量子View。

ItemDecoration的测量是由LayoutManager#measureChildWithMargins()LayoutManager#measureChild()实现的,这两个方法非常类似,差别只在于是否考虑margin

我们以LayoutManager#measureChildWithMargin()为例分析

public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    // 1. 调用ItemDecoration#getItemOffsets()获取ItemDecoration的Rect
    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    // 计算ItemDecoration已经使用的宽高
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;
    // 2. 获取子View测量MeasureSpec
    // 第二个参数指的是已使用的宽高,其中包括margin,ItemDecoration使用的宽高
    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());
    // 3. 测量子View
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}

首先调用getItemDecorInsetsForChild()来获取子View的ItemDecoration区域

    Rect getItemDecorInsetsForChild(View child) {
        // ...
        
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        // 遍历所有的ItemDecoration
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            // 获取每一个ItemDecoration的绘制区域
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            // 累加每一个ItemDecoration所占据的区域
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }

原来是调用每一个ItemDecoration#getItemOffsets()方法获取ItemDecoration需要绘制的区域,然后累加每一个ItemDecorationleft, top, right, bottom值,作为所有ItemDecorationRect

获取了所有ItemDecorationRect后,测量子View就考虑了RecyclerViewpadding,子View的margin,以及子View的ItemDecoration所占用的宽高。

这里用一副图来解释这里的测量逻辑

ItemDecoration

布局

这里说的布局也不是指ItemDecoration的布局,而是指带有ItemDeocation的子View的布局,它是由LayoutManager#layoutDecoratedWithMargins实现的。

public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
                int bottom) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    // 获取ItemDecoration的Rect
    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);
}

这里就是按照上面那副图进行布局的,大家对照这看看就明白了。

测量和布局都需要大家有自定义View的测量和布局的基本功。

绘制

这次就真的是ItemDecoration的绘制

    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);
        }
        
        // over scroll绘制 ...
    }
    
    public void onDraw(Canvas c) {
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            // 按顺序逐个绘制
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }    

这里需要你有一点点自定义View绘制的基本功,绘制顺序如下

  1. 调用ItemDecoration#onDraw()绘制。
  2. 绘制子View。
  3. 调用ItemDecoration#onDrawOver()绘制。

这里我们需要注意一点,由于这里是按照添加ItemDecoration的顺序进行遍历绘制,因此我们在添加多个ItemDecoration的时候,要注意添加的顺序,否则可能造成显示错乱。

思考

我们再来思考一个问题,在测量,布局和绘制的过程中,似乎没有定义marginItemDecoration的顺序,那么到底是ItemDecoration靠近子View,还是margin靠近子View?从ItemDecoration的命名可以猜测,它应该是靠近子View,而margin应该在外侧。但是我们不一定要遵守这种默认的规则,既然RecyclerView没有强制定义顺序,那么就留给我们更大的灵活性,因此可以根据实际情况进行灵活调整。

结束

ItemDecoration的原理比较简单,但是它在实际中的应用却是很广泛的,在下一篇文章中,我们将使用ItemDecoration来实现一个类似微信联系人的效果。

如果你觉得我的文章写的还行,可以点个赞以及关注我。