还是老样子,搬砖嘛。总会看到一些自己知识点以外的东西。就比如:
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,终于水完了。