前言
项目的需要,需要实现一个类似如图所示的效果,就是在列表中添加节点的效果, 并且最终显示走到节点的进度的效果(下图没有如此效果,后续加上)。
一、RecyclerView.ItemDecoration是什么?
RecyclerView.ItemDecoration就是RecyclerView的装饰器,RecyclerView的分隔线也是使用ItemDecoration实现。
例如Android默认实现的分隔线DividerItemDecoration,我们直接参考实现。
RecyclerView.ItemDecoration的源码 ,过时的方法已经剔除,就不列举了。
public abstract static class ItemDecoration {
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
onDraw(c, parent);
}
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
@NonNull State state) {
onDrawOver(c, parent);
}
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
我们最终需要实现的方法就是上述这三个。
我们单独讲下getItemOffsets中的Rect outRect和View view(ItemView) :
二、使用步骤
1.引入库
创建项目之后,RecyclerView跟ViewPager这些一样,应该是包含在android源码中引入的,我们也可以单独做引入。
代码如下(示例):
implementation 'androidx.recyclerview:recyclerview:1.2.1'
2.设置装饰器
2.1 在Activity或者Fragment中设置装饰器
代码如下(示例):
private fun init() {
val list: MutableList<MaintItem> = arrayListOf()
for (i in 0..20) {
list.add(MaintItem("张${i + 1}丰", "${(i + 1) * 100}km/${(i + 1) * 0.5}year", (i + 1) * 100))
}
val adapter = RecyclerAdapter(list) {
Log.e(TAG, "data:: $it")
}
val drawable = ContextCompat.getDrawable(this, R.drawable.item_space)
val itemDecoration = MaintenanceItemDecoration(this, list)
itemDecoration.setDrawable(drawable!!)
rv.addItemDecoration(itemDecoration)
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
rv.layoutManager = layoutManager
rv.adapter = adapter
}
item_space代码如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="5dp"
android:height="5dp"/>
<solid android:color="#00E5FF"/>
</shape>
2.2 实现MaintenanceItemDecoration,继承于RecyclerView.ItemDecoration()
1、实现getItemOffsets方法,代码如下
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
if (mDrawable == null) {
outRect.set(0, 0, 0, 0)
return
}
mDrawable?.let { drawable ->
if (isVertical()) {
outRect.set(0, 0, 0, drawable.intrinsicHeight)
} else {
outRect.set(0, 0, drawable.intrinsicWidth, mOutRectHeight)
}
}
}
效果图如下:可以看到ItemView的右侧和底部都有留白
2、给底部留白部分加上节点,实现onDraw方法
代码如下:在onDraw中实现绘制
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
if (!isVertical()) {
drawHorizontal(c, parent)
} else {
drawVertical(c, parent)
}
}
private fun drawHorizontal(canvas: Canvas, parent: RecyclerView) {
val childCount = parent.childCount
mDrawable?.let { drawable ->
mPaint?.let { paint ->
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val childLayoutPosition = parent.getChildLayoutPosition(child)
Log.i("drawHorizontal", "drawHorizontal childLayoutPosition:: $childLayoutPosition")
if (childLayoutPosition == RecyclerView.NO_POSITION) {
continue
}
val centerX: Float = child.left + (child.right - child.left).toFloat() / 2
val centerY: Float = child.bottom + DEFAULT_RECT_HEIGHT.toFloat() / 2
Log.e("drawHorizontal", "centerX:: $centerX, centerY:: $centerY")
canvas.drawLine(
child.left.toFloat(),
centerY,
child.right.toFloat() + drawable.intrinsicWidth,
centerY, paint)
canvas.drawCircle(centerX, centerY, mCircleRadius, paint)
val text = list[childLayoutPosition].mark
val textWidth = paint.measureText(text)
Log.e("drawHorizontal", "text:: $text")
val textY: Float = child.bottom + DEFAULT_RECT_HEIGHT.toFloat() * 2
val startX = centerX - textWidth / 2
val startY = textY + (paint.descent() - paint.ascent()) / 2
canvas.drawText(text, startX, startY, paint)
}
}
}
}
此处已经完成绘制截图: 需要注意代码中划线的部分,我们正确获取当前ItemView中正确的数据
3、MaintenanceItemDecoration完整代码
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.util.Log
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.dh.daynight.MaintItem
import com.dh.daynight.widget.SizeUtils
import java.lang.IllegalArgumentException
class MaintenanceItemDecoration(
val context: Context,
val list: MutableList<MaintItem>
) : RecyclerView.ItemDecoration() {
companion object {
private val ATTRS: IntArray = intArrayOf(android.R.attr.listDivider)
private const val DEFAULT_RECT_HEIGHT = 50
private const val DEFAULT_COLOR = "#404040"
private const val DEFAULT_CIRCLE_RADIUS = 20f
private const val DEFAULT_TXT_SIZE = 40f
const val HORIZONTAL = LinearLayoutManager.HORIZONTAL
const val VERTICAL = LinearLayoutManager.VERTICAL
}
private var mDrawable: Drawable? = null
private var mOrientation: Int = HORIZONTAL
private var mPaint: Paint? = null
private var mOutRectHeight: Int = DEFAULT_RECT_HEIGHT * 3
private var mCircleRadius: Float = DEFAULT_CIRCLE_RADIUS
init {
val typedArray = context.obtainStyledAttributes(ATTRS)
mDrawable = typedArray.getDrawable(0)
typedArray.recycle()
setOrientation(HORIZONTAL)
initData()
}
private fun initData() {
mPaint = Paint()
mPaint?.isAntiAlias = true
mPaint?.color = Color.parseColor(DEFAULT_COLOR)
mPaint?.style = Paint.Style.FILL
mPaint?.textSize = DEFAULT_TXT_SIZE
}
fun setDrawable(drawable: Drawable) {
this.mDrawable = drawable
}
fun setOrientation(orientation: Int) {
if (orientation != HORIZONTAL || orientation == VERTICAL) {
throw IllegalArgumentException(
"Invalid orientation. It should be either HORIZONTAL or VERTICAL")
}
this.mOrientation = orientation
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
if (!isVertical()) {
drawHorizontal(c, parent)
} else {
drawVertical(c, parent)
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
if (mDrawable == null) {
outRect.set(0, 0, 0, 0)
return
}
mDrawable?.let { drawable ->
if (isVertical()) {
outRect.set(0, 0, 0, drawable.intrinsicHeight)
} else {
outRect.set(0, 0, drawable.intrinsicWidth, mOutRectHeight)
}
}
}
private fun drawHorizontal(canvas: Canvas, parent: RecyclerView) {
val childCount = parent.childCount
mDrawable?.let { drawable ->
mPaint?.let { paint ->
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val childLayoutPosition = parent.getChildLayoutPosition(child)
Log.i("drawHorizontal", "drawHorizontal childLayoutPosition:: $childLayoutPosition")
if (childLayoutPosition == RecyclerView.NO_POSITION) {
continue
}
val centerX: Float = child.left + (child.right - child.left).toFloat() / 2
val centerY: Float = child.bottom + DEFAULT_RECT_HEIGHT.toFloat() / 2
Log.e("drawHorizontal", "centerX:: $centerX, centerY:: $centerY")
canvas.drawLine(
child.left.toFloat(),
centerY,
child.right.toFloat() + drawable.intrinsicWidth,
centerY, paint)
canvas.drawCircle(centerX, centerY, mCircleRadius, paint)
val text = list[childLayoutPosition].mark
val textWidth = paint.measureText(text)
Log.e("drawHorizontal", "text:: $text")
val textY: Float = child.bottom + DEFAULT_RECT_HEIGHT.toFloat() * 2
val startX = centerX - textWidth / 2
val startY = textY + (paint.descent() - paint.ascent()) / 2
canvas.drawText(text, startX, startY, paint)
}
}
}
}
private fun drawVertical(canvas: Canvas, parent: RecyclerView) {
}
private fun isVertical(): Boolean {
return mOrientation == VERTICAL
}
}
3.完成最终版带进度更新的装饰器
3.1 设置装饰器
使用中关注下述代码中带注释部分,谢谢
private fun init() {
val list: MutableList<MaintItem> = arrayListOf()
for (i in 0..20) {
list.add(MaintItem("张${i + 1}丰", "${(i + 1) * 100}km/${(i + 1) * 0.5}year", (i + 1) * 100))
}
val adapter = RecyclerAdapter(list) {
Log.e(TAG, "data:: $it")
}
val drawable = ContextCompat.getDrawable(this, R.drawable.item_space)
val itemDecoration = MaintenanceItemDecoration(this, list, 500)
itemDecoration.setDrawable(drawable!!)
rv.addItemDecoration(itemDecoration)
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
rv.layoutManager = layoutManager
rv.adapter = adapter
}
3.2 书写完整的装饰器
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.util.Log
import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.dh.daynight.MaintItem
import com.dh.daynight.R
import java.lang.IllegalArgumentException
class MaintenanceItemDecoration1(
val context: Context,
val list: MutableList<MaintItem>,
val currentMileage: Int = 0
) : RecyclerView.ItemDecoration() {
companion object {
private const val TAG = "MaintenanceItemDecoration"
private val ATTRS: IntArray = intArrayOf(android.R.attr.listDivider)
private const val HALF = 2f
const val HORIZONTAL = LinearLayoutManager.HORIZONTAL
const val VERTICAL = LinearLayoutManager.VERTICAL
}
private var mDrawable: Drawable? = null
private var mOrientation: Int = HORIZONTAL
private lateinit var mTextPaint: Paint
private lateinit var mLineNormalPaint: Paint
private lateinit var mLineProgressPaint: Paint
private lateinit var mNodeCirclePaint: Paint
private lateinit var mNodeCircleProgressPaint: Paint
private var mOutRectHeight: Int = 0
private var mCircleRadius: Float = 0f
private var mCircleTextSpace: Int = 0
init {
val typedArray = context.obtainStyledAttributes(ATTRS)
mDrawable = typedArray.getDrawable(0)
typedArray.recycle()
setOrientation(HORIZONTAL)
initData()
}
private fun initData() {
setTextPaint()
setLineNormalPaint()
setCirclePaint()
setLineGradientPaint()
setCircleProgressPaint()
mOutRectHeight = context.resources.getDimension(R.dimen.maint_item_rect_h).toInt()
mCircleRadius = context.resources.getDimension(R.dimen.maint_item_circle_radius)
mCircleTextSpace = context.resources.getDimension(R.dimen.maint_item_circle_text_space).toInt()
}
fun setDrawable(drawable: Drawable) {
this.mDrawable = drawable
}
fun setOrientation(orientation: Int) {
if (orientation != HORIZONTAL || orientation == VERTICAL) {
throw IllegalArgumentException(
"Invalid orientation. It should be either HORIZONTAL or VERTICAL")
}
this.mOrientation = orientation
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
if (!isVertical()) {
drawHorizontal(c, parent)
} else {
drawVertical(c, parent)
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
if (mDrawable == null) {
outRect.set(0, 0, 0, 0)
return
}
mDrawable?.let { drawable ->
if (isVertical()) {
outRect.set(0, 0, 0, drawable.intrinsicHeight)
} else {
outRect.set(0, 0, drawable.intrinsicWidth, mOutRectHeight)
}
}
}
@SuppressLint("LongLogTag")
private fun drawHorizontal(canvas: Canvas, parent: RecyclerView) {
val childCount = parent.childCount
Log.d(TAG, "drawHorizontal childCount:: $childCount, currentMileage:: $currentMileage")
mDrawable?.let { drawable ->
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val childLayoutPosition = parent.getChildLayoutPosition(child)
Log.i(TAG, "drawHorizontal childLayoutPosition:: $childLayoutPosition")
if (childLayoutPosition == RecyclerView.NO_POSITION) {
continue
}
val centerX: Float = child.left + (child.right - child.left).toFloat() / 2
val centerY: Float = child.bottom + mCircleRadius
Log.i(TAG, "drawHorizontal centerX:: $centerX, centerY:: $centerY")
canvas.drawLine(
child.left.toFloat(),
centerY,
child.right.toFloat() + drawable.intrinsicWidth,
centerY, mLineNormalPaint)
drawProgress(canvas, child, childLayoutPosition, drawable, centerX, centerY)
drawNodeCircle(canvas, centerX, centerY, childLayoutPosition)
val text = list[childLayoutPosition].mark
val textWidth = mTextPaint.measureText(text)
Log.i(TAG, "drawHorizontal text:: $text")
val textY: Float = child.bottom.toFloat() + mCircleRadius * Companion.HALF + mCircleTextSpace
val startX = centerX - textWidth / Companion.HALF
val startY = textY + (mTextPaint.descent() - mTextPaint.ascent())
canvas.drawText(text, startX, startY, mTextPaint)
}
}
}
private fun drawProgress(
canvas: Canvas,
child: View,
childLayoutPosition: Int,
drawable: Drawable,
centerX: Float,
centerY: Float
) {
val nodeMileage = list[childLayoutPosition].mileage
if (nodeMileage <= 0 || currentMileage <= 0) {
return
}
when {
currentMileage > nodeMileage -> {
canvas.drawLine(
child.left.toFloat(),
centerY,
child.right.toFloat() + drawable.intrinsicWidth,
centerY, mLineProgressPaint)
}
currentMileage < nodeMileage -> {
drawLessThanProgress(
canvas,
child,
childLayoutPosition,
centerY,
nodeMileage
)
}
else -> {
canvas.drawLine(
child.left.toFloat(),
centerY,
centerX,
centerY, mLineProgressPaint)
}
}
}
@SuppressLint("LongLogTag")
private fun drawLessThanProgress(
canvas: Canvas,
child: View,
childLayoutPosition: Int,
centerY: Float,
nodeMileage: Int
) {
val itemWidth = child.right.toFloat() - child.left.toFloat()
val itemHalfWidth = itemWidth / Companion.HALF
if (childLayoutPosition == 0) {
val stopX = itemHalfWidth * (currentMileage.toFloat() / nodeMileage) + child.left
canvas.drawLine(
0f,
centerY,
stopX,
centerY, mLineProgressPaint)
} else {
val preNodeMileage = list[childLayoutPosition - 1].mileage
if (currentMileage !in (preNodeMileage + 1) until nodeMileage) {
return
}
val percent = (currentMileage - preNodeMileage).toFloat() / (nodeMileage - preNodeMileage)
val percentWidth = itemHalfWidth * percent
val startX = if (child.left > 0) {
child.left.toFloat()
} else {
0f
}
val stopX = child.left + percentWidth
Log.e(TAG, "drawProgress left:: ${child.left}, startX:: $startX, stopX:: $stopX")
canvas.drawLine(
startX,
centerY,
stopX,
centerY, mLineProgressPaint)
}
}
private fun drawNodeCircle(
canvas: Canvas,
centerX: Float,
centerY: Float,
childLayoutPosition: Int
) {
val nodeMileage = list[childLayoutPosition].mileage
if (nodeMileage <= 0 || currentMileage <= 0) {
canvas.drawCircle(centerX, centerY, mCircleRadius, mNodeCirclePaint)
return
}
if (currentMileage >= nodeMileage) {
canvas.drawCircle(centerX, centerY, mCircleRadius, mNodeCircleProgressPaint)
} else {
canvas.drawCircle(centerX, centerY, mCircleRadius, mNodeCirclePaint)
}
}
private fun drawVertical(canvas: Canvas, parent: RecyclerView) {
}
private fun isVertical(): Boolean {
return mOrientation == VERTICAL
}
private fun setCirclePaint() {
mNodeCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
mNodeCirclePaint.color = ContextCompat.getColor(context, R.color.maint_item_node_line_color)
mNodeCirclePaint.style = Paint.Style.FILL
}
private fun setCircleProgressPaint() {
mNodeCircleProgressPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mNodeCircleProgressPaint.color = ContextCompat.getColor(context, R.color.node_progress_color)
mNodeCircleProgressPaint.style = Paint.Style.FILL
}
private fun setTextPaint() {
mTextPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mTextPaint.color = ContextCompat.getColor(context, R.color.black)
mTextPaint.style = Paint.Style.FILL
mTextPaint.textSize = context.resources.getDimension(R.dimen.txt_node_size)
}
private fun setLineGradientPaint() {
mLineProgressPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mLineProgressPaint.style = Paint.Style.STROKE
mLineProgressPaint.strokeWidth = context.resources.getDimension(R.dimen.node_progress_line__h)
mLineProgressPaint.color = ContextCompat.getColor(context, R.color.node_progress_color)
}
private fun setLineNormalPaint() {
mLineNormalPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mLineNormalPaint.style = Paint.Style.STROKE
mLineNormalPaint.color = ContextCompat.getColor(context, R.color.maint_item_node_line_color)
mLineNormalPaint.strokeWidth = context.resources.getDimension(R.dimen.node_line_h)
}
}
colors.xml如下
<color name="black">#FF000000</color>
<color name="maint_item_node_line_color">#565656</color>
<color name="node_progress_color">#00F4FF</color>
dimens.xml如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="maint_item_rect_h">100dp</dimen>
<dimen name="maint_item_circle_radius">10dp</dimen>
<dimen name="maint_item_circle_text_space">20dp</dimen>
<dimen name="txt_node_size">25sp</dimen>
<dimen name="node_progress_line__h">8dp</dimen>
<dimen name="node_line_h">4dp</dimen>
</resources>
最终效果:
其实主要就是操作如下两个方法:
1、通过getItemOffsets()在itemView顶部撑出来一片区域 2、通过onDraw()方法来在撑出的区域绘制自己想要的内容
参考
1、解析RecyclerView.ItemDecoration 2、自定义ItemDecoration分割线的高度、颜色、偏移,看完这个你就懂了 3、玩Android上收录的很多关于ItemDecoration的文章
总结
这篇文章主要还是代码,如果有需要如此效果的可以直接在代码山修改,因为确实除了代码这几个方法没什么可讲述的。
如果大家有问题,欢迎在评论区讨论,谢谢
|