最近工作比较忙,项目压力大,又是很长一段时间没有更新blog了。今天虽然是放五一长假,但是由于疫情仍然没有结束,所以没有出游的计划,正好可以写点东西总结一下最近项目中遇到的自定义View相关的内容,实现一个总量一定,支持和反对人数对比的指示条来反映某个问题下大多数人的看法,欢迎交流和讨论~
最终效果图:
变量声明
- 声明支持和反对的绘制画笔,数量以及颜色,定义指示条的宽度,中间间隔的宽度,以及三角分割的宽度,圆角大小,指示条坐标对象
1
2
3
4
5
6
7
8
9
10
11private 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
13data 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
27init {
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
39override 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
23private 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
45private 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
8private 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
14fun 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
204package 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 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)
}