Android | 一篇文章带你玩转RecyclerView.I

作者: 彭旭锐 | 来源:发表于2020-03-14 02:42 被阅读0次

    前言

    • Android开发中,RecyclerView十分常用,结合ItemDecoration还能实现很多意向不到的效果;
    • 这篇文章将总结ItemDecoration用法、源码解析和示例,希望能帮上忙。

    目录


    1. 简介

    • 定义
      列表Item的修饰器,是RecyclerView的抽象静态内部类

    • 作用
      用于装饰列表Item,添加间距、高亮或者分组边界等


    2. 使用示例

    首先,我们使用官方提供的DividerItemDecoration演示ItemDecoration用法,在这里,我们为RecyclerView设置了两条分割线,具体代码如下:

    • 黑色分割线drawable
    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <size android:height="5dp" />
        <solid android:color="#FFFFFF" />
    </shape>
    
    • 白色分割线drawable
    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <size android:height="5dp" />
        <solid android:color="#000000" />
    </shape>
    
    • 调用RecyclerView#addItemDecoration()添加分割线:
    val rv: RecyclerView = findViewById(R.id.rv);
    rv.layoutManager = LinearLayoutManager(this)
    // 添加第一个ItemDecoration
    rv.addItemDecoration(DividerItemDecoration(this, VERTICAL).apply {
        setDrawable(ContextCompat.getDrawable(this@MainActivity, R.drawable.shape_divider_1)!!)
    })
    // 添加第二个ItemDecoration
    rv.addItemDecoration(DividerItemDecoration(this, VERTICAL).apply {
        setDrawable(ContextCompat.getDrawable(this@MainActivity, R.drawable.shape_divider_2)!!)
    })
    rv.adapter = TestAdapter()
    
    • 效果如下:
    效果图
    • 小结
        1. 使用DividerItemDecoration时调用setDrawable()设置分割线
        1. 调用RecyclerView#addItemDecoration()添加ItemDecoration
        1. 可以添加多个ItemDecoration,按添加顺序累加

    3. API

    现在我们关注ItemDecoration提供的三个方法,具体描述如下:

    public abstract static class ItemDecoration {
    
        // 1. 设置ItemView的边距
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
        }
        
        // 2. 在ItemView下层图层绘制,绘制内容会被ItemView遮挡
        public void onDraw(Canvas c, RecyclerView parent, State state) {
        }
    
        // 3. 在ItemView上层图层绘制,绘制内容会遮挡ItemView
        public void onDrawOver(Canvas c, RecyclerView parent, State state) {
        }
    }
    

    3.1 getItemOffsets(Rect outRect,...)

    • 作用:
      RecyclerView的每一项ItemView都绘制在一个矩形区域内;通过修改getItemOffsets(Rect outRect...)第一个参数outRecttop、left、right、bottom属性值,可以控制ItemView在相对于矩形区域的间距,如以下示意图所示:
    getItemOffsets() 示意图
    • 源码分析:
      间距与测量流程有关,因此我们看看RecyclerView#measureChild(...),它是在LayoutManager调用的,如下所示:
    // RecyclerView
    
    public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)child.getLayoutParams();
        // 每个ItemDecoration设置的上、下、左、右边距累加起来
        Rect insets = this.mRecyclerView.getItemDecorInsetsForChild(child);
        widthUsed += insets.left + insets.right;
        heightUsed += insets.top + insets.bottom;
        // 将累加的间距算到ItemView的padding里进行测量
        int widthSpec = getChildMeasureSpec(this.getWidth(), this.getWidthMode(), this.getPaddingLeft() + this.getPaddingRight() + widthUsed, lp.width, this.canScrollHorizontally());
        int heightSpec = getChildMeasureSpec(this.getHeight(), this.getHeightMode(), this.getPaddingTop() + this.getPaddingBottom() + heightUsed, lp.height, this.canScrollVertically());
        if (this.shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
            child.measure(widthSpec, heightSpec);
        }
    }
    
    Rect getItemDecorInsetsForChild(View child) {
        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        } else if (this.mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            return lp.mDecorInsets;
        } else {
            Rect insets = lp.mDecorInsets;
            // 初始化为0
            insets.set(0, 0, 0, 0);
            int decorCount = this.mItemDecorations.size();
    
            for(int i = 0; i < decorCount; ++i) {
                // 将outRech的上、下、左、右置零
                this.mTempRect.set(0, 0, 0, 0);
                // 依次调用每个ItemDecoration#getItemOffsets()为outRect赋值
                ((RecyclerView.ItemDecoration)this.mItemDecorations.get(i)).getItemOffsets(this.mTempRect, child, this, this.mState);
                // 每个ItemDecoration设置的上、下、左、右边距累加起来
                insets.left += this.mTempRect.left;
                insets.top += this.mTempRect.top;
                insets.right += this.mTempRect.right;
                insets.bottom += this.mTempRect.bottom;
            }
    
            lp.mInsetsDirty = false;
            return insets;
        }
    }
    
    • 举例:
      以前面提到的DividerItemDecoration作为例子,在getItemOffsets()中,纵向布局时,它将图片高度作为bottom边距,横向布局时,它将图片宽度作为right边距:
    // DividerItemDecoration
    private Drawable mDivider;
    
    public void setDrawable(Drawable drawable) {
        if (drawable == null) {
            throw new IllegalArgumentException("Drawable cannot be null.");
        }
        mDivider = drawable;
    }
    
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (mDivider == null) {
            outRect.set(0, 0, 0, 0);
            return;
        }
        if (mOrientation == VERTICAL) {
            // 纵向布局时,将图片高度作为bottom边距
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            // 横向布局时,将图片宽度作为right边距
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
    
    • 小结:
        1. 修改getItemOffsets(Rect outRect...)第一个参数outRecttop、left、right、bottom属性值,可以控制ItemView在相对于矩形区域的间距;
        1. RecyclerView在测量时,会将添加的所有ItemDecorationtop、left、right、bottom边距累加起来,影响ItemViewpadding

    3.2 onDraw(Canvas c,...)

    • 作用:
      ItemView的下层图层绘制,因此如果ItemDecoration#onDraw()绘制的内容在ItemView的范围内,将被ItemView遮挡,如以下示意图所示:
    onDraw() 示意图
    • 源码分析:
      draw与绘制流程有关,因此我们看看RecyclerView#onDraw(...),如下所示:
    // RecyclerView
    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);
    
        final int count = mItemDecorations.size();
        // 调用每个ItemDecoration的onDraw(...)
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }
    

    可以看到,RecyclerView#onDraw(...)会调用每个ItemDecoration#onDraw()进行绘制;与getItemOffsets()不同的是,getItemOffsets()是处理每个ItemView的,而onDraw()是针对整个RecyclerView进行绘制

    • 举例:
      以前面提到的DividerItemDecoration作为例子,
    // DividerItemDecoration
    
    private final Rect mBounds = new Rect();
    
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getLayoutManager() == null || mDivider == null) {
            return;
        }
        if (mOrientation == VERTICAL) {
            // 纵向
            drawVertical(c, parent);
        } else {
            // 横向
            drawHorizontal(c, parent);
        }
    }
    
    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int left;
        final int right;
        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }
        // RecyclerView的ChildView的个数,ChildView是可见的区域
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            // 处理每个可见的ChildView
            final View child = parent.getChildAt(i);
            // 获取Item的矩形区域
            parent.getDecoratedBoundsWithMargins(child, mBounds);
            // bottom是矩形区域bottom减去ItemView的translationY
            final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
            // top是bottom减分割线高度
            final int top = bottom - mDivider.getIntrinsicHeight();
            // 设置分割线范围
            mDivider.setBounds(left, top, right, bottom);
            // 绘制分割线
            mDivider.draw(canvas);
        }
        canvas.restore();
    }
    // 横向省略...
    
    • 小结:
        1. onDraw()ItemView的下层图层绘制;
        1. onDraw()是针对整个RecyclerView绘制的,使用时需要先遍历RecyclerView的所有可见的ChildView,分别获取它们的位置信息,然后再绘制内容。

    3.3 onDrawOver(Canvas c,...)

    • 作用:
      ItemView上层图层绘制,因此ItemDecoration#onDrawOver()绘制的内容会覆盖ItemView,如以下示意图所示:
    onDrawOver() 示意图
    • onDrawOver()onDraw()类似,区别在于绘制的图层不同,实战中用的比较少,此处不过多展开。

    4. 示例讲解

    Editing...

    4.1 万能分割线

    4.2 快递时间轴

    4.3 联系人分类


    推荐阅读


    2020 永远不要放弃希望,祝愿大家都能够平安健康!武汉加油!

    相关文章

      网友评论

        本文标题:Android | 一篇文章带你玩转RecyclerView.I

        本文链接:https://www.haomeiwen.com/subject/cosojhtx.html