RecyclerView 中 ItemDecoration 源码分析


对于列表的分割线,ListView 本身有个叫 divider 的属性,但 RecyclerView 没有,怎么办?我们可以在适配器的布局item中添加一个分割线,根据 position 位置来显示或隐藏,但有没有别的方法,把分割线解耦出来,不要和适配器混在一起呢?答案是有,就是 ItemDecoration,我们可以用它来实现更多功能,比如边框、装饰图标、悬浮头部等等。我们先来看看它的源码  

public abstract static 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);
    }
}

它位于 RecyclerView 中,其中有几个方法添加了 @Deprecated 注解,标识过时了,所以我们只看其他三个方法即可。先看 getItemOffsets() 方法,这个方法调用了重载方法,有个默认实现方法,

       

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

方法中给 Rect 设置了值,都是零,我们如果重写这个方法,可以给 Rect 中设置自己的值。设置进去的值,对应的是适配器中item与Recycler的左、右的间隙,item 与 item 之间的上、下间隙,这么说也不准,其实就是item四周的间隙,如果默认item宽是铺满全屏,如果设置了 outRect.set(10, 20, 30, 40),那么item距离左边有的距离是10像素,距离右边距离是40像素;第一个item距离顶部距离是20像素,距离底部是30像素;第二个item距离顶部20像素,但这两个item之间的距离是50像素。


如果这样不太好理解的话,可以把它理解为item的margin就可以了,那么为什么可以这么理解,这个 Rect 又是在哪里调用呢,我们看一个方法    

    Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }

        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }

这个方法中,会从 item 的 LayoutParams 属性中,获取 mDecorInsets 属性,是个 Rect,然后通过遍历,获取 RecyclerView 中所有的 ItemDecoration,调用 getItemOffsets() 方法,如果我们重写了这个方法,并且设置了值,那么此时就会累加到 Rect 中,mInsetsDirty 是个标识,防止多次累加。

那么这个方法又是什么时候调用呢?我们知道,只要是 View ,都逃不过 onMeasure() 测量这个方法,这里也一样,我们使用 RecyclerView 需要设置 LayoutManager,以 LinearLayoutManager 为例,测量时会调用 onLayoutChildren() 方法,它又调用 fill() 方法,fill() 调用 layoutChunk() 方法,注意它里面的 measureChildWithMargins() 方法,  

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

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

我们发现,Rect insets = mRecyclerView.getItemDecorInsetsForChild(child) 获取的就是我们设置的值,然后把 insets.left 和 insets.right 累加到 widthUsed 中,heightUsed 也一样, 通过 getChildMeasureSpec() 方法获取到 widthSpec,同理获取到 heightSpec,就是在这个方法中,把 Rect 中的值,也算作 margin 的值了,然后才计算出 item 的宽和高,最后,通过 child.measure(widthSpec, heightSpec) 来给item进行测量。getChildMeasureSpec() 方法中,传进去的item额外的值,其实是 RecyclerView 的 padding,item 的 margin 和 Rect 三者之和,通常默认前两个值都是0,如果设置了另说。


测量完了,那么放置布局呢? RecyclerView 最终会调用 layoutDecorated() 方法

   

    public void layoutDecorated(View child, int left, int top, int right, int bottom) {
        final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
        child.layout(left + insets.left, top + insets.top, right - insets.right,
                bottom - insets.bottom);
    }

注意看,以 LinearLayoutManager 为例,调用的地方为

   

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
       ...
       layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
                right - params.rightMargin, bottom - params.bottomMargin);
       ...     
    }

其中在 layoutDecorated() 中,item 放置时,调用 layout() 方法,已经把 Rect 的值也计算进去了。


上面说的是测量和布局,那么绘制呢?我们知道 View 的绘制分为五步骤,draw() 方法中会调用 onDraw(canvas) 方法,看看 RecyclerView 的重写的方法,   

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

super.draw(c) 方法中,会调用 onDraw() 方法,我们看那 RecyclerView 中的使用   

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

这意思就是说,在 RecyclerView 绘制的时候,会先调用 ItemDecoration 的 onDraw() 方法,然后是 item 本身的绘制,最后是调用 ItemDecoration 的 onDrawOver() 方法,也就是说如果我们重写 ItemDecoration 中的 onDraw() 和 onDrawOver() 方法,item 可能会覆盖 onDraw() 中的绘制,而 onDrawOver() 的绘制可能覆盖在 item 的绘制上面。它的顺序是  onDraw() > dispatchDraw() (Item 的绘制)> onDrawOver()  的顺序,依靠这点,我们可以实现悬浮头部等功能。

这里写个简单的例子,给 RecyclerView 添加一个红色的分割线。

public class ColorDividerItemDecoration extends RecyclerView.ItemDecoration {

    final static String TAG = "ColorDividerItem";

    private float mDividerHeight;

    private Paint mPaint;

    public ColorDividerItemDecoration() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
//       第一个ItemView不需要在上面绘制分割线
        if (parent.getChildAdapterPosition(view) != 0){
            //这里直接硬编码为1px
            outRect.top = 1;
            mDividerHeight = 1;
        }
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {

        int childCount = parent.getChildCount();
        for ( int i = 0; i < childCount; i++ ) {
            View view = parent.getChildAt(i);
            int index = parent.getChildAdapterPosition(view);
            //第一个ItemView不需要绘制
            if ( index == 0 ) {
                continue;
            }
            float dividerTop = view.getTop() - mDividerHeight;
            float dividerLeft = parent.getPaddingLeft();
            float dividerBottom = view.getTop();
            float dividerRight = parent.getWidth() - parent.getPaddingRight();
            c.drawRect(dividerLeft,dividerTop,dividerRight,dividerBottom,mPaint);
        }
    }

}

Activity:

LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.addItemDecoration(new ColorDividerItemDecoration());
recyclerView.setAdapter(adapter);

这样既可。

发布了176 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/100784016