android一个简单圆形进度条编写(知识点拾遗)

前言

先上UI图,好久没有写过自定义控件了,好多api都忘记了。写票文章记录一下写这个控件时用到的知识点。代码在最下面。
android一个简单圆形进度条编写(知识点拾遗)_第1张图片
参考UI,我得出的需要绘制的图像有3个

  • 刻度
  • 带阴影的背景
  • 渐变色的进度展示

流程与思考

1、首先新建 class继承自View 文件(kotlin代码)
class CloudRecordCircleProgress @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
}

这里注意 @JvmOverloads 注解,编译后可以自动生成 CloudRecordCircleProgress 类对应的三个构造方法,是对如下代码的简化。

class CloudRecordCircleProgress : View{
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
}

再提一个需要注意的地方,如果父类是 AppCompatEditText 这种控件时,特点是第二个构造方法包含如下默认的style样式:

    public AppCompatEditText(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.editTextStyle);
    }

那么在初始化时,应当这样定义,注意 defStyleAttr:Int = R.attr.editTextStyle 默认值。

class CustomCompatEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.editTextStyle)
    : AppCompatEditText(context, attrs, defStyleAttr) {
}
2、接着处理 initAttrs

先简略的写一下,具体代码后面会给出,这块没啥好说的,需要什么就添加什么

    <declare-styleable name="CloudRecordCircleProgress">
        <attr name="sweepAngle" format="integer" />
        ...
    declare-styleable>
    private fun initAttrs(attrs: AttributeSet) {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CloudRecordCircleProgress)
        ...
        typedArray.recycle()
    }
    //在init中调用
	init{
    	 attrs?.let {
            initAttrs(it)
        }
    }
3、再做一些初始化的操作,如Paint,RectF等数据,老生常谈没啥好说的。
	private val mMarkPaint
	init{
    	 attrs?.let {
            initAttrs(it)
        }
       mMarkPaint = Paint()
    }

当然也可以直接在声明处直接赋值

private var mMarkPaint = Paint()
4、size的处理,直接看代码即可

不了解 MeasureSpec 的同学可以参考:Android自定义View:MeasureSpec的真正意义与View大小控制

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val width = measureView(widthMeasureSpec, dipToPx(200f))
        val height = measureView(heightMeasureSpec, dipToPx(200f))
        //以最小值为正方形的长
        val defaultSize = Math.min(width, height)
        setMeasuredDimension(defaultSize, defaultSize)
    }
    
    private fun measureView(measureSpec: Int, defaultSize: Int): Int {
        var result = defaultSize
        val specMode = MeasureSpec.getMode(measureSpec)
        val specSize = MeasureSpec.getSize(measureSpec)
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize
        } else if (specMode == MeasureSpec.AT_MOST) {
            result = min(result, specSize)
        }
        return result
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //求最小值作为实际值
        val minSize = min(w - paddingLeft - paddingRight,
                h - paddingTop - paddingBottom)
        mRadius = (minSize shr 1.toFloat().toInt()).toFloat()
        val mArcRadius = mRadius - mMarkWidth - mMarkDistance - mArcWidth
        mArcRect.top = h.toFloat() / 2 - mArcRadius
        mArcRect.left = w.toFloat() / 2 - mArcRadius
        mArcRect.bottom = h.toFloat() / 2 + mArcRadius
        mArcRect.right = w.toFloat() / 2 + mArcRadius
    }
5、刻度的绘制

注意 save 和 restore 方法的对应。
translate 和 rotate 方法是针对画布中matrix操作的(可以理解为向量)

    private fun drawMark(canvas: Canvas) {
        canvas.apply {
            save()
            translate(mRadius, mRadius)//坐标系平移到中心点
            rotate(mMarkCanvasRotate.toFloat())//旋转一个起始角度
            for (i in 0 until mMarkCount) {
                drawLine(0f, mRadius, 0f, mRadius - mMarkWidth, mMarkPaint)
                rotate(mMarkDividedDegree)
            }
            restore()
        }
    }

可以理解为 红色坐标系–>平移到中心点–>黑色坐标系–>旋转一定角度–>蓝色坐标系
android一个简单圆形进度条编写(知识点拾遗)_第2张图片
第一条线如下
android一个简单圆形进度条编写(知识点拾遗)_第3张图片

6、背景圆弧的绘制

画笔设置阴影

paint.setShadowLayer(8f, 0f, 6f, Color.parseColor("#CC000000"))

画背景圆弧(注意:画笔的中心点和矩形重合,所以要注意一下画笔的 strokeWidth ,防止像上图中粉色矩形内部的圆弧被矩形切割。)

    private fun drawBgArc(canvas: Canvas) {
        canvas.drawArc(mArcRect, mArcBgStartDegree, mSweepAngle.toFloat(), false, mBgArcPaint)
    }
7、属性动画和进度

属性动画顾名思义就是可以把动画作用到view的属性上,也没啥好说的。

贴下代码:注意在 View#onDetachedFromWindow() 方法中取消一下,防止内存泄漏

 	private fun startAnimator(start: Float, end: Float, animTime: Long) {
        mAnimator.apply {
            cancel()
            setFloatValues(start, end)
            duration = animTime
            addUpdateListener { animation ->
                mProgress = animation.animatedValue as Float
                invalidate()
            }
            start()
        }
    }

    fun reset() {
        startAnimator(mProgress, 0.0f, 1000L)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        mAnimator.cancel()
    }

最终代码

最终代码如下,有啥问题请留言。代码没啥亮点,只是简单的记录一下知识点。

    <declare-styleable name="CloudRecordCircleProgress">
        <attr name="sweepAngle" format="integer" />
        <attr name="animTime" format="integer" />
        <attr name="arcWidth" format="dimension" />
        <attr name="bgArcColor" format="color|reference" />
        <attr name="arcStartColor" format="color|reference" />
        <attr name="arcEndColor" format="color|reference" />
        <attr name="markColor" format="color|reference" />
        <attr name="markWidth" format="dimension" />
        <attr name="dottedLineCount" format="integer" />
        <attr name="lineDistance" format="dimension" />
    declare-styleable>
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import androidx.annotation.FloatRange
import com.gas.app.R
import kotlin.math.min


class CloudRecordCircleProgress @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    private var mArcRect = RectF()
    private var mRadius: Float = 0F//总半径 = 0f
    private var mSweepAngle = 0 //总角度 = 0

    //刻度
    private var mMarkPaint = Paint()
    private var mMarkCount = 20 // 刻度数
    private var mMarkColor = Color.BLACK //刻度颜色 = 0
    private var mMarkWidth = dipToPx(4F).toFloat()  //刻度长度 = 0f
    private var mMarkDistance = dipToPx(5F).toFloat()//刻度到线的间距 = 0f

    //绘制圆弧背景
    private var mBgArcPaint = Paint()
    private var mBgArcColor = 0
    private var mArcWidth = 0f

    //圆弧进度
    private var mArcPaint = Paint()
    private var mArcStartColor = 0
    private var mArcEndColor = 0
    private var mAnimator = ValueAnimator()
    private var mAnimTime: Long = 0
    private var mProgress = 0f
    private var mMarkCanvasRotate = 0
    private var mMarkDividedDegree = 0f
    private var mArcBgStartDegree = 0f
    private var mArcStartDegree = 0f
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val width = measureView(widthMeasureSpec, dipToPx(200f))
        val height = measureView(heightMeasureSpec, dipToPx(200f))
        //以最小值为正方形的长
        val defaultSize = Math.min(width, height)
        setMeasuredDimension(defaultSize, defaultSize)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //求最小值作为实际值
        val minSize = min(w - paddingLeft - paddingRight,
                h - paddingTop - paddingBottom)
        mRadius = (minSize shr 1.toFloat().toInt()).toFloat()
        val mArcRadius = mRadius - mMarkWidth - mMarkDistance - mArcWidth
        mArcRect.top = h.toFloat() / 2 - mArcRadius
        mArcRect.left = w.toFloat() / 2 - mArcRadius
        mArcRect.bottom = h.toFloat() / 2 + mArcRadius
        mArcRect.right = w.toFloat() / 2 + mArcRadius
    }

    private fun initAttrs(attrs: AttributeSet) {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CloudRecordCircleProgress)
        mSweepAngle = typedArray.getInt(R.styleable.CloudRecordCircleProgress_sweepAngle, 240)
        mMarkColor = typedArray.getColor(R.styleable.CloudRecordCircleProgress_markColor, Color.WHITE)
        mMarkCount = typedArray.getInteger(R.styleable.CloudRecordCircleProgress_dottedLineCount, mMarkCount)
        mMarkWidth = typedArray.getDimension(R.styleable.CloudRecordCircleProgress_markWidth, 4f)
        mMarkDistance = typedArray.getDimension(R.styleable.CloudRecordCircleProgress_lineDistance, 4f)
        mArcStartColor = typedArray.getColor(R.styleable.CloudRecordCircleProgress_arcStartColor, Color.RED)
        mArcEndColor = typedArray.getColor(R.styleable.CloudRecordCircleProgress_arcEndColor, Color.RED)
        mBgArcColor = typedArray.getColor(R.styleable.CloudRecordCircleProgress_bgArcColor, Color.RED)
        mArcWidth = typedArray.getDimension(R.styleable.CloudRecordCircleProgress_arcWidth, 15f)
        mAnimTime = typedArray.getInt(R.styleable.CloudRecordCircleProgress_animTime, 700).toLong()
        typedArray.recycle()
    }

    private fun initPaint() {
        mMarkPaint.apply {
            isAntiAlias = true
            color = mMarkColor
            style = Paint.Style.STROKE
            strokeWidth = dipToPx(1f).toFloat()
            strokeCap = Paint.Cap.ROUND
        }

        mBgArcPaint.apply {
            isAntiAlias = true
            color = mBgArcColor
            style = Paint.Style.STROKE
            setShadowLayer(8f, 0f, 6f, Color.parseColor("#CC000000"))
            strokeWidth = mArcWidth
            strokeCap = Paint.Cap.ROUND
        }

        mArcPaint.apply {
            isAntiAlias = true
            style = Paint.Style.STROKE
            strokeWidth = mArcWidth
            strokeCap = Paint.Cap.ROUND
        }

    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        setLayerType(LAYER_TYPE_SOFTWARE, null)
        //刻度
        drawMark(canvas)
        //背景圆弧
        drawBgArc(canvas)
        //进度
        drawProgressArc(canvas)
    }

    private fun drawMark(canvas: Canvas) {
        canvas.apply {
            save()
            translate(mRadius, mRadius)
            rotate(mMarkCanvasRotate.toFloat())
            for (i in 0 until mMarkCount) {
                drawLine(0f, mRadius, 0f, mRadius - mMarkWidth, mMarkPaint)
                rotate(mMarkDividedDegree)
            }
            restore()
        }

    }

    private fun drawBgArc(canvas: Canvas) {
        canvas.drawArc(mArcRect, mArcBgStartDegree, mSweepAngle.toFloat(), false, mBgArcPaint)
    }

    private fun drawProgressArc(canvas: Canvas) { //mSweepAngle - 50
        mArcPaint.shader = LinearGradient(
                mArcRect.left, mArcRect.top, mArcRect.right, mArcRect.top, mArcStartColor, mArcEndColor,
                Shader.TileMode.MIRROR)
        canvas.drawArc(mArcRect, mArcStartDegree, -(mSweepAngle * mProgress / MAX_PROGRESS), false, mArcPaint)
    }

    fun setProgress(@FloatRange(from = 0.0, to = 100.0) value: Float) {
        var endValue = value
        if (value >= MAX_PROGRESS) {
            endValue = MAX_PROGRESS.toFloat()
        }
        if (value <= 0) {
            endValue = 0F
        }
        startAnimator(0f, endValue, mAnimTime)
    }

    private fun startAnimator(start: Float, end: Float, animTime: Long) {
        mAnimator.apply {
            cancel()
            setFloatValues(start, end)
            duration = animTime
            addUpdateListener { animation ->
                mProgress = animation.animatedValue as Float
                invalidate()
            }
            start()
        }
    }

    fun reset() {
        startAnimator(mProgress, 0.0f, 1000L)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        mAnimator.cancel()
    }

    private fun dipToPx(dip: Float): Int {
        val density = context.applicationContext.resources.displayMetrics.density
        return (dip * density + 0.5f * if (dip >= 0) 1 else -1).toInt()
    }

    private fun measureView(measureSpec: Int, defaultSize: Int): Int {
        var result = defaultSize
        val specMode = MeasureSpec.getMode(measureSpec)
        val specSize = MeasureSpec.getSize(measureSpec)
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize
        } else if (specMode == MeasureSpec.AT_MOST) {
            result = min(result, specSize)
        }
        return result
    }

    companion object {
        private const val CIRCLE_DEGREE = 360
        private const val RIGHT_ANGLE_DEGREE = 90
        private const val MAX_PROGRESS = 100
    }

    init {
        attrs?.let {
            initAttrs(it)
        }
        initPaint()
        mMarkCanvasRotate = CIRCLE_DEGREE - mSweepAngle shr 1
        mMarkDividedDegree = mSweepAngle / (mMarkCount - 1).toFloat()
        mArcBgStartDegree = RIGHT_ANGLE_DEGREE + (CIRCLE_DEGREE - mSweepAngle shr 1).toFloat()
        mArcStartDegree = RIGHT_ANGLE_DEGREE - (CIRCLE_DEGREE - mSweepAngle shr 1).toFloat()
    }
}

你可能感兴趣的:(Android自定义view)