自定义一个带步骤的进度View

需求

最近需要做一个如下图带步骤的进度UI, 步骤的数量可根据输入而变化:
步骤进度View
我们就实现进度条和下面的文本描述, 本来想的是使用RecyclerView加GridLayoutManager来实现, 如分成五份后, 步骤圆点刚好在每个Item的分界线上, 虽然可以在每个Item布局里画上一条线和半个圆做到, 始尾结点则用完整的圆形, 但是步骤圆点下方的步骤描述文本可就没办法了, 所以决定使用自定义View来实现.
在这里插入图片描述

准备

首先使用一个列表存储步骤文本

/**
 * 步骤视图
 * ```
 * author: zcp
 * created on: 2022/3/15 17:24
 * ```
 */
class StepView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

	private val stepTitleList = mutableListOf<String>()

    /**
     * 当前步数
     */
    var stepIndex = 0
        set(value) {
            field = value
            invalidate()
        }

    /**
     * 添加步骤列表
     * @param stepTitleList List<String>
     */
    fun setList(stepTitleList: List<String>) {
        this.stepTitleList.clear()
        this.stepTitleList.addAll(stepTitleList)
        invalidate()
    }
}

测量

自定义View的第一步就是测量啦, 我们只需要处理wrap_content的情况就行, 如果不处理的话wrap_content的效果就和match_parent一样了.

造成这个原因是:
子View中onMeasure(widthMeasureSpec, heightMeasureSpec)方法里的宽和高测量规格是在其父母ViewGroup中生成的, 默认match_parentwrap_content获取到的大小是一样的, 具体代码可以查看ViewGroup类里的getChildMeasureSpec()方法.

如下代码, 我们暂时只需要处理当高为wrap_content时, 提供一个默认高度就行, 而宽度则让它match_parentwrap_content一样. 这样, 自定义View的测量就简单的完成了.

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
    val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)

    if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, 44.dp)
    } else {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }
}

布局

onLayout()方法用于ViewGroup给子View布局, 但是我们这个并不是一个ViewGroup, 所以无需做任何操作, View类里的onLayout()本身就是一个空方法:
onLayout()

绘制

最后一步就是绘制出来了, 代码逻辑就是根据输入的步骤数量算出每段的长度, 根据stepIndex步进数决定使用的颜色, 和找到绘制需要的x, y点, 使用canvas就能画出来了

View下方的描述文本就有点麻烦, 因为每段步骤的长度是固定的, 如果描述文本超长了, 这时候还使用canvas.drawText()方法的话需要手动再次计算换行的位置, 过于繁琐, 所以可以用StaticLayout类来做, 这个类自动换行文本, 在TextView的内部也有它的身影噢.

绘制的代码有点长, 就不一行行解释了, 知道了思路就行, 其实就是简单的加减乘除计算位置的数学来的.

// ...
override fun onDraw(canvas: Canvas) {
	if(stepTitleList.size == 0) {
	    return
	}
	val interval = width / (stepTitleList.size - 1)
	stepTitleList.forEachIndexed { index, stepTitle ->
	    // 画圆
	    if (stepIndex > index) {
	        paint.style = Paint.Style.STROKE
	        paint.strokeWidth = lineWidth
	    } else {
	        paint.style = Paint.Style.FILL
	    }
	    paint.color = if (stepIndex >= index) highlightColor else normalColor
	
	    val cx = when (index) {
	        0 -> semicircleWidth + lineWidth
	        stepTitleList.lastIndex -> interval * index - (semicircleWidth + lineWidth)
	        else -> interval.toFloat() * index
	    }
	    canvas.drawCircle(cx, drawTop, semicircleWidth, paint)
	
	    // 画线
	    paint.color = if (stepIndex > index) highlightColor else normalColor
	    paint.strokeCap = Paint.Cap.BUTT
	    paint.strokeWidth = lineWidth
	
	    val starX: Float
	    val stopX: Float
	    when (index) {
	        0 -> {
	            starX = circleWidth + (lineWidth * 2) + circleLinePadding
	            stopX = interval - (semicircleWidth + lineWidth + circleLinePadding)
	        }
	        stepTitleList.lastIndex - 1 -> {
	            starX = interval * index + semicircleWidth + lineWidth + circleLinePadding
	            stopX = interval * (index + 1) - (circleWidth + (lineWidth * 2) + circleLinePadding)
	        }
	        stepTitleList.lastIndex -> {
	            starX = -1f
	            stopX = -1f
	        }
	        else -> {
	            starX = interval * index + semicircleWidth + lineWidth + circleLinePadding
	            stopX = interval * (index + 1) - (semicircleWidth + lineWidth + circleLinePadding)
	        }
	    }
	    if (starX > 0 || stopX > 0) {
	        canvas.drawLine(starX, drawTop, stopX, drawTop, paint)
	    }
	
	    // 绘制文字
	    textPaint.color = "#CDD2D8".toColorInt()
	    textPaint.textSize = 10.dp.toFloat()
	
	    val alignment = when (index) {
	        0 -> Layout.Alignment.ALIGN_NORMAL
	        stepTitleList.lastIndex -> Layout.Alignment.ALIGN_OPPOSITE
	        else -> Layout.Alignment.ALIGN_CENTER
	    }
	    val staticLayout = StaticLayout(
	        stepTitle, textPaint, interval,
	        alignment, 1f, 0f, false
	    )
	
	    val dy = drawTop + semicircleWidth + lineWidth + textPadding
	    val dx = when (index) {
	        0 -> 0f
	        stepTitleList.lastIndex -> interval.toFloat() * index - staticLayout.width
	        else -> interval.toFloat() * index - staticLayout.width / 2
	    }
	
	    canvas.save()
	    canvas.translate(dx, dy)
	    staticLayout.draw(canvas)
	    canvas.restore()
	}
}

源码

代码已上传github.com/mainxml/StepView
代码中有个注释起来的init方法块, 把注释解开就可以在Android Studio里预览了, 记得这是测试代码噢, 正式使用要注释掉的
♥

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值