源码分析 - RecyclerView 分割线的工作流程

RecyclerView 分割线的工作流程源码分析

RecyclerView 没有默认的分割线,需要自己定义,开发者可以根据自己想要实现的样式实现分割线。

通过 RecyclerView.addItemDecoration 方法可以添加分割线,该方法需要一个 RecyclerView.ItemDecoration 类对象,该类是抽象的并且 Google 没有提供默认实现类,所以需要自己实现 ItemDecoration 类,并实现其中的抽象方法。

其中 getItenOffsets() 方法用于获取 Item 的偏移量,即控制 Item 之间的间隙宽度,其中 Rect 对象中的上下左右代表四个方向的偏移量。onDraw() 和 onDrawOver() 方法用来绘制分割线的样式。下面是抽象的 ItemDecoration 类。

public static abstract class ItemDecoration {
   /**
    * 
    */
    public void onDraw(Canvas c, RecyclerView parent, State state) {
        onDraw(c, parent);
    }

    
    @Deprecated
    public void onDraw(Canvas c, RecyclerView parent) {
    }

    
    public void onDrawOver(Canvas c, RecyclerView parent, State state) {
        onDrawOver(c, parent);
    }

   
    @Deprecated
    public void onDrawOver(Canvas c, RecyclerView parent) {
    }


    
    @Deprecated
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        outRect.set(0, 0, 0, 0);
    }

    
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
        getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                parent);
    }
}

看了这个类以后,我相信有很多同学可能会一头雾水,偏移量是如何影响 Item 的间隙,而绘制方法是如何决定分割线的样式。下面我们就来将完整的流程分析一遍。

ItemDecoration 工作过程分析

1. ItemDecoration 的添加与移除

首先我们先从 recyclerView.addItemDecoration() 方法入手,由这个方法我们可以看到 RecyclerView 中是有一个存储 ItemDecoration 类型对象的集合,addItemDecoration 方法则是将需要添加的 ItemDecoration 加人到这个集合中。并且在添加时如果是第一次添加,则会将 RecyclerView 绘制自身开启然后重新绘制。同时有 removeItemDecoration 方法用来移除指定的 ItemDecoration,移除后同样会重新绘制。

// RecyclerView
@VisibleForTesting LayoutManager mLayout;
private final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<>();

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

public void addItemDecoration(ItemDecoration decor, int index) {
    if (mLayout != null) {
        mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
                + " layout");
    }
    
    // 第一次添加时将绘制状态设置为 false,如果不设置,ViewGroup 默认不会绘制自身
    if (mItemDecorations.isEmpty()) {
        setWillNotDraw(false);
    }
    
    // 根据指定位置插入
    if (index < 0) {
        mItemDecorations.add(decor);
    } else {
        mItemDecorations.add(index, decor);
    }
    markItemDecorInsetsDirty();
    // 重新绘制
    requestLayout();
}

public void removeItemDecoration(ItemDecoration decor) {
    if (mLayout != null) {
        mLayout.assertNotInLayoutOrScroll("Cannot remove item decoration during a scroll  or"
                + " layout");
    }
    // 移除
    mItemDecorations.remove(decor);
    if (mItemDecorations.isEmpty()) {
        setWillNotDraw(ViewCompat.getOverScrollMode(this) == ViewCompat.OVER_SCROLL_NEVER);
    }
    markItemDecorInsetsDirty();
    requestLayout();
}

2. ItemDecoration 的工作过程

View 的显示过程主要有测量、布局、绘制三个部分,在分析绘制之前,我们应该线看测量部分的工作,在 RecyclerView 的 LayoutManager 的 fill() 方法中,这个方法是用来填充 RecyclerView 的,在填充的过程中会调用 layoutChunk 方法,该方法中除了将子 item 添加到 RecyclerView 中,还会执行子 View 的测量、布局等工作。

1) 测量过程

// LayoutManager
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
    // ... 其他代码
    
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { // 判断还有需要添加的 item
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        // ... 其他代码
    }
    // ... 其他代码
}

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    
    View view = layoutState.next(recycler);
    // ... 其他代码
    
    // 测量子 item
    measureChildWithMargins(view, 0, 0);
    
    // ... 其他代码
    
    // 布局子 item
    layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
    // ... 其他代码
}


// RecyclerView
public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    // 获取指定 item 的偏移量并将偏移量加入已使用的宽高中
    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;

    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);
    }
}

/**
 * 遍历 mItemDecorations 集合,由所有 ItemDecoration 的 getItemOffsets 方法获取指定 Item 的偏移量
 */
Rect getItemDecorInsetsForChild(View child) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (!lp.mInsetsDirty) {
        return lp.mDecorInsets;
    }

    //  从 ItemView 的 LayoutParams 中取出默认偏移量
    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);
        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;
}

上面的代码中,在向 RecyclerView 填充 Item 的时候,每一个 Item 的测量都会调用 measureChildWithMargins 方法,该方法中通过 mRecyclerView.getItemDecorInsetsForChild(child) 方法获取到了该 Item 绘制时的偏移量,该方法中会遍历,然后将偏移量加入父 View 已使用的部分,从而根据偏移量限制 Item 的绘制宽高。这个地方看不懂的可以看一下 View 的测量 这篇文章,其中详细讲解了 View 的测量过程。

我们可以总结一下,测量过程中会根据所有 ItemDecoration 中设置的偏移量限制 RecyclerView 中所有 Item 的绘制宽高。测量完成以后每个 Item 的大小就确定了,然后布局过程会完成每个 Item 绘制的内容在 RecyclerView 中具体位置的计算

2) 布局过程

上面提到的 LayoutManager 的 layoutChunk 方法中,除了对每一个 Item 的测量,还做了对 Item 的布局过程,布局过程主要在 layoutDecorated 方法中完成。

// LayoutManager
public void layoutDecorated(View child, int left, int top, int right, int bottom) {
    // 获取偏移量
    final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
    // 根据在父 View 中的位置及偏移量确定最终在父 View 中的位置
    child.layout(left + insets.left, top + insets.top, right - insets.right,
            bottom - insets.bottom);
}

layoutDecorated 方法中主要就是根据 Item 在 RecyclerView 中的位置及自己的偏移量确定最终的位置,其中偏移量的赋值在测量过程中完成。具体布局过程的讲解可以在这篇文章中查看 View 的布局和绘制

3) 绘制过程

测量和布局完成后在 RecyclerView 绘制时就会分别绘制 Item 和 ItemDecoration 了,这个过程如下:

// RecyclerView
@Override
public void draw(Canvas c) {
    // 1. 首先调用 View 的 draw 方法,其中会调用  onDraw() 方法用于绘制自身、dispatchDraw() 方法用于向下分发子 View 的绘制
    super.draw(c);

    final int count = mItemDecorations.size();
    // 遍历 ItemDecoration 集合,并依次调用 onDrawOver 方法
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    // ... 其他代码
}

@Override
public void onDraw(Canvas c) {
    super.onDraw(c); // View 的 onDraw 方法为空实现
    
    // onDraw 方法中,遍历 ItemDecoration 集合,并依次调用 onDraw 方法
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

RecyclerView 的绘制过程中,会调用 draw() 方法,会首先调用 View 类的 draw() 方法,View 的 draw() 方法中,会按一定顺序执行绘制自身 onDraw()、向子 View 传递绘制过程 dispatchDraw() 等。

首先看一下 Recyclerview 的 onDraw 方法,可以看到,主要的操作就是遍历 ItenDecoration 集合,然后依次调用每一个 ItemDecoration 的 onDraw() 方法进行绘制。因为 onDraw() 方法是先于 dispatchDraw() 方法调用的,所以 ItemDecoration 的 onDraw() 方法在 Item 的绘制前执行。

由上面的代码中可以看到 draw() 方法中在 onDraw()、dispatchDraw() 方法执行之后,会遍历 ItemDecoration 集合然后依次调用每个 ItemDecoration 的 onDrawOver() 方法,所以 onDrawOver() 是在 Item 绘制之后才会绘制。

总结

到这里整个过程就分析完了,ItemDecoration 中的方法我们也都了解了在实现分割线时的作用。由于这里提供的时工作流程的分析,所以这里就不提供示例了,看到文章的同学可以自己试着写一下,在实现过程中需要考虑第一个 Item 或者最后一个 Item 时的效果,实现的多了再有自定义分割线的需求时实现过程也会越来越顺手。

推荐阅读更多精彩内容