List of articles
- Preface
- One 、RecyclerView.ItemDecoration What is it? ?
- Two 、 Use steps
- Reference resources
- summary
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 ).
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)
:
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
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 :
You need to pay attention to the underlined part of the code , We get the current information correctly ItemView Correct data in
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 :
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 areaReference 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