先上需求
需要实现一个扇形进度条和两块文字以及进度条下方带渐变色的图层和虚线(中间图层可以自行绘制也可以用切图),使用了kotlin来编写,一些基础用法应该对使用java的不难看懂
开始绘制
1.新建一个kotlin类继承View,并创建自定义view相关构造方法(这里省略),首先定义需要用到且可以自行设置的相关数据
//这是整个扇形的半径
var radius = SizeUtils.dp2px(100f).toFloat()
//扇形最后展示的进度
private var progress = 0f
//进度条的宽度
var progressStorkPx = SizeUtils.dp2px(14f).toFloat()
//中间文本颜色
var centerTextColor = ContextCompat.getColor(context, R.color.dash_board_progress)
//中部文本大小
var centerTextSizeSp = 50f
//底部文本颜色
var bottomTextColor = ContextCompat.getColor(context, R.color.font_dark_light)
//底部文本大小
var bottomTextSizeSp = 20f
//底部显示文本,为空字符串则不显示
var bottomText = ""
//进度条最大值
var max = 0f
//绘制进度条等需要用到的矩形测量区域
var rect = RectF()
//画笔
var paint = Paint(Paint.ANTI_ALIAS_FLAG)
//控制进度条变化的动画
var animator = ObjectAnimator()
2.进度条的动画控制方法
/**
* 调用这个方法来设置动态的进度条动画
* @param progress 0-max之间
*/
fun setAnimaion(progress: Float) {
//ObjectAnimator.ofFloat方法可以学习一下,第二个参数是当前控制的变量名
//第三个参数0,第四个参数progress,方法表示当前变量名为progress的变量在
//动画持续时间内逐步从0增加到设置的progress,会不停的调用setProgress方法
animator = ObjectAnimator.ofFloat(this, "progress", 0f, progress)
//动画持续时间
animator.duration = 1000
//动画的插值器(参考:https://blog.youkuaiyun.com/pzm1993/article/details/77926373)
animator.interpolator = FastOutLinearInInterpolator()
animator.start()
}
fun setProgress(progress: Float) {
this.progress = progress
//设置进度调用一次就刷新一次view(达到进度条逐步增加的效果)
invalidate()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
//页面被销毁需要终止动画
animator.end()
}
3.接下来先对控件的大小进行适配与设定,主要处理控件warp_content与match_parent的情况,这块就不细讲了,我个笨蛋也是摸石头过河,只懂个大概
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
//设置warp_content的默认宽高
val width = 300f
val height = 200f
//AT_MOST对应wrap_content;EXACTLY对应match_parent
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//都设置的wrap_content的话
setMeasuredDimension(SizeUtils.dp2px(width), SizeUtils.dp2px(height))
} else if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
//都设置的match_parent话
setMeasuredDimension(widthSize, heightSize)
} else if (widthMode == MeasureSpec.AT_MOST) {
//宽度为自适应
setMeasuredDimension(SizeUtils.dp2px(width), heightSize)
} else if (heightMode == MeasureSpec.AT_MOST) {
//高度为自适应
setMeasuredDimension(widthSize, SizeUtils.dp2px(height))
}
}
4.这就是最重要的一步了,进行view的绘制操作,画自定义对称图形中心点很重要,根据设计图我们可以看到,扇形是一个超180度小于360度的图形,中心点在中心偏下的位置,我们设定扇形底部没有的部分为120°,可见该对称图形横坐标的中心点一定是在控件中间,所以横坐标可直接=宽度/2。画了一幅图供理解
纵坐标可以看得为r(不过进度条还需要宽度,所以纵坐标该为r+progressStorkPx)
设半径为r,总高度 = r + h = r + r * cos(60°),由此可以推算出半径的长度
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//圆弧半径大小根据view所获得的高宽来进行自适应判定
//这里首先获取中心点
val centerX = width / 2.toFloat()
val centerY: Float =
//需要根据宽高来判断半径长度,所以就存在整体控件设置的宽高大小的问题,
//需要以最小值来画圆,才可以实现全部视图都在控件内部,而不会绘制到外部
if (width >= height) {
//宽度大于高度,就以高度来计算半径
var t = height / (1 + cos(Math.PI / 3).toFloat())
t = if (t >= centerX) centerX else t
radius = t - (progressStorkPx * 2)
radius + progressStorkPx
} else {
radius = centerX - progressStorkPx
radius + progressStorkPx
}
//画进度条下部扇形阴影区域(特殊需求)
//实现色彩渐变效果
// val colorsNormal = intArrayOf(ContextCompat.getColor(context, R.color.transparent), ContextCompat.getColor(context, R.color.live_bg_grey), ContextCompat.getColor(context, R.color.transparent))
// val shaderNormal: Shader = LinearGradient(centerX - radius + progressStorkPx, centerY + radius - progressStorkPx, centerX + radius - progressStorkPx, centerY + radius - progressStorkPx, colorsNormal, null, Shader.TileMode.CLAMP)
// paint.shader = shaderNormal
paint.style = Paint.Style.STROKE
paint.strokeCap = Paint.Cap.ROUND
paint.strokeWidth = progressStorkPx
// rect.set(centerX - radius + progressStorkPx, centerY - radius + progressStorkPx, centerX + radius - progressStorkPx, centerY + radius - progressStorkPx)
// canvas.drawArc(rect, 150f, 240f, false, paint)
//原自定义扇形阴影和虚线都取消替换成图片
var x=progressStorkPx/2
rect.set(centerX - radius + x, centerY - radius + x, centerX + radius - x, centerY + radius - (progressStorkPx*2))
canvas.drawBitmap(BitmapFactory.decodeResource(resources,R.mipmap.result_bj),null,rect,paint)
//画底部视图
paint.shader = null
paint.color = ContextCompat.getColor(context, R.color.dash_board_grey_shallow)
rect.set((centerX - radius), (centerY - radius), (centerX + radius), (centerY + radius))
//这个方法的第二第三个参数分别表示圆弧的起始角度、圆弧的总角度
canvas.drawArc(rect, 150f, 240f, false, paint)
//画覆盖区域
paint.color = ContextCompat.getColor(context, R.color.dash_board_progress)
canvas.drawArc(rect, 150f, (progress / max) * 240f, false, paint)
//画进度条下部扇形阴影内虚线
//色彩渐变效果
// val colorsDotLine = intArrayOf(ContextCompat.getColor(context, R.color.transparent), ContextCompat.getColor(context, R.color.dash_board_grey_shallow), ContextCompat.getColor(context, R.color.transparent))
// val shaderDotLine: Shader = LinearGradient(centerX - radius + progressStorkPx, centerY + radius - progressStorkPx, centerX + radius - progressStorkPx, centerY + radius - progressStorkPx, colorsDotLine, null, Shader.TileMode.CLAMP)
// paint.shader = shaderDotLine
// paint.strokeWidth = 0f
// paint.pathEffect = DashPathEffect(floatArrayOf(4f, 4f), 0f)
// rect.set(centerX - radius + progressStorkPx, centerY - radius + progressStorkPx, centerX + radius - progressStorkPx, centerY + radius - progressStorkPx)
// canvas.drawArc(rect, 150f, 240f, false, paint)
//画中部文本
paint.shader = null
paint.style = Paint.Style.FILL
paint.color = centerTextColor
paint.textSize = SizeUtils.sp2px(centerTextSizeSp).toFloat()
paint.typeface = Typeface.DEFAULT_BOLD
canvas.drawText(progress.toInt().toString(), centerX, centerY, paint)
//画底部文本
paint.color = bottomTextColor
paint.textSize = SizeUtils.sp2px(bottomTextSizeSp).toFloat()
paint.typeface = Typeface.DEFAULT
canvas.drawText(bottomText, centerX, centerY + (radius * cos(Math.PI / 3).toFloat()), paint)
}