前言
先上UI图,好久没有写过自定义控件了,好多api都忘记了。写票文章记录一下写这个控件时用到的知识点。代码在最下面。
参考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() } }
可以理解为 红色坐标系–>平移到中心点–>黑色坐标系–>旋转一定角度–>蓝色坐标系
第一条线如下
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