0%

Android自定义对比指示条


最近工作比较忙,项目压力大,又是很长一段时间没有更新blog了。今天虽然是放五一长假,但是由于疫情仍然没有结束,所以没有出游的计划,正好可以写点东西总结一下最近项目中遇到的自定义View相关的内容,实现一个总量一定,支持和反对人数对比的指示条来反映某个问题下大多数人的看法,欢迎交流和讨论~


最终效果图:
效果图

变量声明

  • 声明支持和反对的绘制画笔,数量以及颜色,定义指示条的宽度,中间间隔的宽度,以及三角分割的宽度,圆角大小,指示条坐标对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private var downPaint = Paint()
    private var upPaint = Paint()
    private var upCount = 80
    private var downCount = 0
    private var downColor = ContextCompat.getColor(context, R.color.secondary)
    private var upColor = ContextCompat.getColor(context, R.color.primary)
    private var lineWidth = Util.dp2px(6)
    private var attachWidth = Util.dp2px(2)
    private var spanWidth = Util.dp2px(9)
    private var roundSize = lineWidth - Util.dp2px(2)
    private val indicator by lazy { Indicator() }

    指示条坐标对象

  • 整个指示条包含8个坐标,支持段和反对段各4个坐标,所以封装为一个Indicator对象,把支持和反对的绘制路径封装成两个IndicatorPath对象,每个IndicatorPath包含4个坐标,每个坐标包含x,y的数值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    data class Indicator(
    var upPath: IndicatorPath = IndicatorPath(),
    var downPath: IndicatorPath = IndicatorPath()
    )

    data class IndicatorPath(
    var startTop: Pos = Pos(),
    var startBottom: Pos = Pos(),
    var endTop: Pos = Pos(),
    var endBottom: Pos = Pos()
    )

    data class Pos(var x: Float = 0f, var y: Float = 0f)

    初始化逻辑

  • 获取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
    init {
    setLayerType(View.LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.compareindicator)
    downColor = typedArray.getColor(R.styleable.compareindicator_down_color, downColor)
    upColor = typedArray.getColor(R.styleable.compareindicator_up_color, upColor)
    lineWidth = typedArray.getDimension(
    R.styleable.compareindicator_lineWidth,
    lineWidth.toFloat()
    ).toInt()
    attachWidth = typedArray.getDimension(
    R.styleable.compareindicator_attachWidth,
    lineWidth.toFloat()
    ).toInt()
    spanWidth = typedArray.getDimension(
    R.styleable.compareindicator_spanWidth,
    lineWidth.toFloat()
    ).toInt()
    textSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    textSize
    )
    typedArray.recycle()
    upPaint = Paint()
    upPaint.color = upColor
    downPaint = Paint()
    downPaint.color = downColor
    }

    重写onDraw自定义绘制逻辑

  • 当支持和反对数量均不为0时,根据当前View宽度和支持数量百分比计算支持指示条的长度,根据当前View宽度和反对数量百分比计算反对指示条的长度,再计算支持和反对指示条的绘制路径。如果支持数不为0反对数为0则只绘制支持指示条,设置支持绘制画笔的圆角,直接绘制一根长度为View宽度的支持指示条,如果支持数为0反对数不为0则只绘制反对指示条,设置反对绘制画笔的圆角,直接绘制一根长度为View宽度的反对指示条。如果支持和反对数都为0,则支持和反对指示条长度各占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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    val upWidth: Int
    val downWidth: Int
    if (upCount != 0 && downCount != 0) {
    upWidth = width / (downCount + upCount) * upCount
    downWidth = width / (downCount + upCount) * downCount
    getPath(downWidth)
    drawIndicator(canvas)
    } else if (upCount != 0 && downCount == 0) {
    upWidth = width
    upPaint.strokeWidth = lineWidth.toFloat()
    upPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (upWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    upPaint
    )
    } else if (upCount == 0 && downCount != 0) {
    downWidth = width
    downPaint.strokeWidth = lineWidth.toFloat()
    downPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (downWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    downPaint
    )
    } else {
    upWidth = width / 2
    downWidth = width / 2
    getPath(downWidth)
    drawIndicator(canvas)
    }
    drawText(canvas)
    }

    计算绘制路径

  • 分别计算支持指示条和反对指示条的绘制路径,各自计算开始和结束的上下坐标,这里考虑到圆角,所以左边的反对指示条x开始坐标为View的paddingStart+圆角的大小,x结束坐标为View的paddingStart+圆角的大小+反对指示条的长度+三角形分割的宽度-间隔的宽度,y开始坐标为View的paddingTop,y结束坐标为View的paddingTop+指示条的宽度。右边的支持指示条x开始坐标为反对指示条的x坐标+间隔宽度,结束坐标为View的宽度-View的paddingEnd-圆角大小,y开始坐标为paddingTop,结束坐标为paddingTop+指示条的宽度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    private fun getPath(downWidth: Int) {
    val paddingSpanStart = paddingStart + roundSize
    // 反对指示条
    indicator.downPath.startTop.x = paddingSpanStart.toFloat()
    indicator.downPath.startTop.y = paddingTop.toFloat()
    indicator.downPath.startBottom.x = paddingSpanStart.toFloat()
    indicator.downPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.downPath.endTop.x =
    (paddingSpanStart + downWidth + attachWidth - spanWidth).toFloat()
    indicator.downPath.endTop.y = paddingTop.toFloat()
    indicator.downPath.endBottom.x = (paddingSpanStart + downWidth - spanWidth).toFloat()
    indicator.downPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    val paddingSpanEnd = paddingEnd + roundSize
    // 支持指示条
    indicator.upPath.startTop.x = indicator.downPath.endTop.x + spanWidth
    indicator.upPath.startTop.y = paddingTop.toFloat()
    indicator.upPath.startBottom.x = indicator.downPath.endBottom.x + spanWidth
    indicator.upPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.upPath.endTop.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endTop.y = paddingTop.toFloat()
    indicator.upPath.endBottom.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    }

    绘制指示条逻辑

  • 设置支持和反对的绘制画笔为填充模式,绘制反对指示条的路径,从开始的上坐标到结束的上坐标,从结束的下坐标到开始的下坐标都是绘制直线,开始的下坐标到开始的上坐标是绘制圆滑的曲线即贝塞尔曲线,控制点为x的位置是开始的下坐标x-圆角大小,y是开始的下坐标y-指示线的宽度/2,结束点为开始的上坐标。同理,绘制支持指示条的路径,从结束的上坐标到开始的上坐标,从开始的下坐标到结束的下坐标都是绘制直线,结束的下坐标到结束的上坐标同样是绘制贝塞尔曲线,控制点为x的位置是结束的下坐标x-圆角大小,y是结束的下坐标y-指示线的宽度/2,结束点为结束的上坐标。
    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
    private fun drawIndicator(canvas: Canvas?) {
    downPaint.strokeWidth = 1F
    upPaint.strokeWidth = 1F
    downPaint.strokeCap = Paint.Cap.BUTT
    upPaint.strokeCap = Paint.Cap.BUTT
    drawLine(canvas, indicator.downPath, downPaint, true)
    drawLine(canvas, indicator.upPath, upPaint, false)
    }

    private fun drawLine(
    canvas: Canvas?,
    indicatorPath: IndicatorPath,
    paint: Paint,
    down: Boolean
    ) {
    val path = Path()
    if (down) {
    path.moveTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.quadTo(
    indicatorPath.startBottom.x - roundSize,
    indicatorPath.startBottom.y - (lineWidth / 2),
    indicatorPath.startTop.x,
    indicatorPath.startTop.y
    )
    } else {
    path.moveTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.quadTo(
    indicatorPath.endBottom.x + roundSize,
    indicatorPath.endBottom.y - (lineWidth / 2),
    indicatorPath.endTop.x,
    indicatorPath.endTop.y
    )
    }
    path.close()
    paint.strokeWidth = 1f
    paint.style = Paint.Style.FILL
    path.fillType = Path.FillType.WINDING
    canvas?.drawPath(path, paint)
    }

    绘制文本逻辑

  • 左边是反对指示条在开始位置绘制Down的数量文本,x的坐标为反对指示条的开始x坐标,y坐标为反对指示条的开始下坐标y+间隔宽度+文字大小,右边是支持指示条在结束位置绘制Up的数量文本,x的坐标为支持指示条的结束下坐标的x-文本宽度,y坐标为支持指示条的结束下坐标y+间隔宽度+文字大小
    1
    2
    3
    4
    5
    6
    7
    8
    private fun drawText(canvas: Canvas?){
    val downText = "Down: $downCount"
    downPaint.textSize = textSize
    canvas?.drawText(downText, indicator.downPath.startBottom.x, indicator.downPath.startBottom.y + spanWidth + textSize, downPaint)
    val upText = "Up: $upCount"
    upPaint.textSize = textSize
    canvas?.drawText(upText, indicator.upPath.endBottom.x - upPaint.measureText(upText), indicator.upPath.endBottom.y + spanWidth + textSize, upPaint)
    }

    设置支持和反对数量

  • 根据设置的支持反对数量刷新视图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fun updateView(
    up: Int,
    down: Int,
    downColor: Int = ContextCompat.getColor(context, R.color.secondary),
    upColor: Int = ContextCompat.getColor(context, R.color.primary)
    ) {
    this.upCount = up
    this.downCount = down
    this.downColor = downColor
    downPaint.color = downColor
    this.upColor = upColor
    upPaint.color = upColor
    postInvalidate()
    }

    使用

  • xml中声明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <com.example.testapp.CompareIndicator
    android:id="@+id/indicator_compare"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginStart="12dp"
    android:layout_marginTop="200dp"
    android:layout_marginEnd="12dp"
    app:lineWidth="6dp"
    app:textSize="12sp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  • 设置支持反对数量
    1
    compareIndicator.updateView(80,20, ContextCompat.getColor(this, R.color.secondary), ContextCompat.getColor(this, R.color.primary))
    最后附上完整代码:
    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
    package com.example.testapp

    import android.content.Context
    import android.graphics.Canvas
    import android.graphics.Paint
    import android.graphics.Path
    import android.util.AttributeSet
    import android.view.View
    import androidx.core.content.ContextCompat

    class CompareIndicator @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
    ) : View(context, attrs, defStyleAttr) {
    private var downPaint = Paint()
    private var upPaint = Paint()
    private var upCount = 80
    private var downCount = 0
    private var downColor = ContextCompat.getColor(context, R.color.secondary)
    private var upColor = ContextCompat.getColor(context, R.color.primary)
    private var lineWidth = Util.dp2px(6)
    private var attachWidth = Util.dp2px(2)
    private var spanWidth = Util.dp2px(9)
    private var labelSize = 40f
    private var roundSize = lineWidth - Util.dp2px(2)
    private val indicator by lazy { Indicator() }

    init {
    setLayerType(View.LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.compareindicator)
    downColor = typedArray.getColor(R.styleable.compareindicator_down_color, downColor)
    upColor = typedArray.getColor(R.styleable.compareindicator_up_color, upColor)
    lineWidth = typedArray.getDimension(
    R.styleable.compareindicator_lineWidth,
    lineWidth.toFloat()
    ).toInt()
    attachWidth = typedArray.getDimension(
    R.styleable.compareindicator_attachWidth,
    lineWidth.toFloat()
    ).toInt()
    spanWidth = typedArray.getDimension(
    R.styleable.compareindicator_spanWidth,
    lineWidth.toFloat()
    ).toInt()
    labelSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    labelSize
    )
    typedArray.recycle()
    upPaint = Paint()
    upPaint.color = upColor
    downPaint = Paint()
    downPaint.color = downColor
    }

    fun updateView(
    up: Int,
    down: Int,
    downColor: Int = ContextCompat.getColor(context, R.color.secondary),
    upColor: Int = ContextCompat.getColor(context, R.color.primary)
    ) {
    this.upCount = up
    this.downCount = down
    this.downColor = downColor
    downPaint.color = downColor
    this.upColor = upColor
    upPaint.color = upColor
    postInvalidate()
    }

    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    val upWidth: Int
    val downWidth: Int
    if (upCount != 0 && downCount != 0) {
    upWidth = width / (downCount + upCount) * upCount
    downWidth = width / (downCount + upCount) * downCount
    getPath(downWidth)
    drawIndicator(canvas)
    } else if (upCount != 0 && downCount == 0) {
    upWidth = width
    upPaint.strokeWidth = lineWidth.toFloat()
    upPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (upWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    upPaint
    )
    } else if (upCount == 0 && downCount != 0) {
    downWidth = width
    downPaint.strokeWidth = lineWidth.toFloat()
    downPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (downWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    downPaint
    )
    } else {
    upWidth = width / 2
    downWidth = width / 2
    getPath(downWidth)
    drawIndicator(canvas)
    }
    drawText(canvas)
    }

    private fun drawText(canvas: Canvas?){
    val downText = "Down: $downCount"
    downPaint.textSize = labelSize
    canvas?.drawText(downText, indicator.downPath.startBottom.x, indicator.downPath.startBottom.y + spanWidth + labelSize, downPaint)
    val upText = "Up: $upCount"
    upPaint.textSize = labelSize
    canvas?.drawText(upText, indicator.upPath.endBottom.x - upPaint.measureText(upText), indicator.upPath.endBottom.y + spanWidth + labelSize, upPaint)
    }

    private fun drawIndicator(canvas: Canvas?) {
    downPaint.strokeWidth = 1F
    upPaint.strokeWidth = 1F
    downPaint.strokeCap = Paint.Cap.BUTT
    upPaint.strokeCap = Paint.Cap.BUTT
    drawLine(canvas, indicator.downPath, downPaint, true)
    drawLine(canvas, indicator.upPath, upPaint, false)
    }

    private fun drawLine(
    canvas: Canvas?,
    indicatorPath: IndicatorPath,
    paint: Paint,
    down: Boolean
    ) {
    val path = Path()
    if (down) {
    path.moveTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.quadTo(
    indicatorPath.startBottom.x - roundSize,
    indicatorPath.startBottom.y - (lineWidth / 2),
    indicatorPath.startTop.x,
    indicatorPath.startTop.y
    )
    } else {
    path.moveTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.quadTo(
    indicatorPath.endBottom.x + roundSize,
    indicatorPath.endBottom.y - (lineWidth / 2),
    indicatorPath.endTop.x,
    indicatorPath.endTop.y
    )
    }
    path.close()
    paint.strokeWidth = 1f
    paint.style = Paint.Style.FILL
    path.fillType = Path.FillType.WINDING
    canvas?.drawPath(path, paint)
    }

    private fun getPath(downWidth: Int) {
    val paddingSpanStart = paddingStart + roundSize
    // 支持指示条
    indicator.downPath.startTop.x = paddingSpanStart.toFloat()
    indicator.downPath.startTop.y = paddingTop.toFloat()
    indicator.downPath.startBottom.x = paddingSpanStart.toFloat()
    indicator.downPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.downPath.endTop.x =
    (paddingSpanStart + downWidth + attachWidth - spanWidth).toFloat()
    indicator.downPath.endTop.y = paddingTop.toFloat()
    indicator.downPath.endBottom.x = (paddingSpanStart + downWidth - spanWidth).toFloat()
    indicator.downPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    val paddingSpanEnd = paddingEnd + roundSize
    // 反对指示条
    indicator.upPath.startTop.x = indicator.downPath.endTop.x + spanWidth
    indicator.upPath.startTop.y = paddingTop.toFloat()
    indicator.upPath.startBottom.x = indicator.downPath.endBottom.x + spanWidth
    indicator.upPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.upPath.endTop.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endTop.y = paddingTop.toFloat()
    indicator.upPath.endBottom.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    }

    data class Indicator(
    var upPath: IndicatorPath = IndicatorPath(),
    var downPath: IndicatorPath = IndicatorPath()
    )

    data class IndicatorPath(
    var startTop: Pos = Pos(),
    var startBottom: Pos = Pos(),
    var endTop: Pos = Pos(),
    var endBottom: Pos = Pos()
    )

    data class Pos(var x: Float = 0f, var y: Float = 0f)
    }