Recyclerview.com for Android Itemdecoration use


Preface

The needs of the project , You need to achieve an effect similar to that shown in the figure , Is to add nodes to the list The effect of ,
And finally show the progress of reaching the node ( The figure below has no such effect , Then add ).
 Insert picture description here


One 、RecyclerView.ItemDecoration What is it? ?

RecyclerView.ItemDecoration Namely RecyclerView The decorator ,RecyclerView The separation line is also used ItemDecoration Realization .

for example Android The default implementation of the separator DividerItemDecoration, We refer directly to the implementation .

RecyclerView.ItemDecoration Source code , Outdated methods have been eliminated , Let's not list .

public abstract static class ItemDecoration {

/** * Is offering to RecyclerView Of Canvas Draw any appropriate decoration in . * Anything drawn in this way will be drawn before the project view is drawn , * So it will appear below the view . * * @param c The canvas to draw * @param parent RecyclerView This ItemDecoration Drawing * @param state RecyclerView Current state */
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {

onDraw(c, parent);
}
/** * Is offering to RecyclerView Of Canvas Draw any appropriate decoration in . * Anything drawn by this method will be in item view Draw... After drawing * So it will appear on the view . * * @param c The canvas to draw * @param parent RecyclerView This ItemDecoration Drawing * @param state RecyclerView Current state . */
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
@NonNull State state) {

onDrawOver(c, parent);
}
/** * Retrieves any offset for a given item . <code>outRect</code> Each field of the specifies * The number of pixels the project view should insert , Similar to padding or margins . * The default implementation will outRect The boundary of is set to 0 And back to . * * <p> * If this ItemDecoration No effect itemview The positioning of , Should be set * <code>outRect</code> Four fields of ( Left 、 On 、 Right 、 Next ) All zeros * Before returning . * * <p> * If you need to visit Adapter Get more data , You can call * {@link RecyclerView#getChildAdapterPosition(View)} Get the location of the adapter * view . * * @param outRect Rectangle that receives the output . * @param view Sub view to decorate * @param parent RecyclerView This ItemDecoration Decorating * @param state RecyclerView Current state . */
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull State state) {

getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}

The final method we need to implement is the above three .

Let's talk about it alone getItemOffsets Medium Rect outRect and View view(ItemView)
 Insert picture description here

Two 、 Use steps

1. Import and stock in

After creating the project ,RecyclerView Follow ViewPager These are the same , Should be included in android Introduced in the source code , We can also do it alone .

The code is as follows ( Example ):

implementation 'androidx.recyclerview:recyclerview:1.2.1'

2. Decorator settings

2.1 stay Activity perhaps Fragment Set decorator in

The code is as follows ( Example ):

private fun init() {

val list: MutableList<MaintItem> = arrayListOf()
for (i in 0..20) {

list.add(MaintItem(" Zhang ${i + 1} Feng ", "${(i + 1) * 100}km/${(i + 1) * 0.5}year", (i + 1) * 100))
}
val adapter = RecyclerAdapter(list) {

Log.e(TAG, "data:: $it")
}
// Picture of separator line 
val drawable = ContextCompat.getDrawable(this, R.drawable.item_space)
// Create a decorator like 
val itemDecoration = MaintenanceItemDecoration(this, list)
itemDecoration.setDrawable(drawable!!)
// Pass the decorator to RecyclerView
rv.addItemDecoration(itemDecoration)
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
rv.layoutManager = layoutManager
rv.adapter = adapter
}

item_space The code is as follows :

<?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 Realization MaintenanceItemDecoration, Inherited from RecyclerView.ItemDecoration()

1、 Realization getItemOffsets Method , The code is as follows

override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {

super.getItemOffsets(outRect, view, parent, state)
// Judge whether the picture of dividing line is set , If not set , Don't deal with 
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( The width of the dividing line )、bottom( Leave space at the bottom of the decorator )
outRect.set(0, 0, drawable.intrinsicWidth, mOutRectHeight)
}
}
}

The renderings are as follows : You can see ItemView There are blanks on the right and bottom of
 Insert picture description here

2、 Add nodes to the blank part at the bottom , Realization onDraw Method

The code is as follows : stay onDraw Realize drawing in

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 ->
// Be careful :
// At present childCount Get the currently visible ItemView
for (i in 0 until childCount) {

val child = parent.getChildAt(i)
// Here to get the current ItemView The real place in the list 
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)
// Here we need to use the correct position Get data in the list 
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)
}
}
}
}

The screenshot has been drawn here :
 Insert picture description here
You need to pay attention to the underlined part of the code , We get the current information correctly ItemView Correct data in
 Insert picture description here

3、MaintenanceItemDecoration Complete code

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 ->
// Be careful :
// At present childCount Get the currently visible ItemView
for (i in 0 until childCount) {

val child = parent.getChildAt(i)
// Here to get the current ItemView The real place in the list 
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)
// Here we need to use the correct position Get data in the list 
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. Finish the decorator with the progress update of the final version

3.1 Decorator settings

Pay attention to the annotated part of the following code , thank you

private fun init() {

val list: MutableList<MaintItem> = arrayListOf()
for (i in 0..20) {

// The last attribute passes in the value represented by the node 
list.add(MaintItem(" Zhang ${i + 1} Feng ", "${(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)
// Finally, the parameter passes the value of the current progress 
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 Write a complete decorator

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")
// Draw the maintenance scale 
canvas.drawLine(
child.left.toFloat(),
centerY,
child.right.toFloat() + drawable.intrinsicWidth,
centerY, mLineNormalPaint)
// Draw the maintenance progress 
drawProgress(canvas, child, childLayoutPosition, drawable, centerX, centerY)
// Draw the maintenance node 
drawNodeCircle(canvas, centerX, centerY, childLayoutPosition)
// Draw maintenance node text 
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 as follows

<color name="black">#FF000000</color>
<!-- Color of nodes and connectors -->
<color name="maint_item_node_line_color">#565656</color>
<!-- Color values of nodes and connecting lines that have progressed -->
<color name="node_progress_color">#00F4FF</color>

dimens.xml as follows

<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- The height of the bottom blank -->
<dimen name="maint_item_rect_h">100dp</dimen>
<!-- Radius of circular node -->
<dimen name="maint_item_circle_radius">10dp</dimen>
<!-- Spacing between circular nodes and text -->
<dimen name="maint_item_circle_text_space">20dp</dimen>
<!-- Node text size -->
<dimen name="txt_node_size">25sp</dimen>
<!-- Height of progress connection line -->
<dimen name="node_progress_line__h">8dp</dimen>
<!-- Height of node connecting line -->
<dimen name="node_line_h">4dp</dimen>
</resources>

Final effect :
 Insert picture description here
 Insert picture description here

In fact, it mainly operates the following two methods :

1、 adopt getItemOffsets() stay itemView An area protrudes from the top 2、 adopt onDraw() Method to draw what you want in the raised area

Reference resources

1、 analysis RecyclerView.ItemDecoration
2、 Customize ItemDecoration The height of the dividing line 、 Color 、 The offset , After reading this, you will understand
3、 play Android There are a lot of about ItemDecoration The article

summary

This article is mainly about code , If you need such an effect, you can modify it directly in the code mountain , Because it's true that there's nothing to talk about except code .

If you have questions , Welcome to discuss in the comment area , thank you


thank
Similar articles

2022-05-14