美文网首页Android开发录android 快速查询Android技术集锦
教你玩转 Android RecyclerView:深入解析 R

教你玩转 Android RecyclerView:深入解析 R

作者: Carson带你学安卓 | 来源:发表于2017-07-12 09:54 被阅读4407次

    前言

    • 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实战系列 :时间轴


    • 下一篇文章我将继续结合 自定义View & RecyclerView.ItemDecoration进行一些有趣的自定义View实例讲解,有兴趣可以继续关注Carson_Ho的安卓开发笔记

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

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


    欢迎关注Carson_Ho的简书!

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

    相关文章

      网友评论

      • 2d94aff7f193:不错 一直不理解那这个类的使用,写的真是详细 都看懂了
      • 七岁就狠拽:排版很喜欢啊~话说大神如果Decoration比较复杂的话要怎么整?比如我整了一个花俏的东西进一个Layout文件里面怎么弄上去?
      • MigrationUK:膜拜大神。。
      • 慢慢退化ing:最喜欢这种排版风格和思路
        慢慢退化ing:有些图比较模糊:sob:
      • 4640f78109a4:有一个问题想请教一下,在画红色分割线时,top的计算时,要加上Viewcompat.gettranslationY(child),这段是什么意思?
        leiiiooo:其实 你可以去掉的,这个gettranslationY是指整个视图相对于本身top的偏移量,如果内部视图会产生拖动,这种情况是要考虑的。
        leiiiooo:源码:parent.getDecoratedBoundsWithMargins(child, this.mBounds); int bottom = this.mBounds.bottom + Math.round(ViewCompat.getTranslationY(child)); top直接等于bottom - drawable或者你的矩形高度即可
        慢慢退化ing:底加上自身的高度就是顶部咯
      • 4640f78109a4:作者你好,看了你的文章受益匪浅啊
        Carson带你学安卓: @fmlong 客气了呀,希望继续支持
      • 沈敏杰:挺不错的,学习了,希望以后能出更精彩的文章!
        Carson带你学安卓: @沈敏杰 会的,每周定期更新,请持续关注
      • 5cb244f5ed84:太棒了
      • 伪文艺大叔:赞一个
      • RetroX:卧槽写的真好

      本文标题:教你玩转 Android RecyclerView:深入解析 R

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