欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2022-04-18 09:59:30
前言先上UI图,好久没有写过自定义控件了,好多api都忘记了。写票文章记录一下写这个控件时用到的知识点。参考UI,我得出的需要绘制的图像有3个刻度带阴影的背景渐变色的进度展示流程与思考1、首先新建 class继承自View 文件class CloudRecordCircleProgress @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)...

前言

先上UI图,好久没有写过自定义控件了,好多api都忘记了。写票文章记录一下写这个控件时用到的知识点。代码在最下面。
android一个简单圆形进度条编写(知识点拾遗)
参考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一个简单圆形进度条编写(知识点拾遗)
第一条线如下
android一个简单圆形进度条编写(知识点拾遗)

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()
    }
}

本文地址:https://blog.csdn.net/u013728021/article/details/107266622

相关标签: Android自定义view