class DashBoardView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null) : View(context, attrs) {

private var radius = 0f //半径
set(value) {
field = value
outArcRadius = value - outArcMargin
}

var maxPointer = 120 //最大刻度
set(value) {
field = value
everyPointerRotate = ((360f - keepAngle) / value)
}
var warningPointer = 110 //告警的刻度


var keepAngle = 90f //不参与刻度的角度值
set(value) {
field = value
maxAngle = 360f - value
everyPointerRotate = ((360f - value) / maxPointer)
}
private var maxAngle = 360f - keepAngle

private var everyPointerRotate = ((360f - keepAngle) / maxPointer) //绘制每个刻度后旋转的角度

// 设置圆心为0后,下面的绘制法是从右边中间开始,
// 这个值代表旋转多少度作为绘制坐标开始的地方
// 速度的盘默认是90 + keepAngle/2f 度,即左下角作为0进行开始
var rotateAngel = 90 + keepAngle / 2f

private var currentPointer = 0
set(value) {
field = when {
value > maxPointer -> {
maxPointer
}
value < 0 -> {
0
}
else -> {
value
}
}
invalidate()
}

private val frame1To4 = Runnable { currentPointer += everyFrameProgress }
private val frameLast = Runnable { currentPointer = needProgress }
var animationDuration = 50L
set(value) {
field = value
onFrameTime = value / 3
onFrameTimeHardwareAccelerated = value / 5
}

//值动画消耗内存多,改为自己实现,有硬件加速绘制一次要8毫秒左右 50毫秒绘制5次 没有硬件加速会波动比较大
private var onFrameTime = animationDuration / 3
private var needProgress = 0
private var everyFrameProgress = 0
private val frameHandler: Handler by lazy { Handler() }

fun setCurrentPointerAnimator(value: Int) {
val real = when {
value > maxPointer -> {
maxPointer
}
value < 0 -> {
0
}
else -> {
value
}
}
if (real != currentPointer) {
needProgress = real
everyFrameProgress = (real - currentPointer) / 3
frameHandler.removeCallbacksAndMessages(null)
currentPointer += everyFrameProgress
frameHandler.postDelayed(frame1To4, onFrameTime)
frameHandler.postDelayed(frameLast, onFrameTime * 2)
}
}

private var onFrameTimeHardwareAccelerated = animationDuration / 5

/**
* 在清单文件开启硬件加速后的处理,但是Native Heap和Graphics 会在切换应用之前一直涨,最终会很慢
*/
fun setCurrentPointerAnimatorHardwareAccelerated(value: Int) {
val real = when {
value > maxPointer -> {
maxPointer
}
value < 0 -> {
0
}
else -> {
value
}
}
if (real != currentPointer) {
needProgress = real
everyFrameProgress = (real - currentPointer) / 5
frameHandler.removeCallbacksAndMessages(null)
currentPointer += everyFrameProgress
frameHandler.postDelayed(frame1To4, onFrameTime)
frameHandler.postDelayed(frame1To4, onFrameTime * 2)
frameHandler.postDelayed(frame1To4, onFrameTime * 3)
frameHandler.postDelayed(frameLast, onFrameTime * 4)
}
}

private val normalSimplePointerColor: Int by lazy { Color.parseColor("#555555") } //非报警的一般刻度的颜色
private val waringFocusPointerColor: Int by lazy { resources.getColor(R.color.colorAlarm, null) } //报警时的大中刻度的颜色
private val waringSimplePointerColor: Int by lazy { resources.getColor(R.color.colorAlarmSimple, null) } //报警时的小刻度的颜色

private val longPointerWidth = 16f //长刻度的宽
private val longPointerHeight = 4f //长刻度的高

private val normalPointerWidth = 7f //短刻度的宽
private val normalPointerHeight = 2f //短刻度的高

private val pointerMargin = 4f //刻度到边的margin
private val pointerRadius: Float by lazy { radius - pointerMargin } //刻度的实际半径

private var pointerTextSize = 24f //刻度数字文字大小
private var pointerTextMargin = 8 //刻度数字距离刻度的距离

//绘制低刻度,进度刻度,进度指针的paint
private val pointerPaint: Paint by lazy {
val pointerPaint = Paint()
pointerPaint.isAntiAlias = true
pointerPaint.textSize = pointerTextSize
pointerPaint.textAlign = Paint.Align.CENTER
pointerPaint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
pointerPaint
}

//用于计算刻度文字的BaseLine
private val fontMetrics: Paint.FontMetrics by lazy { pointerPaint.fontMetrics }

/**
* 是否绘制不能除5时的刻度
*/
var isDrawSimplePointer = true

/**
* 是否 5 15 等中等等级的刻度使用 0 10等刻度的颜色绘制
*/
var isMediumPointerUseFocusColor = true

/**
* 是否把刻度数字简化 / 10,比如10简化为1 100也简化为10
*/
var isPointerTextSimple = false

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)

var width: Int
var height: Int
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize
} else {
width = paddingLeft + 128 * 2 + paddingRight
if (widthMode == MeasureSpec.AT_MOST) {
width = min(width, widthSize)
}
}

if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize
} else {
height = paddingTop + 128 * 2 + paddingBottom
if (heightMode == MeasureSpec.AT_MOST) {
height = min(height, heightSize)
}
}

setMeasuredDimension(width, height)

radius = min(measuredWidth - paddingLeft - paddingRight,
measuredHeight - paddingTop - paddingBottom) / 2f
}

//每次postInvalidate canvas都会自动清空并且重置坐标系
override fun onDraw(canvas: Canvas) {
//绘制底部刻度
canvas.translate(paddingLeft + radius, paddingTop + radius) //移动canvas的起点为中心点,即圆心为(0,0)
canvas.rotate(rotateAngel) //rotate是改变坐标系,不是改变了画布,已经画上去的不会有任何变化
canvas.save()
drawDefaultPointerLine(canvas)
canvas.restore()
if (currentPointer in 1..maxPointer) {
val currentAngle = maxAngle * currentPointer / maxPointer
if (currentAngle > 0) {
drawPointerLine(canvas)
drawArc(canvas, currentAngle)
drawShadow(canvas, currentAngle)
}
}
}

private val textRect = Rect()

/**
* 绘制刻度线
*/
private fun drawDefaultPointerLine(canvas: Canvas) {
for (i in 0..maxPointer) {
if (i < warningPointer) {
if (isMediumPointerUseFocusColor) {
if (i % 5 == 0) {
pointerPaint.color = Color.WHITE
} else {
pointerPaint.color = normalSimplePointerColor
}
} else {
if (i % 10 == 0) {
pointerPaint.color = Color.WHITE
} else {
pointerPaint.color = normalSimplePointerColor
}
}
} else {
if (isMediumPointerUseFocusColor) {
if (i % 5 == 0) {
pointerPaint.color = waringFocusPointerColor
} else {
pointerPaint.color = waringSimplePointerColor
}
} else {
if (i % 10 == 0) {
pointerPaint.color = waringFocusPointerColor
} else {
pointerPaint.color = waringSimplePointerColor
}
}
}

if (i % 10 == 0) { //长刻度
pointerPaint.strokeWidth = longPointerHeight
canvas.drawLine(pointerRadius, 0f, (pointerRadius - longPointerWidth), 0f, pointerPaint)

//绘制刻度数字
val text = if (isPointerTextSimple) {
(i / 10).toString()
} else {
i.toString()
}

canvas.save()
pointerPaint.getTextBounds(text, 0, text.length, textRect)
val currentCenterX = (pointerRadius - longPointerWidth - pointerTextMargin
- max(textRect.height(), textRect.width()) / 2f) //只以width为标准,在字旋转过多之后会有问题
pointerPaint.measureText(text)
canvas.translate(currentCenterX, 0f)
canvas.rotate(360 - rotateAngel - everyPointerRotate * i)

val textBaseLine = (fontMetrics.bottom - fontMetrics.top) / 2f - fontMetrics.bottom
canvas.drawText(text, 0f, textBaseLine, pointerPaint)
canvas.restore()
} else { //短刻度
if (isDrawSimplePointer || i % 5 == 0) {
pointerPaint.strokeWidth = normalPointerHeight
canvas.drawLine(pointerRadius, 0f, (pointerRadius - normalPointerWidth), 0f, pointerPaint)
}
}
canvas.rotate(everyPointerRotate)
}
}

var outArcWidth = 5f //外圆的线宽
var outArcMargin = 4 //外圆距离整个控件的距离
set(value) {
field = value
outArcRadius = radius - outArcMargin
}
private var outArcRadius = radius - outArcMargin //外圆的实际半径
set(value) {
field = value
outArcRect.set(-value, -value, value, value)
}
private val outArcRect: RectF by lazy {
RectF(-outArcRadius, -outArcRadius, outArcRadius, outArcRadius)
}

private val normalArcColor: Int by lazy { resources.getColor(R.color.colorNormal, null) }

//绘制进度圆弧和阴影的paint
private val arcPaint: Paint by lazy {
val arcPaint = Paint()
arcPaint.isAntiAlias = true
arcPaint.style = Paint.Style.STROKE
arcPaint
}

var inArcWidth = -1f //内圆的线宽,如果存在内圆,则指针会到内圆为止,如果内圆线宽为-1则使用pointerLength作为指针长度
var inArcRadius = -1f //内圆的半径
set(value) {
field = value
inArcRect.set(-value, -value, value, value)
}
private val inArcRect: RectF by lazy {
RectF(-inArcRadius, -inArcRadius, inArcRadius, inArcRadius)
}

//8刻度的角度,-0.6-8 使用阴影绘制
private val inArcGradientAngle: Int by lazy { (maxAngle * 8 / maxPointer).toInt() }

var pointerLength = 52f //没有内圈时如果要绘制指针,需要定义的指针长度
var pointerWidth = 3f //指针的粗细
private val pointerPath = Path() //用于绘制指针的path

/**
* 有进度时的有颜色的刻度
*/
private fun drawPointerLine(canvas: Canvas) {
if (currentPointer < warningPointer) {
pointerPaint.color = normalArcColor
} else {
pointerPaint.color = waringFocusPointerColor
}
canvas.save()
for (i in 0..currentPointer) {
if (i % 10 == 0) { //长刻度
pointerPaint.strokeWidth = longPointerHeight
canvas.drawLine(pointerRadius, 0f, (pointerRadius - longPointerWidth), 0f, pointerPaint)
} else { //短刻度
if (isDrawSimplePointer || i % 5 == 0) {
pointerPaint.strokeWidth = normalPointerHeight
canvas.drawLine(pointerRadius, 0f, (pointerRadius - normalPointerWidth), 0f, pointerPaint)
}
}
if (i == currentPointer) { //最后一个时用Path绘制一个三角形小指针
pointerPath.reset() //不reset会越来越慢
pointerPath.moveTo(outArcRadius, longPointerHeight / 2)
if (inArcWidth != -1f) {
pointerPath.lineTo(inArcRadius - inArcWidth + 5, 0f)
} else {
pointerPath.lineTo(outArcRadius - pointerLength, 0f)
}
pointerPath.lineTo(outArcRadius, -pointerWidth)
pointerPath.close()
canvas.drawPath(pointerPath, pointerPaint)
}
canvas.rotate(everyPointerRotate)
}
canvas.restore()
}

/**
* 有进度时最外层的圈和如果有内层的圈
*/
private fun drawArc(canvas: Canvas, currentAngle: Float) {
val paintColor = if (currentPointer < warningPointer) {
normalArcColor
} else {
waringFocusPointerColor
}

arcPaint.strokeWidth = outArcWidth
//-0.6是因为0刻度有宽度,会有一半未覆盖到
arcPaint.color = paintColor
canvas.drawArc(outArcRect, -0.6f, currentAngle + 1f, false, arcPaint) //第二个值代表绘制多少度,不是绘制到多少度

arcPaint.strokeWidth = inArcWidth
if (inArcRadius != -1f) {
//内圈0-8刻度时绘制渐变
if (currentAngle < inArcGradientAngle) {
val startAngle = 0f
val end = currentAngle.toInt()
for (i in 0..end) {
arcPaint.color = getGradient((i / end.toFloat()), Color.TRANSPARENT, paintColor)
if (i >= end - 1) { //2f绘制的渐变更连续,倒数第二格的时候开始只绘制1f
canvas.drawArc(inArcRect, startAngle + i, 1f, false, arcPaint)
} else {
canvas.drawArc(inArcRect, startAngle + i, 2f, false, arcPaint)
}
}
} else {
val startAngle = 0f
for (i in 0..inArcGradientAngle) {
arcPaint.color = getGradient((i / inArcGradientAngle.toFloat()), Color.TRANSPARENT, paintColor)
canvas.drawArc(inArcRect, startAngle + i, 2f, false, arcPaint)
}
arcPaint.color = paintColor
canvas.drawArc(inArcRect, inArcGradientAngle.toFloat(), currentAngle - inArcGradientAngle, false, arcPaint)
}
}
}

//获取渐变色
private fun getGradient(fraction: Float, startColor: Int, endColor: Int): Int {
var newFraction = fraction
if (newFraction > 1) newFraction = 1f
val alphaStart = Color.alpha(startColor)
val redStart = Color.red(startColor)
val blueStart = Color.blue(startColor)
val greenStart = Color.green(startColor)
val alphaEnd = Color.alpha(endColor)
val redEnd = Color.red(endColor)
val blueEnd = Color.blue(endColor)
val greenEnd = Color.green(endColor)
val alphaDifference = alphaEnd - alphaStart
val redDifference = redEnd - redStart
val blueDifference = blueEnd - blueStart
val greenDifference = greenEnd - greenStart
val alphaCurrent = (alphaStart + newFraction * alphaDifference).toInt()
val redCurrent = (redStart + newFraction * redDifference).toInt()
val blueCurrent = (blueStart + newFraction * blueDifference).toInt()
val greenCurrent = (greenStart + newFraction * greenDifference).toInt()
return Color.argb(alphaCurrent, redCurrent, greenCurrent, blueCurrent)
}

var shadowWidth = 60 //显示阴影的宽度,从外远往内的距离
private val shadowRect = RectF()

/**
* 有进度时的阴影效果.
*/
private fun drawShadow(canvas: Canvas, currentAngle: Float) {
val showAttenuation: Float
val paintColor = if (currentPointer < warningPointer) {
showAttenuation = 0.35f
normalArcColor
} else {
showAttenuation = 0.45f
waringFocusPointerColor
}
val shadowsRadius = outArcRadius - outArcWidth + 4

arcPaint.strokeWidth = 2f

for (i in shadowWidth downTo 0) {
arcPaint.color = getGradient(i / shadowWidth.toFloat() * showAttenuation, Color.TRANSPARENT, paintColor)
val rectRadius = shadowsRadius - shadowWidth + i
shadowRect.set(-rectRadius, -rectRadius, rectRadius, rectRadius)
canvas.drawArc(shadowRect, -1f, currentAngle, false, arcPaint)
}
}
}