教你玩转 Android RecyclerView:深入解析 RecyclerView.ItemDecoration类(含实例讲解)


前言

  • RecyclerViewAndroid开发中非常常用,如果能结合ItemDecoration类使用,那么将大大提高RecyclerView的表现效果
  • 本文全面解析了ItemDecoration类,包括ItemDecoration类简介、使用方法 & 实例讲解,最终结合 自定义View实现 时间轴UI开发,希望你们会喜欢。
  1. ItemDecoration类属于RecyclerView的高级用法
  2. 阅读本文前请先学习RecyclerView的使用:Android开发:ListView、AdapterView、RecyclerView全面解析

目录

目录

1. ItemDecoration类 简介

1.1 定义

RecyclerView类的静态内部类

1.2 作用

RecyclerView中的 ItemView 添加装饰

即绘制更多内容,丰富ItemViewUI效果


2. 具体使用

ItemDecoration类中仅有3个方法,具体如下:

public class TestDividerItemDecoration extends RecyclerView.ItemDecoration {

    // 方法1:getItemOffsets()
    // 作用:设置ItemView的内嵌偏移长度(inset)
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
       ...
  }

    // 方法2:onDraw()
    // 作用:在子视图上设置绘制范围,并绘制内容
    // 类似平时自定义View时写onDraw()一样
    // 绘制图层在ItemView以下,所以如果绘制区域与ItemView区域相重叠,会被遮挡
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    ...
  }

    // 方法3:onDrawOver()
    // 作用:同样是绘制内容,但与onDraw()的区别是:绘制在图层的最上层
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
      ...
}

下面,我将详细介绍这3个方法。


2.1 getItemOffsets()

2.1.1 作用

设置ItemView的内嵌偏移长度(inset)

  • 如图,其实RecyclerView 中的 ItemView 外面会包裹着一个矩形(outRect
  • 内嵌偏移长度 是指:该矩形(outRect)与 ItemView的间隔
示意图
  • 内嵌偏移长度分为4个方向:上、下、左、右,并由outRect 中的 top、left、right、bottom参数 控制

top、left、right、bottom参数默认 = 0,即矩形和Item重叠,所以看起来矩形就消失了

示意图

2.1.2 具体使用


    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
      // 参数说明:
        // 1. outRect:全为 0 的 Rect(包括着Item)
        // 2. view:RecyclerView 中的 视图Item
        // 3. parent:RecyclerView 本身
        // 4. state:状态

      outRect.set(50, 0, 0,50);
      // 4个参数分别对应左(Left)、上(Top)、右(Right)、下(Bottom)
      // 上述语句代表:左&下偏移长度=50px,右 & 上 偏移长度 = 0
       ...
  }

示意图

2.1.3 源码分析

  • RecyclerView本质上是一个自定义ViewGroup,子视图child = 每个ItemView
  • 其通过 LayoutManager测量并布局 ItemView
public void measureChild(View child, int widthUsed, int heightUsed) {

// 参数说明:
  // 1. child:要测量的子view(ItemView)
  // 2. widthUsed: 一个ItemView的所有ItemDecoration占用的宽度(px)
  // 3. heightUsed:一个ItemView的所有ItemDecoration占用的高度(px)

    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    // 累加当前ItemDecoration 4个属性值->>分析1
    
    widthUsed += insets.left + insets.right;
    // 计算每个ItemView的所有ItemDecoration的宽度
    heightUsed += insets.top + insets.bottom;
    // 计算每个ItemView的所有ItemDecoration的高度

    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
            getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
            canScrollHorizontally());
    // 测量child view(ItemView)的宽度
    // 第三个参数设置 child view 的 padding,即ItemView的Padding
    // 而该参数把 insets 的值算进去,所以insets 值影响了每个 ItemView 的 padding值

    // 高度同上
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
            getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
            canScrollVertically());
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}

// 分析完毕,请跳出
<-- 分析1:getItemDecorInsetsForChild()-->
Rect getItemDecorInsetsForChild(View child) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    insets.set(0, 0, 0, 0);
    for (int i = 0; i < decorCount; i++) {
        mTempRect.set(0, 0, 0, 0);

        // 获取getItemOffsets() 中设置的值
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
        // 将getItemOffsets() 中设置的值添加到insets 变量中

        insets.left += mTempRect.left;
        insets.top += mTempRect.top;
        insets.right += mTempRect.right;
        insets.bottom += mTempRect.bottom;
    }
    // 最终返回
    return insets;
}

// insets介绍
  // 1. 作用:
    // a. 把每个ItemView的所有 ItemDecoration 的 getItemOffsets 中设置的值累加起来,(每个ItemView可添加多个ItemDecoration)
    // 即把每个ItemDecoration的left, top, right, bottom 4个属性分别累加
    // b. 记录上述结果
    // c. inset就像padding和margin一样,会影响view的尺寸和位置

  // 2. 使用场景:设置View的边界大小,使得其大小>View的背景大小
  // 如 按钮图标(View的背景)较小,但是我们希望按钮有较大的点击热区(View的边界大小)

// 返回到分析1进来的原处

总结

  • 结论:outRect4个属性值影响着ItemView的Padding值
  • 具体过程:在RecyclerView进行子View宽高测量时(measureChild()),会将getItemOffsets()里设置的 outRect4个属性值(Top、Bottom、Left、Right)通过insert值累加 ,并最终添加到子ViewPadding属性中

2.2 onDraw()

2.2.1 作用

通过 Canvas 对象绘制内容

2.2.2 具体使用

  • 使用方法类似自定义View时的onDraw()

请看我写的自定义View文章:自定义View Draw过程- 最易懂的自定义View原理系列(4)

@Override
    public void onDraw(Canvas c, RecyclerView parent, 
                                  RecyclerView.State state) {
      ....
      // 使用类似自定义View时的 onDraw()
      
}

2.2.3 特别注意

注意点1:ItemdecorationonDraw()绘制会先于ItemViewonDraw()绘制,所以如果在ItemdecorationonDraw()中绘制的内容在ItemView边界内,就会被ItemView遮挡住。如下图:

此现象称为onDraw()OverDraw现象

示意图

解决方案:配合前面的 getItemOffsets() 一起使用在outRect矩形 与 ItemView的间隔区域 绘制内容

即:通过getItemOffsets() 设置与 Item 的间隔区域,从而获得与ItemView不重叠的绘制区域

示意图

注意点2: getItemOffsets() 针对是每一个 ItemView的,而 onDraw() 针对 RecyclerView 本身

解决方案:在 使用onDraw()绘制时,需要先遍历RecyclerView 的所有ItemView分别获取它们的位置信息,然后再绘制内容

  1. 此处遍历的RecyclerViewItemView(即Child view),并不是 Adapter 设置的每一个 item,而是可见的 item
  2. 因为只有可见的Item 才是RecyclerViewChild view
@Override
    public void onDraw(Canvas c, RecyclerView parent, 
                                  RecyclerView.State state) {

    // RecyclerView 的左边界加上 paddingLeft距离 后的坐标位置
    final int left = parent.getPaddingLeft();
    // RecyclerView 的右边界减去 paddingRight 后的坐标位置
    final int right = parent.getWidth() - parent.getPaddingRight();
    // 即左右边界就是 RecyclerView 的 ItemView区域

    // 获取RecyclerView的Child view的个数
    final int childCount = parent.getChildCount();

    // 设置布局参数
    final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                .getLayoutParams();

    // 遍历每个RecyclerView的Child view
    // 分别获取它们的位置信息,然后再绘制内容
    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        int index = parent.getChildAdapterPosition(view);
            // 第一个Item不需要绘制
            if ( index == 0 ) {
                continue;
            }
        // ItemView的下边界:ItemView 的 bottom坐标 + 距离RecyclerView底部距离 +translationY
        final int top = child.getBottom() + params.bottomMargin +
                Math.round(ViewCompat.getTranslationY(child));
        // 绘制分割线的下边界 = ItemView的下边界+分割线的高度
        final int bottom = top + mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);
    }
}

}

2.2.4 应用场景

在丰富 ItemView 的显示效果,即在ItemView 的基础上绘制内容

如分割线等等

2.2.5 实例讲解

  • 实例说明:在ItemView设计一个高度为 10 px 的红色分割线
  • 思路
    1. 通过getItemOffsets()设置与 Item 的下间隔区域 = 10 px

设置好onDraw()可绘制的区域

  1. 通过onDraw()绘制一个高度 = 10px的矩形(填充颜色=红色)
示意图
  • 具体实现

步骤1:自定义ItemDecoration类

ItemDecoration.java

public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    private Paint mPaint;
    
    // 在构造函数里进行绘制的初始化,如画笔属性设置等
    public DividerItemDecoration() {

        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        // 画笔颜色设置为红色
    }
    
    // 重写getItemOffsets()方法
    // 作用:设置矩形OutRect 与 Item 的间隔区域
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);


        int itemPosition = parent.getChildAdapterPosition(view);
        // 获得每个Item的位置

        // 第1个Item不绘制分割线
        if (itemPosition != 0) {
            outRect.set(0, 0, 0, 10);
            // 设置间隔区域为10px,即onDraw()可绘制的区域为10px
        }
    }
    
    // 重写onDraw()
    // 作用:在间隔区域里绘制一个矩形,即分割线
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);

        // 获取RecyclerView的Child view的个数
        int childCount = parent.getChildCount();

        // 遍历每个Item,分别获取它们的位置信息,然后再绘制对应的分割线
        for ( int i = 0; i < childCount; i++ ) {
            // 获取每个Item的位置
            final View child = parent.getChildAt(i);
            int index = parent.getChildAdapterPosition(child);
            // 第1个Item不需要绘制
            if ( index == 0 ) {
                continue;
            }
            
            // 获取布局参数
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            // 设置矩形(分割线)的宽度为10px
            final int mDivider = 10;

            // 根据子视图的位置 & 间隔区域,设置矩形(分割线)的2个顶点坐标(左上 & 右下)

            // 矩形左上顶点 = (ItemView的左边界,ItemView的下边界)
            // ItemView的左边界 = RecyclerView 的左边界 + paddingLeft距离 后的位置
            final int left = parent.getPaddingLeft();
            // ItemView的下边界:ItemView 的 bottom坐标 + 距离RecyclerView底部距离 +translationY
            final int top = child.getBottom() + params.bottomMargin +
                    Math.round(ViewCompat.getTranslationY(child));

            // 矩形右下顶点 = (ItemView的右边界,矩形的下边界)
            // ItemView的右边界 = RecyclerView 的右边界减去 paddingRight 后的坐标位置
            final int right = parent.getWidth() - parent.getPaddingRight();
            // 绘制分割线的下边界 = ItemView的下边界+分割线的高度
            final int bottom = top + mDivider;


            // 通过Canvas绘制矩形(分割线)
            c.drawRect(left,top,right,bottom,mPaint);
        }
    }
}

步骤2:在设置RecyclerView时添加该分割线即可

        Rv = (RecyclerView) findViewById(R.id.my_recycler_view);
        //使用线性布局
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        Rv.setLayoutManager(layoutManager);
        Rv.setHasFixedSize(true);

        // 通过自定义分割线类 添加分割线
        Rv.addItemDecoration(new DividerItemDecoration());

        //为ListView绑定适配器
        myAdapter = new MyAdapter(this,listItem);
        Rv.setAdapter(myAdapter);
        myAdapter.setOnItemClickListener(this);

2.2.6 结果展示

示意图

2.2.7 源码地址

Carson_Ho的Github地址:RecyclerView_ItemDecoration


2.3 onDrawOver()

2.3.1 作用

  • onDraw()类似,都是绘制内容
  • 但与onDraw()的区别是:ItemdecorationonDrawOver()绘制 是后于 ItemViewonDraw()绘制
  1. 即不需要考虑绘制内容被ItemView遮挡的问题,反而 ItemView会被onDrawOver()绘制的内容遮挡
  2. 绘制时机比较:
    Itemdecoration.onDraw()> ItemView.onDraw() > Itemdecoration.onDrawOver()
示意图

2.3.2 具体使用

  • 使用方法类似自定义View时的onDraw()

请看我写的自定义View文章:自定义View Draw过程- 最易懂的自定义View原理系列(4)

@Override
    public void onDrawOver(Canvas c, RecyclerView parent, 
                                  RecyclerView.State state) {
      ....
      // 使用类似自定义View时的 onDraw()
      
}

2.3.3 应用场景

RecyclerView / 特定的 ItemView 上绘制内容,如蒙层、重叠内容等等

2.3.4 实例讲解

  • 实例说明:在 RecyclerView 上每个 ItemView 上叠加一个角标
角度示意图
  • 具体代码实现

** 步骤1:自定义 ItemDecoration类**

public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    private Paint mPaint;
    private Bitmap mIcon;

    // 在构造函数里进行绘制的初始化,如画笔属性设置等
    public DividerItemDecoration(Context context) {

        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        // 画笔颜色设置为红色

        // 获取图片资源
        mIcon = BitmapFactory.decodeResource(context.getResources(), R.mipmap.logo);
    }

    // 重写onDrawOver()
    // 将角度绘制到ItemView上
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);

        // 获取Item的总数
        int childCount = parent.getChildCount();
        // 遍历Item
        for ( int i = 0; i < childCount; i++ ) {
            // 获取每个Item的位置
            View view = parent.getChildAt(i);
            int index = parent.getChildAdapterPosition(view);

            // 设置绘制内容的坐标(ItemView的左边界,ItemView的上边界)
            // ItemView的左边界 = RecyclerView 的左边界 = paddingLeft距离 后的位置
            final int left = parent.getWidth()/2;
            // ItemView的上边界
            float top = view.getTop();

            // 第1个ItemView不绘制
            if ( index == 0 ) {
                continue;
            }
                // 通过Canvas绘制角标
                c.drawBitmap(mIcon,left,top,mPaint);
        }
    }

}

** 步骤2:在设置RecyclerView时添加即可 **

  Rv = (RecyclerView) findViewById(R.id.my_recycler_view);
        //使用线性布局
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        Rv.setLayoutManager(layoutManager);
        Rv.setHasFixedSize(true);

        //用自定义分割线类设置分割线
        Rv.addItemDecoration(new DividerItemDecoration());

        //为ListView绑定适配器
        myAdapter = new MyAdapter(this,listItem);
        Rv.setAdapter(myAdapter);
        myAdapter.setOnItemClickListener(this);

2.3.5 结果展示

示意图

2.3.6 源码地址

Carson_Ho的Github地址:RecyclerView_ItemDecoration


3. 使用总结

我用一张图总结RecyclerView ItemDecoration类的使用

示意图


4. 结合自定义View的实践应用:时间轴

  • Android开发中,时间轴的 UI需求非常常见,如下图:

    示意图

  • 本次实例将结合 自定义View & RecyclerView的知识,手把手教你实现该常见 & 实用的自定义View:时间轴

具体请看文章:Android 自定义View实战系列 :时间轴



请点赞!因为你的鼓励是我写作的最大动力!

相关文章阅读
Android开发:最全面、最易懂的Android屏幕适配解决方案
Android事件分发机制详解:史上最全面、最易懂
Android开发:史上最全的Android消息推送解决方案
Android开发:最全面、最易懂的Webview详解
Android开发:JSON简介及最全面解析方法!
Android四大组件:Service服务史上最全面解析
Android四大组件:BroadcastReceiver史上最全面解析


欢迎关注Carson_Ho的简书!

不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度

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

推荐阅读更多精彩内容