源码分析 - 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 时的效果,实现的多了再有自定义分割线的需求时实现过程也会越来越顺手。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容