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

前言先上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://www.ctvol.com/addevelopment/896406.html

(0)
上一篇 2021年10月22日
下一篇 2021年10月22日

精彩推荐