Kotlin自定义圆形进度条

       实现一个圆形进度条,效果如下:

Kotlin自定义圆形进度条_第1张图片 

      自定义布局属性:



    
    
    
    
    
    
    
    
    
    
    
    
    
    

       CircleProgressBar实现如下:

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

    companion object {
        private val DEFAULT_MAX = 100
        private val DEFAULT_PROGRESS = 0
        private val DEFAULT_START_ANGLE = -90f
        private val DEFAULT_REVERSE = false
        private val DEFAULT_ROUND_CAP = true
        private val DEFAULT_RING_WIDTH = SizeUtil.dp2px(5f)
        private val DEFAULT_RING_COLOR = Color.parseColor("#0888FF")
        private val DEFAULT_RING_BACKGROUND_COLOR = Color.parseColor("#EFEFEF")
        private val DEFAULT_INNER_BACKGROUND_COLOR = Color.TRANSPARENT
        private val DEFAULT_DESC_TEXT = ""
        private val DEFAULT_DESC_TEXT_SIZE = SizeUtil.sp2px(12f)
        private val DEFAULT_DESC_TEXT_COLOR = Color.parseColor("#0888FF")
        private val DEFAULT_PERCENT_TEXT_SIZE = SizeUtil.sp2px(20f)
        private val DEFAULT_PERCENT_TEXT_COLOR = Color.parseColor("#0888FF")
    }

    //进度最大值
    var max = DEFAULT_MAX
    //当前进度
    var progress = DEFAULT_PROGRESS
        get() = field
        set(value) {
            field = correctProgress(value)
            invalidate()
        }
    //绘制开始的角度
    var startAngle = DEFAULT_START_ANGLE
    //进度条是否逆时针滚动,默认为false,即顺时针滚动
    var reverse = DEFAULT_REVERSE
    //是否设置画笔帽为Paint.Cap.ROUND,即圆形样式,默认为true
    var roundCap = DEFAULT_ROUND_CAP
    //圆环宽度
    var ringWidth = DEFAULT_RING_WIDTH
    //圆环颜色,若在代码中设置了rindColorArray则无效果
    var ringColor = DEFAULT_RING_COLOR
    //圆环渐变色,通过代码设置,实现渐变效果,优先级比ringColor高
    var rindColorArray: IntArray? = null
    //圆环背景色
    var ringBackgroungColor = DEFAULT_RING_BACKGROUND_COLOR
    //内部圆圈背景色
    var innerBackgroundColor = DEFAULT_INNER_BACKGROUND_COLOR
    //描述文本
    var descText = DEFAULT_DESC_TEXT
    //描述文本字体大小
    var descTextSize = DEFAULT_DESC_TEXT_SIZE
    //描述文本字体颜色
    var descTextColor = DEFAULT_DESC_TEXT_COLOR
    //进度文本字体大小
    var percentTextSize = DEFAULT_PERCENT_TEXT_SIZE
    //进度文本字体颜色
    var percentTextColor = DEFAULT_PERCENT_TEXT_COLOR

    //更新当前进度,带动画效果
    fun startAnim(progress: Int, duration: Long = 2000) {
        val percent = correctProgress(progress)
        val animator = ValueAnimator.ofInt(0, percent)
        animator.setDuration(duration)
        animator.addUpdateListener {
            this.progress = it.getAnimatedValue() as Int
        }
        animator.start()
    }

    private fun correctProgress(progress: Int): Int {
        var progress = progress
        if (progress > max) {  //处理错误输入progress大于max的情况
            if (progress % max == 0) {
                progress = max
            } else {
                progress %= max
            }
        }
        return progress
    }

    private lateinit var mArcPaint: Paint        //绘制圆环
    private lateinit var mBgPaint: Paint         //绘制内部圆圈
    private lateinit var mTextPaint: Paint       //绘制文字
    private lateinit var mRectF: RectF           //圆环对应的RectF
    private lateinit var mShader: SweepGradient  //实现圆环渐变效果,如果有在代码中设置rindColorArray的话

    private var mWidth: Int = 0
    private var mHeight: Int = 0

    init {
        attrs?.let {
            parseAttribute(getContext(), it)
        }
        initPaint()
    }

    //获取布局属性并设置属性默认值
    private fun parseAttribute(context: Context, attrs: AttributeSet) {
        val ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar)
        max = ta.getInt(R.styleable.CircleProgressBar_max, DEFAULT_MAX)
        if (max <= 0) {
            max = DEFAULT_MAX
        }
        startAngle = ta.getFloat(R.styleable.CircleProgressBar_startAngle, DEFAULT_START_ANGLE)
        reverse = ta.getBoolean(R.styleable.CircleProgressBar_reverse, DEFAULT_REVERSE)
        roundCap = ta.getBoolean(R.styleable.CircleProgressBar_roundCap, DEFAULT_ROUND_CAP)
        progress = ta.getInt(R.styleable.CircleProgressBar_progress, DEFAULT_PROGRESS)
        ringWidth = ta.getDimension(R.styleable.CircleProgressBar_ringWidth, DEFAULT_RING_WIDTH)
        ringColor = ta.getColor(R.styleable.CircleProgressBar_ringColor, DEFAULT_RING_COLOR)
        ringBackgroungColor = ta.getColor(
            R.styleable.CircleProgressBar_ringBackgroungColor,
            DEFAULT_RING_BACKGROUND_COLOR
        )
        innerBackgroundColor = ta.getColor(
            R.styleable.CircleProgressBar_innerBackgroundColor,
            DEFAULT_INNER_BACKGROUND_COLOR
        )
        descText = ta.getString(R.styleable.CircleProgressBar_descText) ?: DEFAULT_DESC_TEXT
        descTextSize =
            ta.getDimension(R.styleable.CircleProgressBar_descTextSize, DEFAULT_DESC_TEXT_SIZE)
        descTextColor =
            ta.getColor(R.styleable.CircleProgressBar_descTextColor, DEFAULT_DESC_TEXT_COLOR)
        percentTextSize = ta.getDimension(
            R.styleable.CircleProgressBar_percentTextSize,
            DEFAULT_PERCENT_TEXT_SIZE
        )
        percentTextColor =
            ta.getColor(R.styleable.CircleProgressBar_percentTextColor, DEFAULT_PERCENT_TEXT_COLOR)
        ta.recycle()
    }

    //初始化画笔
    private fun initPaint() {
        mArcPaint = Paint()
        mArcPaint.isAntiAlias = true
        mBgPaint = Paint()
        with(mBgPaint) {
            isAntiAlias = true
            color = innerBackgroundColor
        }
        mTextPaint = Paint()
        with(mTextPaint) {
            isAntiAlias = true
            textAlign = Paint.Align.CENTER
        }
    }

    //重写onMeasure支持wrap_content
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(measure(widthMeasureSpec), measure(heightMeasureSpec))
    }

    private fun measure(measureSpec: Int): Int {
        var result = 0
        val specMode = MeasureSpec.getMode(measureSpec)
        val specSize = MeasureSpec.getSize(measureSpec)
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize
        } else {
            result = SizeUtil.dp2px(90f).toInt()
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize)
            }
        }
        return result
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mWidth = measuredWidth
        mHeight = measuredHeight
        mRectF = RectF(
            ringWidth / 2.0f, ringWidth / 2.0f,
            mWidth - ringWidth / 2.0f, mHeight - ringWidth / 2.0f
        )
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.let {
            drawBackgroung(it)
            drawArc(it)
            drawText(it)
        }
    }

    //绘制背景
    private fun drawBackgroung(canvas: Canvas) {
        canvas.drawCircle(mRectF.centerX(), mRectF.centerY(), mWidth / 2.0f, mBgPaint)
    }

    //绘制圆环
    private fun drawArc(canvas: Canvas) {
        with(mArcPaint) {
            color = ringBackgroungColor
            style = Paint.Style.STROKE
            strokeWidth = ringWidth
            setShader(null)
        }
        //绘制圆环背景
        canvas.drawArc(mRectF, 0f, 360f, false, mArcPaint)
        with(mArcPaint) {
            color = ringColor
            //strokeCap取值:ROUND(圆形)、SQUARE(方形)和BUTT,默认是BUTT
            //SQUARE和BUTT显示效果一样,但是SQUARE会凸出一部分,和ROUND凸出的圆角一样
            if (roundCap) {
                strokeCap = Paint.Cap.ROUND
            }
        }
        //绘制圆环
        rindColorArray?.let {
            //绘制渐变效果
            mShader = SweepGradient(mRectF.centerX(), mRectF.centerY(), rindColorArray!!, null)
            val matrix = Matrix()
            matrix.setRotate(
                if (roundCap) calculateOffset() else startAngle,
                mRectF.centerX(),
                mRectF.centerY()
            )
            mShader.setLocalMatrix(matrix)
            mArcPaint.setShader(mShader)
        }
        var sweepAngle = progress.toFloat() / max * 360f
        if (reverse) {
            sweepAngle = -sweepAngle  //逆时针滚动
        }
        canvas.drawArc(mRectF, startAngle, sweepAngle, false, mArcPaint)
    }

    //设置渐变效果时让SweepGradient偏移一定角度来保证圆角凸出部分和圆环进度条颜色一样
    private fun calculateOffset(): Float {
        //计算strokeCap为Paint.Cap.ROUND时圆角凸出部分相当于整个圆环占的比例,半圆的直径 = 线的宽度
        val roundPercent =
            (Math.atan(ringWidth / 2.0 / (mWidth / 2.0 - ringWidth / 2.0)) / (2 * Math.PI)).toFloat()
        val curentPercent = progress / max.toFloat()  //当前进度
        if (curentPercent + roundPercent >= 1.0f) {
            return startAngle
        } else if (curentPercent + 2 * roundPercent >= 1.0f) {
//            mArcPaint.strokeCap = Paint.Cap.BUTT
            if (reverse) {
                return startAngle + (1 - curentPercent - roundPercent) * 360f
            } else {
                return startAngle - (1 - curentPercent - roundPercent) * 360f
            }
        } else {
            if (reverse) {
                return startAngle + roundPercent * 360f
            } else {
                return startAngle - roundPercent * 360f
            }
        }
    }

    //绘制文字
    private fun drawText(canvas: Canvas) {
        with(mTextPaint) {
            color = descTextColor
            textSize = descTextSize
        }
        //绘制描述文本
        canvas.drawText(descText, mRectF.centerX(), mHeight * 0.42f, mTextPaint)
        with(mTextPaint) {
            color = percentTextColor
            textSize = percentTextSize
        }
        //绘制进度文本
        canvas.drawText(
            "${(progress / max.toFloat() * 100).toInt()}%",
            mRectF.centerX(),
            mHeight * 0.69f,
            mTextPaint
        )
    }

}

       使用举例:

binding.circleCpb1.startAnim(75)
with(binding.circleCpb2) {
    rindColorArray = intArrayOf(
        Color.parseColor("#0888FF"),
        Color.parseColor("#6CD0FF")
    )
    startAnim(85)
}

       源码地址

你可能感兴趣的:(Android控件篇)