渐变圆环进度条

1,142 阅读2分钟

渐变圆环效果图:

progress.png

这个图有点模糊,我是截取的 android studio 中的预览图

开发中遇到的问题:

当 SweepGradient 遇上 Paint.strokeCap = Paint.Cap.ROUND 时,在加上渐变会出现奇怪的现象
Paint.Cap.BUTT:   是默认的,不会多出任何东西
Paint.Cap.ROUND:  圆头,多出半个圆
Paint.Cap.SQUARE: 平头,多出半个方形

因为我们需要Paint.strokeCap = Paint.Cap.ROUND,保证进度条是圆头的:
    不是渐变的圆环,那还好可以正常显示;
    是渐变的圆环,会出现首尾重叠,并且尾部圆头会覆盖在首部,及其丑陋。

如果解决该问题呢?
    那就是计算首尾两个半圆头所占的弧度,减去即可

偏移角度算法如下图:

角度算法.png

源码如下

class GradientAnnulusProgress @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    /**
     * 代表当前进度
     */
    @FloatRange(from = 0.0, to = 1.0)
    private var currentProgress = 0.75f
        set(value) {
            field = when {
                value > maxProgress -> maxProgress
                value < 0 -> 0f
                else -> value
            }
            postInvalidate()
        }

    /**
     * 最大的进度,默认为 1f  如果设置为 0.5f,那么圆环进度最多只会走整体的一半
     */
    @FloatRange(from = 0.0, to = 1.0)
    private var maxProgress = 1f

    /**
     * 开始的角度, 默认为 0   比如说 圆环的起点为 135,那么圆环的起点就会从x轴正方向顺时针旋转135
     */
    @FloatRange(from = 0.0, to = 360.0)
    private var startAngle = 0f

    /**
     * SweepGradient 的需要的颜色值
     */
    @ColorInt
    var colorsArray: IntArray = intArrayOf(0xFFBB86FC.toInt(), 0xFF03DAC5.toInt())

    @Nullable
    var positionsArray: FloatArray? = floatArrayOf(0f, maxProgress)
    private var sweepGradient: SweepGradient? = null

    /**
     * view 的中心的 位置,在 onSizeChanged 中赋值
     */
    private var center = 0f

    /**
     * 圆环的宽度
     */
    @Px
    private var progressWidth = 100

    /**
     * 圆环的边界
     */
    private val progressBounds = RectF()

    /**
     * 绘制圆环进度的画笔
     */
    private val progressPaint = Paint().apply {
        isAntiAlias = true
        style = Paint.Style.STROKE
        strokeWidth = progressWidth.toFloat()
        strokeCap = Paint.Cap.ROUND
    }

    /**
     * 圆环的背景
     */
    @ColorInt
    var bgColor: Int = Color.LTGRAY
    private val bgPaint = Paint().apply {
        color = bgColor
        isAntiAlias = true
        style = Paint.Style.STROKE
        strokeWidth = progressWidth.toFloat()
        strokeCap = Paint.Cap.ROUND
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        sweepGradient = SweepGradient(
            0f, 0f, colorsArray, positionsArray
        )
        progressPaint.shader = sweepGradient
        center = width / 2f
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val widthSize = max(MeasureSpec.getSize(widthMeasureSpec), (progressWidth * 5))
        val heightSize = max(MeasureSpec.getSize(heightMeasureSpec), (progressWidth * 5))
        val size = min(widthSize, heightSize)
        setMeasuredDimension(
            resolveSize(size, widthMeasureSpec), resolveSize(size, widthMeasureSpec)
        )
    }

    override fun onDraw(canvas: Canvas) {
        // 将画布移动的 view 中心点
        canvas.translate(center, center)
        // 圆环的半径   view 的 一半 减去  画笔的一半
        val progressRadius = center - progressWidth / 2f

        // Paint.Cap.ROUND 这种模式会导致起点和终点多出个圆头(起点圆头的颜色是终点的颜色值,所以显示很奇怪),
        // 那么就要计算这个圆头所占的弧度,在绘制扇形的时候,起始点设置为该值
        val fixStartAngle =
            Math.toDegrees(asin(progressWidth / 2f / (progressRadius)).toDouble()).toFloat()
        // 根据进度计算当前的弧度, 经过观察,终点圆头占据的弧度是 fixStartAngle 的两倍,所以要减去这个值
        // 那么随之带来的问题就是: currentProgress * 360 - fixStartAngle * 2 有可能小于 0,
        // 必须要保证 currentAngle 大于等于 0,就需要 max 一下
        val currentAngle = max(currentProgress * 360 - fixStartAngle * 2, 0f)

        canvas.rotate(startAngle)

        progressBounds.set(
            -progressRadius, -progressRadius, progressRadius, progressRadius
        )

        canvas.drawArc(
            progressBounds, fixStartAngle, 360 - fixStartAngle * 2, false, bgPaint
        )

        canvas.drawArc(
            progressBounds, fixStartAngle, currentAngle, false, progressPaint
        )
    }
}