栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Android的RecyclerView.ItemDecoration使用

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Android的RecyclerView.ItemDecoration使用

文章目录
  • 前言
  • 一、RecyclerView.ItemDecoration是什么?
  • 二、使用步骤
    • 1.引入库
    • 2.设置装饰器
      • 2.1 在Activity或者Fragment中设置装饰器
      • 2.2 实现MaintenanceItemDecoration,继承于RecyclerView.ItemDecoration()
        • 1、实现getItemOffsets方法,代码如下
        • 2、给底部留白部分加上节点,实现onDraw方法
        • 3、MaintenanceItemDecoration完整代码
    • 3.完成最终版带进度更新的装饰器
      • 3.1 设置装饰器
      • 3.2 书写完整的装饰器
  • 参考
  • 总结


前言

项目的需要,需要实现一个类似如图所示的效果,就是在列表中添加节点的效果,
并且最终显示走到节点的进度的效果(下图没有如此效果,后续加上)。


一、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 = 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代码如下:



    
    

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
) : 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 = 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,
        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()
    }

    
    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) {
        // TODO
    }

    
    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如下

#FF000000

#565656

#00F4FF

dimens.xml如下



    
    100dp
    
    10dp
    
    20dp
    
    25sp
    
    8dp
    
    4dp

最终效果:

其实主要就是操作如下两个方法:

1、通过getItemOffsets()在itemView顶部撑出来一片区域 2、通过onDraw()方法来在撑出的区域绘制自己想要的内容
参考

1、解析RecyclerView.ItemDecoration
2、自定义ItemDecoration分割线的高度、颜色、偏移,看完这个你就懂了
3、玩Android上收录的很多关于ItemDecoration的文章

总结

这篇文章主要还是代码,如果有需要如此效果的可以直接在代码山修改,因为确实除了代码这几个方法没什么可讲述的。

如果大家有问题,欢迎在评论区讨论,谢谢

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/884237.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号