0%

Android自定义仪表盘


又是更新blog的一天,最近项目中遇到一个需求,绘制一个渐变色的仪表盘去展示某个行情当前的热度,为用户提供当前市场热度更直观的一个大概参考,需要用到自定义View的相关知识,在这里做一个总结,欢迎一起学习和讨论~


最终效果图:
效果图

变量声明

  • 声明画笔,颜色,刻度数,属性动画,进度条宽度,表盘矩形区域,文本大小,必要的偏移量,表盘的开始和过渡角度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    var progressColor = intArrayOf() // 渐变色开始颜色
    var progressBackgroundColor = R.color.emphasis16 // 进度条背景颜色
    var textColor = R.color.primary // 文字颜色
    var tickScaleColor = R.color.emphasis8 // 普通刻度线颜色
    var groupScaleColor = R.color.emphasis38 // 分组刻度线颜色
    var progressStrokeWidth = 24f // 进度条宽度
    var paintProgressBackground = Paint() // 进度条背景画笔
    private var paintProgress = Paint() // 进度条画笔
    var paintText = Paint() // 文字画笔
    private var paintNum = Paint() // 刻度画笔
    var rect = RectF() // 表盘矩形区域
    private var viewWidth = 0 // 宽度
    private var viewHeight = 0 // 高度
    var percent = 0f // 百分比
    var oldPercent = 0f // 过去的百分比
    var textSize = 100f // 文本大小
    private var valueAnimator: ValueAnimator? = null // 属性动画
    var animatorDuration = 0L // 动画时长
    private var groupNum = 5 // 分组数
    private var ticksNum = 6 // 每组刻度数
    var pointerWidth = 15f // 指针的宽度
    private var ticksCount = groupNum * ticksNum + 1// 总刻度数

    companion object {
    var OFFSET = 30f // 偏移量
    var START_ARC = 150f // 开始角度
    var DURING_ARC = 240f // 过渡调度
    }

初始化逻辑

  • 获取xml的配置进行变量初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    init {
    setLayerType(LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.dashboard)
    progressBackgroundColor = typedArray.getColor(
    R.styleable.dashboard_progressBackgroundColor,
    ContextCompat.getColor(context, progressBackgroundColor)
    )
    progressStrokeWidth = typedArray.getDimension(
    R.styleable.dashboard_progressStrokeWidth,
    progressStrokeWidth
    )

    textSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    textSize
    )
    textColor = typedArray.getColor(R.styleable.dashboard_textColor, textColor)
    tickScaleColor =
    typedArray.getColor(R.styleable.dashboard_tickScaleColor, tickScaleColor)
    groupScaleColor =
    typedArray.getColor(R.styleable.dashboard_groupScaleColor, groupScaleColor)
    groupNum = typedArray.getInt(R.styleable.dashboard_groupNum, groupNum)
    ticksNum = typedArray.getInt(R.styleable.dashboard_ticksNum, ticksNum)
    pointerWidth =
    typedArray.getDimension(R.styleable.dashboard_pointerWidth, pointerWidth)
    val colorsId = typedArray.getResourceId(R.styleable.dashboard_progressColors, 0)
    progressColor =typedArray.resources.getIntArray(colorsId)
    ticksCount = groupNum * ticksNum + 1
    typedArray.recycle()
    OFFSET = progressStrokeWidth + 10f
    initPaint()
    }

    /**
    * 初始化画笔
    */
    private fun initPaint() {
    paintProgressBackground.isAntiAlias = true
    paintProgressBackground.strokeWidth = progressStrokeWidth
    paintProgressBackground.style = Paint.Style.STROKE
    paintProgressBackground.color = progressBackgroundColor
    paintProgressBackground.isDither = true
    paintProgress.isAntiAlias = true
    paintProgress.strokeWidth = progressStrokeWidth
    paintProgress.style = Paint.Style.STROKE
    paintProgress.isDither = true
    paintText.isAntiAlias = true
    paintText.color = textColor
    paintText.strokeWidth = 1F
    paintText.style = Paint.Style.FILL
    paintText.isDither = true
    paintNum.isAntiAlias = true
    paintNum.strokeWidth = 3f
    paintNum.style = Paint.Style.FILL
    paintNum.isDither = true
    }

    声明xml配置属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <declare-styleable name="dashboard">
    <attr name="progressStrokeWidth" format="dimension" />
    <attr name="progressBackgroundColor" format="color" />
    <attr name="textColor" format="color" />
    <attr name="tickScaleColor" format="color" />
    <attr name="groupScaleColor" format="color" />
    <attr name="groupNum" format="integer" />
    <attr name="ticksNum" format="integer" />
    <attr name="pointerWidth" format="dimension" />
    <attr name="textSize" format="dimension" />
    <attr name="progressColors" format="reference"/>
    </declare-styleable>

    获取自定义View宽高,初始化渐变进度条

    回顾一下View的生命周期

    1
    activity.onCreate -> onFinishInflate -> activity.onStart -> activity.onResume -> onAttachedToWindow -> onWindowVisibilityChanged -> onVisibilityChanged -> onMeasure -> onSizeChanged -> onLayout -> onDraw -> onWindowFocusChanged -> activity.onPause -> onWindowFocusChanged -> onWindowVisibilityChanged -> activity.onStop -> onVisibilityChanged -> activity.onDestroy -> onDetachedFromWindow
  • 重写onSizeChanged,此时自定义View已经完成测量可以拿到当前自定义View的宽高
  • 初始化指针宽度,当前变盘的矩形区域,以0,0点为原点,左边上角的坐标,x为-(View宽度 / 2)+ 偏移量 + paddingLeft,y为- (view高度 / 2) + 偏移量 + paddingTop,右下角的坐标,x为(View宽度 / 2)- 偏移量 - paddingRight,y为(view高度 / 2) - 偏移量 - paddingBottom,渐变色采用以原点为中心的90度渐变,初始化Shader的子类SweepGradient并设置一个以原点为中心的90度旋转操作后的矩阵,再设置这个Shader为画笔的Shader
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    viewWidth = width
    viewHeight = height
    pointerWidth = (viewWidth / 40).toFloat()
    initShader()
    }

    /**
    * 初始化渐变颜色
    */
    private fun initShader() {
    rect.set(
    (-viewWidth / 2) + OFFSET + paddingLeft, paddingTop - (viewHeight / 2) + OFFSET,
    (viewWidth / 2) - paddingRight - OFFSET,
    (viewWidth / 2) - paddingBottom - OFFSET
    )
    val shader = SweepGradient(
    0f,
    0f,
    progressColor,
    null
    )
    val rotate = 90f
    val gradientMatrix = Matrix()
    gradientMatrix.preRotate(rotate, 0f, 0f)
    shader.setLocalMatrix(gradientMatrix)
    paintProgress.shader = shader
    }

    测量View的宽高

  • 根据当前的模式和大小决定View大小,如果是明确指明宽高的大小就用当前声明的值,否则就设置一个固定值使得宽高一致防止自定义View形变
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val realWidth = startMeasure(widthMeasureSpec)
    val realHeight = startMeasure(heightMeasureSpec)
    setMeasuredDimension(realWidth, realHeight)
    }

    fun startMeasure(msSpec: Int): Int {
    val mode = MeasureSpec.getMode(msSpec)
    val size = MeasureSpec.getSize(msSpec)
    return if (mode == MeasureSpec.EXACTLY) {
    size
    } else {
    Util.dp2px(200)
    }
    }

    重写绘制方法

  • 当前的仪表盘的绘制逻辑分为绘制表盘刻度,绘制进度条,绘制文本,绘制指针,canvas的原点为左上角,为了方便计算在绘制前需要把原点移动到canvas的中心,所以x方向平移(View宽度 / 2),y方向平移(View高度 / 2)
    1
    2
    3
    4
    5
    6
    7
    8
    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    canvas?.translate((viewWidth / 2).toFloat(), (viewHeight / 2).toFloat())
    drawPanel(canvas)
    drawProgress(canvas, percent)
    drawText(canvas, percent)
    drawPointer(canvas, percent)
    }

    绘制表盘刻度

  • 以0,0点为圆心,绘制起始点逆时针旋转-120度,计算刻度的y坐标为 -(View高度 / 2) + 偏移量 + 进度条宽度,计算每个刻度的旋转角度为过渡角度 / (总刻度数 - 1),再以总刻度数进行循环绘制刻度线条,判断当前下标是否为新一组开始刻度的位置,区分普通刻度和分割刻度并设置不同的颜色
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    * 绘制刻度
    * @param canvas Canvas?
    */
    fun drawPanel(canvas: Canvas?) {
    canvas?.save()
    canvas?.rotate(-(180 - START_ARC + 90), 0f, 0f)
    val numY = -viewHeight / 2 + OFFSET + progressStrokeWidth
    val angle = DURING_ARC / ((ticksCount - 1) * 1.0f)
    for (i in 0 until ticksCount) {
    canvas?.save()
    canvas?.rotate(angle * i, 0f, 0f)
    if (i == 0 || i % groupNum == 0) {
    paintNum.color = groupScaleColor
    } else {
    paintNum.color = tickScaleColor
    }
    canvas?.drawLine(0f, numY + 2, 0f, numY + (pointerWidth * 2) + 5, paintNum)
    canvas?.restore()
    }
    canvas?.restore()
    }

    绘制进度条

  • 先绘制背景进度条,绘制一个圆弧,绘制区域为表盘矩形区域,设置开始角度和过渡角度
  • 绘制进度条,判断当前的进度百分比如果大于1则为1,过滤掉其他异常情况,如果百分比大于0绘制一个渐变色圆弧,根据当前百分比和过渡角度计算当前需要绘制的渐变色过渡角度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * 绘制进度
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawProgress(canvas: Canvas?, percent: Float) {
    canvas?.drawArc(rect, START_ARC, DURING_ARC, false, paintProgressBackground)
    var curPercent = percent
    if (curPercent > 1.0f) {
    curPercent = 1.0f
    }
    if (curPercent > 0.0f) {
    canvas?.drawArc(rect, START_ARC, percent * DURING_ARC, false, paintProgress)
    }
    }

    绘制文本

  • 根据当前的进度百分比设置文本的内容和颜色,根据当前的View宽度计算一个Y方向的偏移量,如果百分比小于0.2展示“Very Cold”文本,渐变色数组中取第一个颜色,绘制文本的x坐标为文本宽度/2,第一个词y坐标为1.4倍偏移量,第二个词为2.4倍偏移量,如果百分比大于等于0.2并且小于0.4展示“Cold”文本,y坐标为2.2倍偏移量,如果百分比大于等于0.4并且小于0.6展示“Normal”文本,y坐标为2.2倍偏移量,如果百分比大于等于0.6并且小于0.8展示“Hot”文本,y坐标为2.2倍偏移量,如果是剩下的情况展示“Very Hot”文本,第一个词y坐标为1.4倍偏移量,第二个词为2.4倍偏移量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    /**
    * 绘制文本
    * @param canvas Canvas?
    * @param percent Float
    */
    @SuppressLint("RestrictedApi")
    fun drawText(canvas: Canvas?, percent: Float) {
    val offsetY = viewWidth / 8
    paintText.textSize = textSize
    if (percent < 0.2f) {
    val subTitle1 = "Very"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Cold"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    } else if (percent >= 0.2f && percent < 0.4f) {
    val subTitle = "Cold"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.4f && percent < 0.6f) {
    val subTitle = "Normal"
    paintText.color = progressColor[2]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.6f && percent < 0.8f) {
    val subTitle = "Hot"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else {
    val subTitle1 = "Very"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Hot"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    }

    paintText.textSize = (textSize * 1.3).toFloat()
    val text = (percent * 100).toInt().toString()
    canvas?.drawText(
    text,
    -paintText.measureText(text) / 2,
    - textSize / 5f,
    paintText
    )
    }

    绘制指针

  • 绘制一个三角形的指针,根据当前的百分比计算指针所在位置的旋转角度,再计算指针所在位置需要绘制三角形的路径,从(0, view的高度/2 - 偏移量 - 进度条宽度)为起点,绘制一个倒三角形,绘制右边的线段和左边的线段再绘制顶部的线段,以垂直向下为0度,旋转范围是-300到-60并设置填充模式绘制这条路径
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /**
    * 绘制指针
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawPointer(canvas: Canvas?, percent: Float) {
    canvas?.save()
    val angle = DURING_ARC * (percent - 0.5f) - 180
    canvas?.rotate(angle, 0f, 0f)
    val pointer = Path()
    pointer.moveTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.lineTo(
    pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(
    -pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.close()
    pointer.fillType = Path.FillType.EVEN_ODD
    canvas?.drawPath(pointer, paintText)
    canvas?.restore()
    }

设置百分比和动画

  • 传入当前的百分比范围0~1,初始化属性动画设置属性值变化监听刷新当前百分比,根据上一次的百分比和当前百分比计算动画时间,设置动画开始结束的百分比数值,设置动画结束监听刷新上一次百分比数值并做边界判断。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
    * 设置当前百分比
    * @param curPercent Float 范围0~1
    */
    fun setProgress(curPercent: Float) {
    if (valueAnimator?.isRunning == true) {
    valueAnimator?.cancel()
    }
    animatorDuration = (abs(curPercent - oldPercent) * 20).toLong()
    valueAnimator = ValueAnimator.ofFloat(oldPercent, curPercent).setDuration(animatorDuration)
    valueAnimator?.addUpdateListener {
    percent = it.animatedValue as Float
    invalidate()
    }
    valueAnimator?.interpolator = LinearInterpolator()
    valueAnimator?.addListener(onEnd = {
    oldPercent = curPercent
    if (percent < 0.0f) {
    percent = 0.0f
    invalidate()
    }
    if (percent > 1f) {
    percent = 1f
    invalidate()
    }
    })
    valueAnimator?.start()
    }

    使用

  • xml声明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <com.example.testapp.DashboardView
    android:id="@+id/view_dashboard"
    android:layout_width="200dp"
    android:layout_height="200dp"
    app:layout_constraintTop_toTopOf="parent"
    android:layout_marginStart="6dp"
    android:layout_marginTop="8dp"
    app:groupNum="5"
    app:groupScaleColor="@color/emphasis38"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:pointerWidth="2dp"
    app:progressBackgroundColor="@color/emphasis8"
    app:progressStrokeWidth="6dp"
    app:progressColors="@array/dashboardColors"
    app:textSize="24sp"
    app:tickScaleColor="@color/emphasis8"
    app:ticksNum="6" />
  • 设置当前百分比
    1
    dashboardView.setProgress((float) i / 100)
    最后附上完整代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    package com.example.testapp

    import android.animation.ValueAnimator
    import android.annotation.SuppressLint
    import android.content.Context
    import android.graphics.*
    import android.util.AttributeSet
    import android.view.View
    import android.view.animation.LinearInterpolator
    import androidx.core.animation.addListener
    import androidx.core.content.ContextCompat
    import kotlin.math.abs

    @SuppressLint("CustomViewStyleable")
    class DashboardView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
    ) :
    View(context, attrs, defStyleAttr) {
    var progressColor = intArrayOf() // 渐变色开始颜色
    var progressBackgroundColor = R.color.emphasis16 // 进度条背景颜色
    var textColor = R.color.primary // 文字颜色
    var tickScaleColor = R.color.emphasis8 // 普通刻度线颜色
    var groupScaleColor = R.color.emphasis38 // 分组刻度线颜色
    var progressStrokeWidth = 24f // 进度条宽度
    var paintProgressBackground = Paint() // 进度条背景画笔
    private var paintProgress = Paint() // 进度条画笔
    var paintText = Paint() // 文字画笔
    private var paintNum = Paint() // 刻度画笔
    var rect = RectF() // 表盘矩形区域
    private var viewWidth = 0 // 宽度
    private var viewHeight = 0 // 高度
    var percent = 0f // 百分比
    var oldPercent = 0f // 过去的百分比
    var textSize = 100f // 文本大小
    private var valueAnimator: ValueAnimator? = null // 属性动画
    var animatorDuration = 0L // 动画时长
    private var groupNum = 5 // 分组数
    private var ticksNum = 6 // 每组刻度数
    var pointerWidth = 15f // 指针的宽度
    private var ticksCount = groupNum * ticksNum + 1// 总刻度数

    companion object {
    var OFFSET = 30f // 偏移量
    var START_ARC = 150f // 开始角度
    var DURING_ARC = 240f // 过渡调度
    }

    init {
    setLayerType(LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.dashboard)
    progressBackgroundColor = typedArray.getColor(
    R.styleable.dashboard_progressBackgroundColor,
    ContextCompat.getColor(context, progressBackgroundColor)
    )
    progressStrokeWidth = typedArray.getDimension(
    R.styleable.dashboard_progressStrokeWidth,
    progressStrokeWidth
    )

    textSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    textSize
    )
    textColor = typedArray.getColor(R.styleable.dashboard_textColor, textColor)
    tickScaleColor =
    typedArray.getColor(R.styleable.dashboard_tickScaleColor, tickScaleColor)
    groupScaleColor =
    typedArray.getColor(R.styleable.dashboard_groupScaleColor, groupScaleColor)
    groupNum = typedArray.getInt(R.styleable.dashboard_groupNum, groupNum)
    ticksNum = typedArray.getInt(R.styleable.dashboard_ticksNum, ticksNum)
    pointerWidth =
    typedArray.getDimension(R.styleable.dashboard_pointerWidth, pointerWidth)
    val colorsId = typedArray.getResourceId(R.styleable.dashboard_progressColors, 0)
    progressColor =typedArray.resources.getIntArray(colorsId)
    ticksCount = groupNum * ticksNum + 1
    typedArray.recycle()
    OFFSET = progressStrokeWidth + 10f
    initPaint()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val realWidth = startMeasure(widthMeasureSpec)
    val realHeight = startMeasure(heightMeasureSpec)
    setMeasuredDimension(realWidth, realHeight)
    }

    fun startMeasure(msSpec: Int): Int {
    val mode = MeasureSpec.getMode(msSpec)
    val size = MeasureSpec.getSize(msSpec)
    return if (mode == MeasureSpec.EXACTLY) {
    size
    } else {
    Util.dp2px(200)
    }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    viewWidth = width
    viewHeight = height
    pointerWidth = (viewWidth / 40).toFloat()
    initShader()
    }

    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    canvas?.translate((viewWidth / 2).toFloat(), (viewHeight / 2).toFloat())
    drawPanel(canvas)
    drawProgress(canvas, percent)
    drawText(canvas, percent)
    drawPointer(canvas, percent)
    }

    /**
    * 绘制刻度
    * @param canvas Canvas?
    */
    fun drawPanel(canvas: Canvas?) {
    canvas?.save()
    canvas?.rotate(-(180 - START_ARC + 90), 0f, 0f)
    val numY = -viewHeight / 2 + OFFSET + progressStrokeWidth
    val angle = DURING_ARC / ((ticksCount - 1) * 1.0f)
    for (i in 0 until ticksCount) {
    canvas?.save()
    canvas?.rotate(angle * i, 0f, 0f)
    if (i == 0 || i % groupNum == 0) {
    paintNum.color = groupScaleColor
    } else {
    paintNum.color = tickScaleColor
    }
    canvas?.drawLine(0f, numY + 2, 0f, numY + (pointerWidth * 2) + 5, paintNum)
    canvas?.restore()
    }
    canvas?.restore()
    }

    /**
    * 绘制进度
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawProgress(canvas: Canvas?, percent: Float) {
    canvas?.drawArc(rect, START_ARC, DURING_ARC, false, paintProgressBackground)
    var curPercent = percent
    if (curPercent > 1.0f) {
    curPercent = 1.0f
    }
    if (curPercent > 0.0f) {
    canvas?.drawArc(rect, START_ARC, percent * DURING_ARC, false, paintProgress)
    }
    }

    /**
    * 绘制指针
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawPointer(canvas: Canvas?, percent: Float) {
    canvas?.save()
    val angle = DURING_ARC * (percent - 0.5f) - 180
    canvas?.rotate(angle, 0f, 0f)
    val pointer = Path()
    pointer.moveTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.lineTo(
    pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(
    -pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.close()
    pointer.fillType = Path.FillType.EVEN_ODD
    canvas?.drawPath(pointer, paintText)
    canvas?.restore()
    }

    /**
    * 绘制文本
    * @param canvas Canvas?
    * @param percent Float
    */
    @SuppressLint("RestrictedApi")
    fun drawText(canvas: Canvas?, percent: Float) {
    val offsetY = viewWidth / 8
    paintText.textSize = textSize
    if (percent < 0.2f) {
    val subTitle1 = "Very"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Cold"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    } else if (percent >= 0.2f && percent < 0.4f) {
    val subTitle = "Cold"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.4f && percent < 0.6f) {
    val subTitle = "Normal"
    paintText.color = progressColor[2]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.6f && percent < 0.8f) {
    val subTitle = "Hot"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else {
    val subTitle1 = "Very"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Hot"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    }

    paintText.textSize = (textSize * 1.3).toFloat()
    val text = (percent * 100).toInt().toString()
    canvas?.drawText(
    text,
    -paintText.measureText(text) / 2,
    - textSize / 5f,
    paintText
    )
    }

    /**
    * 设置当前百分比
    * @param curPercent Float 范围0~1
    */
    fun setProgress(curPercent: Float) {
    if (valueAnimator?.isRunning == true) {
    valueAnimator?.cancel()
    }
    animatorDuration = (abs(curPercent - oldPercent) * 20).toLong()
    valueAnimator = ValueAnimator.ofFloat(oldPercent, curPercent).setDuration(animatorDuration)
    valueAnimator?.addUpdateListener {
    percent = it.animatedValue as Float
    invalidate()
    }
    valueAnimator?.interpolator = LinearInterpolator()
    valueAnimator?.addListener(onEnd = {
    oldPercent = curPercent
    if (percent < 0.0f) {
    percent = 0.0f
    invalidate()
    }
    if (percent > 1f) {
    percent = 1f
    invalidate()
    }
    })
    valueAnimator?.start()
    }

    /**
    * 设置进度条宽度
    * @param dp Int
    */
    fun setProgressStroke(dp: Int) {
    progressStrokeWidth = Util.dp2px(dp).toFloat()
    paintProgress.strokeWidth = progressStrokeWidth
    paintProgressBackground.strokeWidth = progressStrokeWidth
    invalidate()
    }



    /**
    * 初始化画笔
    */
    private fun initPaint() {
    paintProgressBackground.isAntiAlias = true
    paintProgressBackground.strokeWidth = progressStrokeWidth
    paintProgressBackground.style = Paint.Style.STROKE
    paintProgressBackground.color = progressBackgroundColor
    paintProgressBackground.isDither = true
    paintProgress.isAntiAlias = true
    paintProgress.strokeWidth = progressStrokeWidth
    paintProgress.style = Paint.Style.STROKE
    paintProgress.isDither = true
    paintText.isAntiAlias = true
    paintText.color = textColor
    paintText.strokeWidth = 1F
    paintText.style = Paint.Style.FILL
    paintText.isDither = true
    paintNum.isAntiAlias = true
    paintNum.strokeWidth = 3f
    paintNum.style = Paint.Style.FILL
    paintNum.isDither = true
    }

    /**
    * 初始化渐变颜色
    */
    private fun initShader() {
    rect.set(
    (-viewWidth / 2) + OFFSET + paddingLeft, paddingTop - (viewHeight / 2) + OFFSET,
    (viewWidth / 2) - paddingRight - OFFSET,
    (viewWidth / 2) - paddingBottom - OFFSET
    )
    val shader = SweepGradient(
    0f,
    0f,
    progressColor,
    null
    )
    val rotate = 90f
    val gradientMatrix = Matrix()
    gradientMatrix.preRotate(rotate, 0f, 0f)
    shader.setLocalMatrix(gradientMatrix)
    paintProgress.shader = shader
    }
    }