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

相关推荐
大耳猫4 小时前
主动测量View的宽高
android·ui
帅次6 小时前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
枯骨成佛7 小时前
Android中Crash Debug技巧
android
kim565912 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼12 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ12 小时前
Android Studio使用c++编写
android·c++
csucoderlee13 小时前
Android Studio的新界面New UI,怎么切换回老界面
android·ui·android studio
kim565913 小时前
各版本android studio下载地址
android·ide·android studio
饮啦冰美式13 小时前
Android Studio 将项目打包成apk文件
android·ide·android studio
夜色。13 小时前
Unity6 + Android Studio 开发环境搭建【备忘】
android·unity·android studio