手把手教你实现一个炫酷的环形进度条。

支持纯色/渐变,可选线条样式,动画时长。

可实现扇形进度条、多个进度叠加、最小/最大进度等。

先上个效果图: Demo点我

实现思路

环形进度条,本质就是根据角度、半径等条件实现画圆的过程

  1. 创建一个类继承View,并实现几个构造方法
  2. 定义样式属性,获取属性值
  3. 创建画笔,设置线条等样式
  4. 在onDraw方法中进行绘制
  • 定义的属性样式

    <declare-styleable name="RingProgressView">
      <!--前景环形是否使用渐变-->
      <attr format="boolean" name="f_useGradient"/>
      <!--前景环形渐变起始颜色-->
      <attr format="color" name="f_startColor"/>
      <!--前景环形渐变中间颜色-->
      <attr format="color" name="f_centerColor"/>
      <!--前景环形渐变结束颜色-->
      <attr format="color" name="f_endColor"/>
      <!--前景环形颜色-->
      <attr format="color" name="f_ringColor"/>
      <!--背景环形颜色-->
      <attr format="color" name="f_ringBgColor"/>
      <!--前景环形起始角度-->
      <attr format="float" name="f_startAngle"/>
      <!--前景环形结束角度-->
      <attr format="float" name="f_endAngle"/>
      <!--背景环形起始角度-->
      <attr format="float" name="f_startBgAngle"/>
      <!--背景环形结束角度-->
      <attr format="float" name="f_endBgAngle"/>
      <!--环形线宽-->
      <attr format="dimension" name="f_roundWidth"/>
      <!--进度变化动画时长-->
      <attr format="integer" name="f_duration"/>
      <!--前景环形开始进度-->
      <attr format="float" name="f_startProgress"/>
      <!--前景环形结束进度-->
      <attr format="float" name="f_endProgress"/>
      <!--前景环形目标进度(你想设置的)-->
      <attr format="float" name="f_progress"/>
      <!--环形线帽样式Paint.Cap-->
      <attr format="enum" name="f_strokeCap">
          <!--默认没有-->
          <enum name="butt" value="0"/>
          <!--圆角-->
          <enum name="round" value="1"/>
          <!--直角-->
          <enum name="square" value="2"/>
      </attr>
      <!--环形描边样式,详见Paint.Join-->
      <attr format="enum" name="f_strokeJoin">
          <enum name="miter" value="0"/>
          <enum name="round" value="1"/>
          <enum name="bevel" value="2"/>
      </attr>
    </declare-styleable>
    
  • 定义的属性,及初始化

    xml属性的读取,变量含义可参考上面的属性定义

    //属性配置
    val ta = context.obtainStyledAttributes(attrs, R.styleable.RingProgressView)
    ringColor = ta.getColor(R.styleable.RingProgressView_f_ringColor, ringColor)
    ringBgColor = ta.getColor(R.styleable.RingProgressView_f_ringBgColor, ringBgColor)
    startColor = ta.getColor(R.styleable.RingProgressView_f_startColor, startColor)
    centerColor = ta.getColor(R.styleable.RingProgressView_f_centerColor, centerColor)
    endColor = ta.getColor(R.styleable.RingProgressView_f_endColor, endColor)
    //渐变色设置检查
    if (startColor != Color.TRANSPARENT || startColor != Color.TRANSPARENT || startColor != Color.TRANSPARENT) {
        useGradient = true
    }
    useGradient = ta.getBoolean(R.styleable.RingProgressView_f_useGradient, useGradient)
    startAngle = ta.getFloat(R.styleable.RingProgressView_f_startAngle, startAngle)
    endAngle = ta.getFloat(R.styleable.RingProgressView_f_endAngle, endAngle)
    startBgAngle = ta.getFloat(R.styleable.RingProgressView_f_startBgAngle, startBgAngle)
    endBgAngle = ta.getFloat(R.styleable.RingProgressView_f_endBgAngle, endBgAngle)
    roundWidth = ta.getDimensionPixelSize(R.styleable.RingProgressView_f_roundWidth, roundWidth)
    strokeCap = when (ta.getInteger(R.styleable.RingProgressView_f_strokeCap, 0)) {
        0 -> Paint.Cap.BUTT
        1 -> Paint.Cap.ROUND
        else -> Paint.Cap.SQUARE
    }
    strokeJoin = when (ta.getInteger(R.styleable.RingProgressView_f_strokeJoin, 0)) {
        0 -> Paint.Join.MITER
        1 -> Paint.Join.ROUND
        else -> Paint.Join.BEVEL
    }
    duration = ta.getInt(R.styleable.RingProgressView_f_duration, duration.toInt()).toLong()
    startProgress = ta.getFloat(R.styleable.RingProgressView_f_startProgress, startProgress)
    endProgress = ta.getFloat(R.styleable.RingProgressView_f_endProgress, endProgress)
    progress = ta.getFloat(R.styleable.RingProgressView_f_progress, progress)
    tempProgress = progress
    ta.recycle()
    
  • 动画插值器及画笔

    这里使用的是线性插值器(LinearInterpolator),插值器是控制动画过程的关键,也有其他可代替的方案有Scroller、ValueAnimator等

    有关插值器等了解看文章最后的参考部分

    init {	
            va.interpolator = LinearInterpolator()
            va.setFloatValues(startProgress, progress)
            va.duration = duration
            va.addUpdateListener {
                tempProgress = it.animatedValue as Float
                postInvalidate()
            }
            va.start()
        }
    
    /**
     * 初始化参数
     */
    private fun init() {
        //设置画笔参数
        bgPaint.strokeWidth = roundWidth.toFloat()
        bgPaint.strokeCap = strokeCap
        bgPaint.strokeJoin = Paint.Join.ROUND
        bgPaint.style = Paint.Style.STROKE
        bgPaint.isAntiAlias = true
        bgPaint.color = ringBgColor
        forePaint.strokeWidth = roundWidth.toFloat()
        forePaint.strokeCap = strokeCap
        forePaint.strokeJoin = Paint.Join.ROUND
        forePaint.style = Paint.Style.STROKE
        forePaint.isAntiAlias = true
        //着色器颜色筛选
        val filter = intArrayOf(startColor, centerColor, endColor).filter { it != Color.TRANSPARENT }
        val colorArray:IntArray = when{
            filter.isEmpty()-> intArrayOf(startColor, centerColor, endColor)
            filter.size>1-> filter.toIntArray()
            else->{
                intArrayOf(filter.first(),filter.first())
            }
        }
        //着色器/颜色设置
        if (useGradient) {
            forePaint.shader = LinearGradient(
                0f,
                0f,
                width.toFloat(),
                height.toFloat(),
                colorArray,
                null,
                Shader.TileMode.CLAMP
            )
        } else {
            forePaint.shader = null
            forePaint.color = ringColor
        }
        //矩形
        rectF
        .set(
            0f + roundWidth / 2,
            0f + roundWidth / 2,
            width.toFloat() - roundWidth / 2,
            height.toFloat() - roundWidth / 2
        )
        if (init) {
            postInvalidate()
        }
    }
    
  • 绘制部分

    override fun onDraw(canvas: Canvas) {
        //画背景环形
        canvas.drawArc(rectF, startBgAngle, endBgAngle - startBgAngle, false, bgPaint)
        //计算前景环形角度
        sweepAngle = tempProgress / (endProgress - startProgress) * (endAngle - startAngle)
        //画前景环形
        canvas.drawArc(rectF, startAngle, sweepAngle, false, forePaint)
    }
    

总结

  • 动画的实现主要依赖于插值器的配合
  • 渐变的功能实现在于着色器(Paint.shader及LinearGradient)的应用

参考

Interpolator插值器

Scroller

ValueAnimator