文章目录
前言
项目的需要,需要实现一个类似如图所示的效果,就是在列表中添加节点的效果,
并且最终显示走到节点的进度的效果(下图没有如此效果,后续加上)。
一、RecyclerView.ItemDecoration是什么?
RecyclerView.ItemDecoration就是RecyclerView的装饰器,RecyclerView的分隔线也是使用ItemDecoration实现。
例如Android默认实现的分隔线DividerItemDecoration,我们直接参考实现。
RecyclerView.ItemDecoration的源码
,过时的方法已经剔除,就不列举了。
public abstract static class ItemDecoration {
/**
* 在提供给 RecyclerView 的 Canvas 中绘制任何适当的装饰。
* 任何通过该方法绘制的内容都会在项目视图绘制之前被绘制,
* 因此将出现在视图下方。
*
* @param c 要绘制的画布
* @param parent RecyclerView 这个 ItemDecoration 正在绘制
* @param state RecyclerView 的当前状态
*/
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
onDraw(c, parent);
}
/**
* 在提供给 RecyclerView 的 Canvas 中绘制任何适当的装饰。
* 该方法绘制的任何内容都会在item view绘制完成后绘制
* 因此将出现在视图上。
*
* @param c 要绘制的画布
* @param parent RecyclerView 这个 ItemDecoration 正在绘制
* @param state RecyclerView 的当前状态。
*/
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
@NonNull State state) {
onDrawOver(c, parent);
}
/**
* 检索给定项目的任何偏移量。 <code>outRect</code> 的每个字段指定
* 项目视图应插入的像素数,类似于填充或边距。
* 默认实现将 outRect 的边界设置为 0 并返回。
*
* <p>
* 如果这个ItemDecoration不影响itemview的定位,应该设置
* <code>outRect</code> 的四个字段(左、上、右、下)全部归零
* 在返回之前。
*
* <p>
* 如果需要访问 Adapter 获取更多数据,可以调用
* {@link RecyclerView#getChildAdapterPosition(View)} 获取适配器的位置
* 看法。
*
* @param outRect 接收输出的矩形。
* @param view 要装饰的子视图
* @param parent RecyclerView 这个 ItemDecoration 正在装饰
* @param state RecyclerView 的当前状态。
*/
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!!)
// 将装饰器传递给RecyclerView
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 {
// left、 top、right(分隔线的宽度)、bottom(给装饰器底部留白)
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 ->
// 注意:
// 当前childCount获取的是当前可见的ItemView
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
// 此处为了获取当前的ItemView在列表中真正的位置
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)
// 此处我们需要使用正确的position在列表中获取数据
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 ->
// 注意:
// 当前childCount获取的是当前可见的ItemView
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
// 此处为了获取当前的ItemView在列表中真正的位置
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)
// 此处我们需要使用正确的position在列表中获取数据
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 var mPaint: Paint? = null
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()
}
/**
* Init data.
*/
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()
}
/**
* Set the item decoration drawable.
*
* @param drawable drawable
*/
fun setDrawable(drawable: Drawable) {
this.mDrawable = drawable
}
/**
* Set the display direction of RecyclerView.
*
* @param orientation
* @see HORIZONTAL
* @see VERTICAL
*/
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)
}
}
}
/**
* When RecyclerView is displayed horizontally, draw item decoration.
*
* @param canvas Canvas
* @param parent RecyclerView
*/
@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)
}
}
}
/**
* Draw maintenance progress.
*
* @param canvas Canvas
* @param child RecyclerView item view
* @param childLayoutPosition Visible item's position for lit
* @param drawable divider line
* @param centerX item center point
* @param centerY
*/
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)
}
}
}
/**
* The current mileage is less than the mileage of the current node.
*
* @param canvas Canvas
* @param child Child view
* @param childLayoutPosition Child view position for list
* @param centerY
* @param nodeMileage Current node mileage
*/
@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) {
// TODO
}
/**
* Determine whether it is vertical.
*/
private fun isVertical(): Boolean {
return mOrientation == VERTICAL
}
/**
* Modify the Paint style to be the horizontal line of the item decorator.
*/
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
}
/**
* Modify the Paint style to be the horizontal line of the item decorator.
*/
private fun setCircleProgressPaint() {
mNodeCircleProgressPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mNodeCircleProgressPaint.color = ContextCompat.getColor(context, R.color.node_progress_color)
mNodeCircleProgressPaint.style = Paint.Style.FILL
}
/**
* Modify the Paint style to the text of the item decorator.
*/
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)
}
/**
* Set the Paint style of the line.
*/
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)
}
/**
* Set the Paint style of the line.
*/
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的文章
总结
这篇文章主要还是代码,如果有需要如此效果的可以直接在代码山修改,因为确实除了代码这几个方法没什么可讲述的。
如果大家有问题,欢迎在评论区讨论,谢谢