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,终于水完了。

相关推荐
落落落sss2 小时前
项目集成sharding-jdbc
android·java·数据库·spring·mybatis
哈哈皮皮虾的皮2 小时前
安卓开发中,可以反射去换肤,那么我们应该也可以使用反射去串改我们的程序,作为开发者,我们如何保证我们自己的应用的安全呢?
android·网络·安全
是阿臻2 小时前
在 macOS 上安装 ADB给安卓手机装APK,同样适用智能电视、车机
android·macos·adb
不太会写3 小时前
dhtmlxGantt 甘特图 一行展示多条任务类型
android
中式代码美式咖啡3 小时前
在Spring Boot中实现多环境配置
android·spring boot·后端
吾爱星辰4 小时前
Kotlin 抛出和捕获异常(十一)
android·java·开发语言·jvm·kotlin
alexhilton4 小时前
搞定在Jetpack Compose中优雅地申请运行时权限
android·kotlin·android jetpack
GEEKVIP7 小时前
摆脱困境并在iPhone手机上取回删除照片的所有解决方案
android·macos·ios·智能手机·电脑·笔记本电脑·iphone
呆萌小新@渊洁11 小时前
后端接收数组,集合类数据
android·java·开发语言
ByteSaid13 小时前
Android 内核开发之—— repo 使用教程
android·git