效果:
思路:
- 1、首先画一个圆弧;
- 2、画刻度,就是一个个小矩形,但是有方向。用PathDashPathEffect来画就很简单,把小矩形当做虚线路径;
- 3、画指针,就是一条线,起点是圆心,终点用三角函数计算
- 4、加一个pointerProgress属性,并且设置set方法,给属性动画使用,这样指针就可以做动画了
- ps:给圆弧和刻度的绘画过程也加了动画,只是为了更深刻的体会绘制的过程。
代码中各种注释都有
自定义view代码
package com.xc.test
import android.animation.Animator
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import com.xc.test.util.logE
import com.xc.test.util.px
import kotlin.math.cos
import kotlin.math.sin
/**
* @author: xuchun
* @time: 2021/4/1 - 16:20
* @desc: 仪表盘,支持指针转动动画
* 1、首先画一个圆弧;
* 2、画刻度,就是一个个小矩形,但是有方向。用PathDashPathEffect来画就很简单,把小矩形当做虚线路径;
* 3、画指针,就是一条线,起点是圆心,终点用三角函数计算
* 4、加一个pointerProgress属性,并且设置set方法,给属性动画使用,这样指针就可以做动画了
*
* ps:给圆弧和刻度的绘画过程也加了动画,只是为了更深刻的体会绘制的过程
*/
//仪表盘开口角度
const val OPEN_ANGLE = 90f
//仪表盘半径
val RADIUS = 100f.px
//刻度,即一个小矩形的宽高
val DASH_WIDTH = 2f.px
val DASH_LENGTH = 5f.px
//指针的长度
val POINTER_LENGTH = 80f.px
//刻度间隔数
const val DASH_COUNT = 20f
class DashboardView : 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
)
/**值区间:0.0 - DASH_COUNT*/
private var pointerProgress = 0f
set(value) {
field = value
invalidate()
}
/**值区间:0.0 - 100*/
private var arcProgress = 0f
set(value) {
field = value
invalidate()
}
/**值区间:0.0 - 100*/
private var dashProgress = 0f
set(value) {
field = value
invalidate()
}
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
//整体所在矩形
private lateinit var rectF: RectF
//表盘圆弧
private val tempArc = Path()
private val pathArc = Path()
//刻度的圆弧
private val pathDashArc = Path()
//刻度线小矩形,虽然叫做path,其实更多是用来画图形的
private val dash = Path()
//用来计算path长度
private lateinit var pathMeasure: PathMeasure
//路径效果定制器
private var pathDashPathEffect: PathDashPathEffect? = null
//指针角度 (用三角函数时需要把角度转成弧度)
var pointerAngle = (360 - OPEN_ANGLE) / 20f * pointerProgress + 90 + OPEN_ANGLE / 2
init {
//因为画笔颜色没变,这里就不用填充了,不然刻度和指针都看不见
paint.style = Paint.Style.STROKE
paint.strokeWidth = 3f.px
//构建刻度的小矩形
dash.addRect(0f, 0f, DASH_WIDTH, DASH_LENGTH, Path.Direction.CW)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
pathArc.reset()
tempArc.reset()
rectF =
RectF(width / 2 - RADIUS, height / 2 - RADIUS, width / 2 + RADIUS, height / 2 + RADIUS)
tempArc.addArc(rectF, 90 + OPEN_ANGLE / 2, (360 - OPEN_ANGLE))
//动画画圆弧
val objectAnimator = ObjectAnimator.ofFloat(this@DashboardView, "arcProgress", 0f, 100f)
objectAnimator.duration = 1000
// objectAnimator.interpolator = LinearInterpolator()
objectAnimator.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator?) {
}
override fun onAnimationCancel(animation: Animator?) {
}
override fun onAnimationRepeat(animation: Animator?) {
}
override fun onAnimationEnd(animation: Animator?) {
//圆弧画完 再画其他
drawArcFinished = true
preDrawDsh()
//开始画刻度
val objectA = ObjectAnimator.ofFloat(this@DashboardView, "dashProgress", 0f, 100f)
objectA.duration = 2000
objectA.start()
}
})
objectAnimator.start()
}
private var drawArcFinished = false
private fun preDrawDsh() {
//假如有20个格子(21个刻度) 那就用弧线总长度除以20 就是间隔
//计算弧线总长度
//创建路径效果:用虚线模式 虚线就是刻度小矩形
pathMeasure = PathMeasure(tempArc, false)
"pathMeasure.length = ${pathMeasure.length}".logE()
pathDashPathEffect = PathDashPathEffect(
dash,
(pathMeasure.length - DASH_WIDTH) / 20f,
0f,
PathDashPathEffect.Style.ROTATE
)
}
override fun onDraw(canvas: Canvas?) {
/**
* useCenter = true 可以用来做饼图! =false就是仪表盘了 不过要想用userCenter要用canvas?.drawArc不能用path.addArc
*/
//1、画圆弧
pathArc.addArc(rectF, 90 + OPEN_ANGLE / 2, (360 - OPEN_ANGLE) * (arcProgress / 100))
canvas?.drawPath(pathArc, paint)
//2、画刻度效果
drawDashPath(canvas)
//3、画指针
drawPointer(canvas)
}
private fun drawPointer(canvas: Canvas?) {
if (!drawArcFinished) return
//指针的起点是圆心,终点坐标就是三角函数的正余弦,正弦*斜边长度=纵坐标,余弦*斜边长度 = 横坐标,算好坐标再加上起始坐标即可
//注意:三角函数需要传入的是弧度值,要先把角度转弧度 、并且不用考虑坐标的正负数,因为三角函数自带正负
//指针的角度计算 = 圆弧的划过角度除以DASH_COUNT乘以progress可以得到指针在圆弧的角度,再加上圆弧的起始角度(90+半个开口角度),最终得到指针真正的角度
pointerAngle = (360 - OPEN_ANGLE) / DASH_COUNT * pointerProgress + 90 + OPEN_ANGLE / 2
// "pointerAngle = $pointerAngle".logE()
canvas?.drawLine(
width / 2f,
height / 2f,
(cos(Math.toRadians(pointerAngle.toDouble())) * POINTER_LENGTH + width / 2f).toFloat(),
(sin(Math.toRadians(pointerAngle.toDouble())) * POINTER_LENGTH + height / 2f).toFloat(),
paint
)
}
private fun drawDashPath(canvas: Canvas?) {
pathDashPathEffect?.let {
paint.pathEffect = pathDashPathEffect
pathDashArc.addArc(
rectF,
90 + OPEN_ANGLE / 2,
(360 - OPEN_ANGLE) * (dashProgress / 100)
)
canvas?.drawPath(pathDashArc, paint)
paint.pathEffect = null
}
}
}
工具类代码
val Float.px
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this,
Resources.getSystem().displayMetrics
)
fun String.logE() {
Log.e("xc", this)
}
activity中指针动画代码
startButton.setOnClickListener {
val objectAnimator = ObjectAnimator.ofFloat(dash_view, "pointerProgress", 0f, 20f)
objectAnimator.duration = 10000
objectAnimator.interpolator = LinearInterpolator()
objectAnimator.start()
}