RecyclerView中Decoration解读,就只能做分割占位了吗

还是老样子,搬砖嘛。总会看到一些自己知识点以外的东西。就比如:

scss 复制代码
recyclerview.addItemDecoration()

把代码注释后打开,发现这个玩意竟然是分割线。emmmm? 为啥我分割线不是这么画的,有一种泪流满面的感觉,我为啥要自己的写一个view,然后通过设置背景颜色去解决,有的时候还会出现分割线显示不出来的情况,emmmmm? 既然如此,那就加入。

正文

本着打不过就加入的原则,那么就直接开始写Demo,原封不动的抄过来。

简单使用

DividerItemDecoration

这个是recyclerview包下面自带的一个decoration, 砖里面的代码也很简单:

ini 复制代码
binding.recyclerBody.apply {
    addItemDecoration(DividerItemDecoration(context,LinearLayout.VERTICAL))
    layoutManager=LinearLayoutManager(context,RecyclerView.VERTICAL,false)
    val vAdapter=VerticalAdapter()
    vAdapter.submitList(getDebug())
    adapter=vAdapter
}

效果图:看起来这个分割线,有点粗,颜色也不是我们想要的颜色。

但是不要慌,他允许设置一个Drawable 进去:比如像这样:

scss 复制代码
binding.recyclerBody.apply {
    val divider= DividerItemDecoration(context,LinearLayout.VERTICAL)
    getDrawable(R.drawable.shape_linear_line)?.let { divider.setDrawable(it) }
    addItemDecoration(divider)
    layoutManager=LinearLayoutManager(context,RecyclerView.VERTICAL,false)
    val vAdapter=VerticalAdapter()
    vAdapter.submitList(getDebug())
    adapter=vAdapter
}

分割线:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <size android:width="10dp" android:height="10dp" />
    <!-- 设置渐变填充色 -->
    <gradient android:startColor="#00f" android:centerColor="#0f0" android:endColor="#f00"></gradient>
</shape>

那么画出来就长这样:

我们可以发现,这个分割线的高度和我们shape里面定义的高度是一致的,所以说,shape里面的size是必要的。 在drawVertical 函数里面,可以看到:

css 复制代码
            final int top = bottom - mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);

emmm,所以说,size没有,top 就是0,那么就没有。通过这个函数的整个源码:

ini 复制代码
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();
    }

    final int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        parent.getDecoratedBoundsWithMargins(child, mBounds);
        final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
        final int top = bottom - mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(canvas);
    }
    canvas.restore();
}

可以发现,emmm,这个玩意为啥会不停的叠加计算。这个后面再说。通过对 mDivider 对象的使用可以看到另外一个函数也在使用 mDivider

csharp 复制代码
    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) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }

我们可以看到,大致有两个地方都在调我们设置进去的图片的宽高,那么那个的优先级高一些呢?当然是getItemOffsets h函数了,我们知道UI绘制是先确定宽高再填充内容。这个函数就是类似于确定宽高的概念。

RecyclerView.ItemDecoration

上面通过对于DividerItemDecoration的简单使用对于这个itemDecoration 进行了大致的分析。看到了他主要是重写了 getItemOffsets()onDraw() 两个函数。然后还有一个函数onDrawOver 并未重写。这3个函数是做什么的呢?

  • onDraw 也就是我们字面意义上的分割线 绘制区域,这个是位于item 的下面的,可以被item 所覆盖。
  • onDrawOuver 是可以覆盖到item 上面的。当然通过位置计算,也可以画分割线。
  • getItemOffsets是告诉LayoutManger 需要占用多少空间,便于item的排列,因为item的排列是需要开始位置的,逻辑换算一下,这个是不是等于item有背景设置,还有一个magin。我们通过对于recyclerview设置背景色达到所谓分割线的效果。

自定义

既然知道了,他的规则,剩下的全靠我们的想象了。

canvas 绘制颜色

我们知道,getItemOffset() 主要作用是告诉item的位置的,所以逻辑上canvas可以操作的区域就是整个recyclerview 所占用的区域,而canvas直接通过drawColor() 填充出来的也就是整个view,所以说,我们还没有写代码就可以知道,如果item个数不足以充满整个view,那么底部总会有一块区域是我们绘制点颜色。 ok,直接上代码:

kotlin 复制代码
class MyItemDecoration :RecyclerView.ItemDecoration(){
    override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(canvas, parent, state)
        canvas.save()
        canvas.drawColor(Color.BLACK)
        canvas.restore()
    }

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
        outRect.set(0, 0, 0, 10)
    }
}

这很符合我们的前期预期。问题来了,这么写算UI过度渲染吗?从逻辑上而言,应该是算的。

基于坐标绘制形状

通过上面绘制color效果和ItemDecoration的函数解读,我们知道Canvas可绘制区域是整个recyclerView所在区域。 那么引发了一个问题。分割线绘制,应该是需要计算的,而且应该只是绘制了一次。

还是直接上代码:

kotlin 复制代码
class MyItemDecoration1 :RecyclerView.ItemDecoration(){
   private var  paint: Paint = Paint().apply {
       color=Color.BLACK
       style=Paint.Style.STROKE
       strokeCap=Paint.Cap.ROUND
       strokeWidth=100f
   }
    override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(canvas, parent, state)
        canvas.save()
        paint.color=Color.BLACK
        canvas.drawLine(0f,0f, 1000f,500f,paint)
        canvas.restore()
    }

    override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(canvas, parent, state)
        canvas.save()
        paint.color=Color.BLACK
        canvas.drawLine(0f,400f, 1000f,800f,paint)
        canvas.restore()
    }
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
        outRect.set(0, 0, 0, 10)
    }
}

展示效果:

可以看到onDraw() 绘制点在item的下面。onDrawOver() 则在item的上面。所以这种3层的方式,不一定是做分割线,比如说,水印啊,什么的也可以这么写。我们都拿到canvas,这不就是自定义view的相关内容了吗,限制我们的完全是思路。

回归本质,画分割线

既然如此,我们还是画分割线吧,我们可以发现一个问题,那就是分割线在最后一个item的点时候也有。上面尝试了onDraw()onDrawOver() 只会调用一次。那么:getItemOffsets() 会调用多少次呢?

答案是有多少个item就调用多少次。那么我们就可以实现分割线的在某个为啥是否没有的逻辑了。

直接上代码:

kotlin 复制代码
class MyItemDecoration2(
    private val endShowLine: Boolean = true,// 是否显示最后一个的分割线
    private val lineColor: Int = Color.BLACK,// 分割线的颜色
    private val lineHeight: Float = 2f,// 分割线的高度.这个高度包含了paddingVertical的值。
    private val paddingVertical: Float = 0f,// 分割线上下间距
    private val paddingHorizontal: Float = 0f, // 分割线 左右

) : RecyclerView.ItemDecoration() {
     var paint: Paint = Paint().apply {
        color = lineColor
        style = Paint.Style.FILL
        strokeCap = Paint.Cap.ROUND
        // 线条模式
        strokeWidth = lineHeight-paddingVertical*2
    }
    private val mBounds = Rect()
    override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(canvas, parent, state)
        canvas.save()
        // 剩下的就是复制 DividerItemDecoration.drawVertical() 的代码,然后进行更改就行。
        val left: Int
        val right: Int
        if (parent.clipToPadding) {
            left = parent.paddingLeft
            right = parent.width - parent.paddingRight
            canvas.clipRect(
                left, parent.paddingTop, right,
                parent.height - parent.paddingBottom
            )
        } else {
            left = 0
            right = parent.width
        }

        val childCount = parent.childCount
        val startX = paddingHorizontal + left
        val stopX = right - paddingHorizontal
        LogUtils.e(stopX)
        for (i in 0 until childCount) {
            val child = parent.getChildAt(i)
            // 获取item的margins
            parent.getDecoratedBoundsWithMargins(child, mBounds)
            val bottom = mBounds.bottom + child.translationY
            val top = bottom - lineHeight+paddingVertical
            val rect=Rect(startX.toInt(),top.toInt(),stopX.toInt(),(bottom-paddingVertical).toInt())
            //paint.color=lineColor
            // 通过矩形绘制
            canvas.drawRect(rect,paint)
            // 通过线条绘制
            //paint.color=Color.RED
            //canvas.drawLine(startX,rect.centerY().toFloat(),stopX,rect.centerY().toFloat(),paint)
        }
        canvas.restore()
    }


    override fun getItemOffsets(outRect: Rect, itemPosition: Int, parent: RecyclerView) {
        if (parent.adapter == null) {
            outRect.set(0, 0, 0, lineHeight.toInt())
        } else {
            if (endShowLine) {
                outRect.set(0, 0, 0, lineHeight.toInt())
            } else {
                if (itemPosition == (parent.adapter!!.itemCount - 1)) {
                    outRect.set(0, 0, 0, 0)
                } else {
                    outRect.set(0, 0, 0, lineHeight.toInt())
                }
            }
        }
    }
}

通过下面创建对象的展示效果:

ini 复制代码
MyItemDecoration2(endShowLine = false, lineHeight = 30f, paddingHorizontal = 10f, paddingVertical = 10f)

这个里面的主要是需要获取到绘制区域。因为绘制线条特性,支持圆头啥的,我更倾向于使用线条。OK,这个就大致完成了,那么我们就进入总结阶段。

总结

通过这次demo编辑其实对已有知识回顾了很多,当然也有新的知识。本次Demo的完整代码地址

  • DividerItemDecoration+shape 就可以满足常见的分割线需求。但是最后一个item也有分割线。
  • 通过自定义可以解决最后一个item显示分割线的问题。
  • getDecoratedBoundsWithMargins 可以获取到当前view的绘制区域。同时也说明分割线也属于item的一部分。
  • 当画笔有宽度的时候,drawLine的开始位置其实应该是线条对应的居中位置,所谓宽度,应该是颜色的扩散。
  • onDraw 属于低级公民,会被item 遮挡住,设置数据后只会调用一次。
  • onDrawOver 属于高级公民,他会覆盖item。所以分割线使用onDraw 绘制较好。
  • getItemOffsets 每次添加item进入viewGroup的时候就会调用一次。
  • 基于这种3级分层,很多覆盖类型的列表,就容易绘制了。

最后,UI绘制的方式各种各样,限制我们的永远只有自己 。OK,OK,终于水完了。

相关推荐
zhangphil39 分钟前
Android简洁缩放Matrix实现图像马赛克,Kotlin
android·kotlin
m0_5127446439 分钟前
极客大挑战2024-web-wp(详细)
android·前端
lw向北.1 小时前
Qt For Android之环境搭建(Qt 5.12.11 Qt下载SDK的处理方案)
android·开发语言·qt
不爱学习的啊Biao1 小时前
【13】MySQL如何选择合适的索引?
android·数据库·mysql
Clockwiseee2 小时前
PHP伪协议总结
android·开发语言·php
mmsx8 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
众拾达人11 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌12 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley13 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
hedalei15 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576