最近项目中,针对 App
首次启动新手指引做了适配,新手指引大概的效果是如下图这样的,原本项目中使用的是纯图片,下方的箭头和小圆圈是固定显示在中间的,随着项目的不断庞大,这个指引的位置不是固定的,可能在左边也可能在右边,而且更加重要的是后续需要适配平板,如果还是用图片处理就会变得非常困难,所以只能使用代码画出来了,本文将以这个需求为起始点,展示如何使用 Jetpack Compose
进行绘制。我们将从这个示例开始,逐步尝试和了解 Compose
中的各种绘制 API
和方法
开始
首先分析一下这个效果,上方是一个圆角矩形, 圆角矩形底部中间一个小三角形,这个三角形连接着一条线连接着两个重叠的小圆。要使用 Compose 纯代码绘制,起始和 Android
原生差不多,需要使用到 Canvas
和 Path
,直接上代码
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
|
@Composable
fun TestCanvas() {
Canvas(modifier = Modifier.fillMaxSize()) {
val rectWidth = this.size.width // 矩形宽度
val rectHeight = 150.dp.toPx() // 矩形高度
val gradientBrush = Brush.linearGradient( // 绘制渐变色
colors = listOf(Color.Blue, Color.Red, Color.Yellow),
start = Offset.Zero, // 渐变开始的坐标
end = Offset.Infinite // 渐变终点的坐标,
tileMode = TileMode.Clamp
)
Path().apply {
val cornerRadius = 5.dp.toPx() // 定义圆角矩形的四个角的半径
// 定义矩形的边界
val rect = Rect(50f, 50f, rectWidth - 50f, rectHeight - 50f)
// 添加圆角矩形到路径
addRoundRect(
RoundRect(
rect = rect,
cornerRadius = CornerRadius(cornerRadius, cornerRadius) // 圆角
)
)
}
}
}
|
上述代码效果就是这样,首先是使用 Brush(笔刷)这个类达到渐变效果,而 linearGradient
就是线性渐变,linearGradient
方法中,一共有 4 个参数可以配置,其余的参数都在注释上说明了,这里说下 TileMode
参数配置区别,它的默认值是 TileMode.Clamp
- TileMode.Clamp: 当使用
TileMode.Clamp
时,渐变在到达终点后,继续使用终点的颜色填充超出范围的区域。例如,如果渐变从红色到蓝色,超出范围的区域将填充蓝色
- TileMode.Repeated:当使用
TileMode.Repeated
时,渐变在到达终点后,将从起点重新开始,形成一个连续的重复图案。例如,红色到蓝色的渐变会不断重复,填充整个区域
- TileMode.Mirror:当使用
TileMode.Mirror
时,渐变在到达终点后,会镜像反转然后再重复。例如,红色到蓝色的渐变会变成蓝色到红色,然后再重复红色到蓝色,如此循环
绘制好渐变颜色后,再使用 path
的 addRoundRect
添加了一个圆角矩形,配置我们刚刚编写好的渐变颜色,这样就完成了一个渐变颜色的圆角矩形
绘制三角形
接下来,我们再绘制矩形中间底部的三角形,同样也是使用 path,但是这里需要使用的 addPath 添加自定义的路径,这个路径就是三角形的路径
1
2
3
4
5
6
7
8
9
10
11
12
|
// 定义三角形的顶点
val triangleHeight = 30f
val triangleWidth = 60f // 三角形的宽度
val trianglePath = Path().apply {
moveTo(rect.center.x - triangleWidth / 2, rect.bottom) // 三角形右边的点
lineTo(rect.center.x, rect.bottom + triangleHeight) // 三角形底部的点
lineTo(rect.center.x + triangleWidth / 2, rect.bottom) // 三角形右边的点
close() // 闭合路径
}
// 添加三角形到路径
addPath(trianglePath)
|
效果就是这样,代码比较简单,这里我们首先了解一下 moveTo
和 lineTo
的作用
- moveTo(x, y):将当前绘图点移动到指定的 (x, y) 坐标,而不绘制任何线段。通常用于设置起始点或跳过某些区域
- lineTo(x, y):从当前绘图点绘制一条直线到指定的 (x, y) 坐标,并将当前绘图点更新为新的 (x, y) 坐标
所以,上述的代码就是将绘图的起点设置在了 rect.center.x - triangleWidth / 2, rect.bottom
的位置,就是圆角矩形底部中间左边的位置,然后再 lineTo rect.center.x, rect.bottom + triangleHeight
, 这个坐标点就是下面三角形的底部的那个点,最后 lineTo 到三角形右边的那个点,最后调用 close
,闭合整个路径,这样就完成了这个三角形的绘制
画出一条线
1
2
3
4
|
path().apply {
moveTo(rect.center.x, rect.bottom + triangleHeight - 1.dp.toPx()) // 上移 1个 dp 像素,线条衔接的更自然
lineTo(rect.center.x, rect.bottom + lineHeight)
}
|
这里也很简单, 将绘图的起始点移动到三角形的下方区域,笔直的往下,lineTo
画出一条线,其实这里也可以使用 drawLine
圆
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 定义两个小圆的半径
val circleRadius = 4.dp.toPx()
// 定义两个小圆的位置,分别位于线条的底部
val circleCenter = Offset(rect.center.x, rect.bottom + lineHeight)
// 绘制第一个小圆(较为外侧,透明度小)
drawCircle(
brush = gradientBrush,
radius = circleRadius * 2f, // 半径稍大
center = circleCenter, // 中心位置
alpha = 0.5f // 透明度降低
)
// 绘制第二个小圆(较为内侧,透明度大)
drawCircle(
brush = gradientBrush,
radius = circleRadius, // 半径
center = circleCenter // 中心位置
)
|
这部分代码使用 circleRadius
定义圆的半径,circleCenter
设置圆的起始位置,这里设置的位置,也就是底部线条的位置,画出一个内圆和外圆,外圆的半径较大,并且设置浅透明度,就可以看下如下这样扩散的效果一样
至此,整个效果就这样完成了,不管下方的箭头指向何处,我们都可以自定义,这样就没有了图片不能修改和图片会被缩放的局限性,最后完成的代码如下
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
|
@Composable
fun TestCanvas1() {
Canvas(modifier = Modifier.fillMaxSize()) {
val rectWidth = this.size.width
val rectHeight = 150.dp.toPx()
// 定义渐变颜色 线性渐变的画笔
val gradientBrush = Brush.linearGradient(
colors = listOf(Color.Blue, Color.Red, Color.Yellow),
start = Offset.Zero, // 渐变开始的坐标
end = Offset.Infinite // 渐变终点的坐标,
)
var linePath = Path()
val path = Path().apply {
// 定义圆角矩形的四个角的半径
val cornerRadius = 5.dp.toPx()
val lineHeight = 80.dp.toPx()
// 定义矩形的边界
val rect = Rect(50f, 50f, rectWidth - 50f, rectHeight - 50f)
// 添加圆角矩形到路径
addRoundRect(
RoundRect(
rect = rect,
cornerRadius = CornerRadius(cornerRadius, cornerRadius)
)
)
// 定义三角形的顶点
val triangleHeight = 30f
val triangleWidth = 60f
val trianglePath = Path().apply {
moveTo(rect.center.x - triangleWidth / 2, rect.bottom)
lineTo(rect.center.x, rect.bottom + triangleHeight)
lineTo(rect.center.x + triangleWidth / 2, rect.bottom)
close()
}
// 添加三角形到路径
addPath(trianglePath)
// 定义沿着三角形底部的线条路径
linePath.apply {
moveTo(rect.center.x, rect.bottom + triangleHeight - 1.dp.toPx())
lineTo(rect.center.x, rect.bottom + lineHeight)
}
// 使用黄色填充路径
drawPath(
path = this,
brush = gradientBrush,
style = Fill,
)
// 绘制线条并设置线条宽度
drawPath(
path = linePath,
brush = gradientBrush,
style = Stroke(width = 2.dp.toPx()) // 设置线条宽度为4dp
)
// 定义两个小圆的半径
val circleRadius = 4.dp.toPx()
// 定义两个小圆的位置,分别位于线条的底部
val circleCenter = Offset(rect.center.x, rect.bottom + lineHeight)
// 绘制第一个小圆(较为外侧,透明度小)
drawCircle(
brush = gradientBrush,
radius = circleRadius * 2f, // 半径稍大
center = circleCenter,
alpha = 0.5f
)
// 绘制第二个小圆(较为内侧,透明度大)
drawCircle(
brush = gradientBrush,
radius = circleRadius, // 原始半径
center = circleCenter
)
}
}
}
|
上面通过这个小需求使用了 addRoundRect
, drwLine
drawCircle
等方法,基本使用已经大概了解了,他们分别都是位于 DrawScope
和 Path
类中的方法,为了以后面对各种各样的绘制更加的得心应手,接下来乘着这一股劲,了解一下内部所有的方法,和方法中其他可配置的参数
drawLine
画一条线
1
2
3
4
5
6
7
8
9
10
11
|
fun drawLine(
color: Color, // 颜色
start: Offset, // 线条的起点
end: Offset, // 线条的终点
strokeWidth: Float = Stroke.HairlineWidth, // 线条的宽度, 默认值 HairlineWidth 是一个 1 像素的宽度
cap: StrokeCap = Stroke.DefaultCap, // 线条末端的样式, 默认是矩形,可以
pathEffect: PathEffect? = null, // 线条的路径效果,可以用来创建虚线、波浪线等效果
alpha: Float = 1.0f, // 透明值
colorFilter: ColorFilter? = null, // 颜色过滤器
blendMode: BlendMode = DefaultBlendMode // 颜色混合模式
)
|
线条末端的样式
- StrokeCap.Butt 末端是平直的,但不会超出线条的终点,比如说终点是 (100, 0),那么他的终点就就刚好在 (100, 0) 的位置
- StrokeCap.Square 末端是平直的,会超过末端终点,超过一半的距离是线的宽度一半, 比如宽度是 10 dp, 终点是 (100, 0), 那么终点会在 (105, 0), 其实和 Butt 的差别,视觉上就是一个一长一短
StrokeCap.Round 末端是圆弧
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
|
Canvas(modifier = Modifier.fillMaxSize()) {
this.drawLine(
Color.Red,
start = Offset(x = 330f, y = 50f),
end = Offset(x = 330f, y = 1000f),
strokeWidth = 60f,
cap = StrokeCap.Round
)
this.drawLine(
Color.Red,
start = Offset(x = 430f, y = 50f),
end = Offset(x = 430f, y = 1000f),
strokeWidth = 60f,
cap = StrokeCap.Butt
)
this.drawLine(
Color.Red,
start = Offset(x = 530f, y = 50f),
end = Offset(x = 530f, y = 1000f),
strokeWidth = 60f,
cap = StrokeCap.Square
)
}
|
上面的代码,参数配置都比较简单,都差不太多,只是配置了 cap 的差别,运行后,效果如下
线条的路径效果
接下来看看 pathEffect
的效果
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
|
@Composable
fun canvasTest3() {
val dashEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), phase = 0f)
Column(modifier = Modifier.fillMaxSize()) {
Canvas(modifier = Modifier.size(50.dp)) {
drawLine(
color = Color.Red,
start = Offset(10f, 60f),
end = Offset(490f, 80f),
strokeWidth = 20f,
pathEffect = dashEffect
)
}
Row {
Canvas(modifier = Modifier.size(100.dp)) {
val path = Path().apply {
moveTo(50f, 50f)
lineTo(150f, 50f)
lineTo(150f, 150f)
lineTo(50f, 150f)
close()
}
drawPath(
path = path,
color = Color.Blue,
style = Stroke(width = 5f, pathEffect = PathEffect.cornerPathEffect(radius = 30f)) // 圆角 30f
)
}
Canvas(
modifier = Modifier
.padding(start = 50.dp)
.size(100.dp)
) {
val path = Path().apply {
moveTo(50f, 50f)
lineTo(150f, 50f)
lineTo(150f, 150f)
lineTo(50f, 150f)
close()
}
drawPath(
path = path,
color = Color.Blue,
style = Stroke(width = 5f, pathEffect = PathEffect.cornerPathEffect(radius = 5f)) // 圆角 5f
)
}
}
Row {
Canvas(modifier = Modifier.size(200.dp)) {
val path = Path().apply {
moveTo(50f * 2, 50f * 2)
lineTo(150f * 2, 50f * 2)
lineTo(150f * 2, 150f * 2)
lineTo(50f * 2, 150f * 2)
close()
}
drawPath(
path = path,
color = Color.Red,
style = Stroke(
width = 10f, pathEffect = PathEffect.chainPathEffect(
PathEffect.cornerPathEffect(radius = 0f),
PathEffect.dashPathEffect(floatArrayOf(10f, 10f), phase = 0f)
)
)
)
}
Canvas(modifier = Modifier.padding(start = 10.dp).size(200.dp)) {
val path = Path().apply {
moveTo(50f * 2, 50f * 2)
lineTo(150f * 2, 50f * 2)
lineTo(150f * 2, 150f * 2)
lineTo(50f * 2, 150f * 2)
close()
}
drawPath(
path = path,
color = Color.Red,
style = Stroke(
width = 10f, pathEffect = PathEffect.chainPathEffect(
PathEffect.cornerPathEffect(radius = 90f), // 注意, 虚线和圆弧一起设置,看不出来效果,可能是路径被覆盖了
PathEffect.dashPathEffect(floatArrayOf(10f, 10f), phase = 0f)
)
)
)
}
}
}
}
|
上面的代码运行后的效果如下,不做过多解释,感兴趣的看看代码就知道了,基本上就是 path 路径效果的区别
stampedPathEffect 效果
使用一个路径作为模板,在另一条路径上重复绘制该模板, 比如绘制一个三角形的路径,然后一直重复
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
|
@Preview
@Composable
fun DrawStampedEffectPath() {
// 绘制三角形路径
val stampPath = Path().apply {
moveTo(0f, -10f)
lineTo(10f, 10f)
lineTo(-10f, 10f)
close()
}
// 绘制圆形路径
val stampPath = Path().apply {
addOval(androidx.compose.ui.geometry.Rect(0f, 0f, 20f, 20f))
}
// 绘制星星路径
val stampPath = Path().apply {
moveTo(10f, 0f)
lineTo(12f, 7f)
lineTo(20f, 7f)
lineTo(14f, 11f)
lineTo(16f, 18f)
lineTo(10f, 14f)
lineTo(4f, 18f)
lineTo(6f, 11f)
lineTo(0f, 7f)
lineTo(8f, 7f)
close()
}
// Create the stamped path effect
val stampedEffect = PathEffect.stampedPathEffect(
shape = stampPath,
advance = 30f,
phase = 0f,
style = StampedPathEffectStyle.Rotate
)
Canvas(modifier = Modifier.size(300.dp)) {
val path = Path().apply {
moveTo(50f, 50f)
lineTo(250f, 50f)
lineTo(250f, 250f)
lineTo(50f, 250f)
close()
}
drawPath(
path = path,
color = Color.Red,
style = Stroke(
width = 10f, pathEffect = stampedEffect
)
)
}
}
|
colorFilter
颜色过滤器可以用来改变绘制内容的颜色,比如调整亮度、对比度、饱和度,或者应用颜色矩阵
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
|
@Preview
@Composable
fun DrawPathWithColorFilter() {
val colorMatrix = ColorMatrix() // 默认情况
val colorMatrix1 = ColorMatrix().apply {
setToSaturation(0f) // 将饱和度设置为0,创建灰度效果
}
val colorMatrix2 = ColorMatrix().apply {
setToScale(1.2f, 1.2f, 1.2f, 1f) // 增加亮度
}
val colorMatrix3 = ColorMatrix().apply {
setToRotateRed( 45f) // 旋转红色通道
setToRotateGreen( 45f) // 旋转绿色通道
setToRotateBlue(45f) // 旋转蓝色通道
}
val list = listOf(colorMatrix, colorMatrix1, colorMatrix2, colorMatrix3)
Row {
list.forEach {
Canvas(modifier = Modifier.size(90.dp)) {
val path = Path().apply {
moveTo(50f, 50f)
lineTo(250f, 50f)
lineTo(250f, 250f)
lineTo(50f, 250f)
close()
}
drawPath(
path = path,
color = Color.Red,
style = Stroke(width = 10f),
colorFilter = ColorFilter.colorMatrix(it)
)
}
}
}
}
|
上面的代码, 主要是针对 ColorMatrix 对颜色的配置, 代码运行后效果如下
drawCircle
画一个圆
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
|
@Composable
fun CircleTest() {
Row {
Canvas(modifier = Modifier.size(100.dp)) {
drawCircle(
color = Color.Red, // 颜色
radius = 90f, // 半径
center = Offset(this.size.width / 2f, this.size.height / 2f), // 圆的位置
alpha = 1f, // 透明度
style = Fill, // 填充模式
colorFilter = null, // 颜色过滤器
)
}
// 渐变
val brush = Brush.linearGradient(listOf(Color.Red, Color.Green))
Canvas(modifier = Modifier.size(100.dp)) {
drawCircle(
brush = brush,
radius = 90f,
center = Offset(this.size.width / 2f, this.size.height / 2f),
alpha = 1f,
style = Fill,
)
}
}
}
|
由于其余的参数, 上面的示例已经讲解过了, 就不再多说了, 这里我们来看下其他没有用过的, Brush, 它一般是用来设置渐变的,可以有以下几种设置方式
- SolidColor 单一颜色填充
- LinearGradient 线性渐变填充
- RadialGradient 放射状渐变填充
- SweepGradient 扫描渐变填充
而这个颜色渐变方法参数配置, 基本都一致, 我们这里用 LinearGradient 举例
1
2
3
4
5
6
|
fun linearGradient(
colors: List<Color>, // 多个颜色
start: Offset = Offset.Zero, // 渐变开始的坐标
end: Offset = Offset.Infinite,// 渐变终点的坐标
tileMode: TileMode = TileMode.Clamp
)
|
tileMode
- Clamp 渐变在到达终点后,继续使用终点的颜色填充超出范围的区域。例如,如果渐变从红色到蓝色,超出范围的区域将填充蓝色
- Repeated 渐变在到达终点后,将从起点重新开始,形成一个连续的重复图案。例如,红色到蓝色的渐变会不断重复,填充整个区域
- Mirror 渐变在到达终点后,会镜像反转然后再重复。例如,红色到蓝色的渐变会变成蓝色到红色,然后再重复红色到蓝色,如此循环
上面的代码运行后的效果如下
drawRect
绘制一个矩形
1
2
3
4
5
6
7
8
9
10
|
fun drawRect(
color: Color, // 颜色
topLeft: Offset = Offset.Zero, // 矩形的左上角位置,默认为 (0, 0)
size: Size = this.size.offsetSize(topLeft), // 矩形的尺寸,默认为整个画布的尺寸减去 topLeft 的偏移。
/*@FloatRange(from = 0.0, to = 1.0)*/
alpha: Float = 1.0f, // 透明度,范围为 0.0 到 1.0,默认值为 1.0(不透明)。
style: DrawStyle = Fill, // 绘制样式,默认为 Fill(填充矩形),也可以是 Stroke(仅绘制边框)。
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
)
|
参数比较简单,唯一比较陌生的是 DrawStyle,看下这个例子就知道是什么样子了, 一个是全部填充, 一个仅仅绘制边框
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
|
@Composable
fun DrawRectTest() {
Row(modifier = Modifier.fillMaxSize()) {
Canvas(modifier = Modifier.size(100.dp)) {
drawRect(
color = Color.Red,
topLeft = Offset.Zero,
style = Fill
)
}
Canvas(
modifier = Modifier
.padding(start = 30.dp, top = 10.dp)
.size(100.dp)
) {
drawRect(
color = Color.Red,
topLeft = Offset.Zero,
size = Size(300f, 100f),
style = Stroke(width = 10f)
)
}
}
}
|
drawRoundRect
绘制圆角矩形
drawRoundRect 和 drawRect 参数配置和使用上都差不多, 无非就是多了 cornerRadius 圆角的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Preview
@Composable
fun DrawRoundRect1() {
Canvas(
modifier = Modifier
.padding(start = 10.dp)
.size(100.dp)
) {
// 圆角矩形
drawRoundRect(
color = Color.Blue,
cornerRadius = CornerRadius(50f, 50f)
)
}
}
|
drawArc
用于绘制一个弧或扇形
1
2
3
4
5
6
7
8
9
10
11
12
13
|
fun drawArc(
color: Color, // 圆弧的颜色
startAngle: Float, // 圆弧起始角度(以度为单位)
sweepAngle: Float, // 圆弧扫过的角度(以度为单位)
useCenter: Boolean,// 是否连接圆弧的两端到圆心
topLeft: Offset = Offset.Zero, // 圆弧外接矩形的左上角位置,默认为 (0, 0)
size: Size = this.size.offsetSize(topLeft), // 圆弧外接矩形的尺寸,默认为整个画布的尺寸减去 topLeft 的偏移
/*@FloatRange(from = 0.0, to = 1.0)*/
alpha: Float = 1.0f,
style: DrawStyle = Fill, // 填充模式
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
)
|
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
|
@Preview
@Composable
fun DrawArcTest() {
Row {
Canvas(modifier = Modifier.size(100.dp)) {
drawArc(
color = Color.Green,
startAngle = 0f,
sweepAngle = 360f,
useCenter = true,
topLeft = Offset(10f, 10f),
size = Size(200f, 200f),
alpha = 0.8f,
)
}
Canvas(modifier = Modifier.padding(start = 5.dp).size(100.dp)) {
drawArc(
brush = Brush.linearGradient(listOf(Color.Red, Color.Blue)),
startAngle = 0f,
sweepAngle = 270f,
useCenter = false,
topLeft = Offset(10f, 10f),
size = Size(200f, 200f),
alpha = 0.8f,
)
}
Canvas(modifier = Modifier.padding(start = 5.dp).size(100.dp)) {
drawArc(
brush = Brush.linearGradient(listOf(Color.Black, Color.Yellow)),
startAngle = 0f,
sweepAngle = 270f,
useCenter = true,
topLeft = Offset(10f, 10f),
size = Size(200f, 200f),
alpha = 0.8f,
)
}
}
}
|
drawImage
1
2
3
4
5
6
7
8
9
10
11
12
13
|
fun drawImage(
image: ImageBitmap, // 要绘制的图像
srcOffset: IntOffset = IntOffset.Zero, // 图像的偏移量,表示从图像的哪个位置开始绘制
srcSize: IntSize = IntSize(image.width, image.height), // 图像的尺寸,表示从源图像中截取的区域大小,默认为整个图像的尺寸
dstOffset: IntOffset = IntOffset.Zero, // 目标位置的偏移量,表示在画布上从哪个位置开始绘制图像,默认为 (0,0)
dstSize: IntSize = srcSize, // 目标尺寸,表示图像在画布上的绘制大小,默认为源图像的尺寸
/*@FloatRange(from = 0.0, to = 1.0)*/
alpha: Float = 1.0f,
style: DrawStyle = Fill, // 图像的填充模式
colorFilter: ColorFilter? = null, // 颜色过滤器
blendMode: BlendMode = DefaultBlendMode, // 混合模式
filterQuality: FilterQuality = DefaultFilterQuality // 过滤质量,用于指定图像缩放时的过滤质量
)
|
这里有些区别需要注意的是 srcOffset dstOffset 的偏移量,假设 srcOffset 是 (50, 50) 它是从图像的位置上截图的图像的,所以这样图片会被裁切掉一部分,
而 dstOffset 是画布的偏移量,同样设置 (50, 50) 图片会相对画布的位置进行偏移,还有一个参数 filterQuality 是图片绘制的质量,默认有低中高几个参数配置
看下如下代码的示例, 感受一下 srcOffset dstOffset 的区别
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
|
@Composable
fun DrawImageTest() {
val imageBitmap = ImageBitmap.imageResource(id = R.mipmap.qqq)
Row {
Canvas(modifier = Modifier.size(100.dp), onDraw = {
/** 原始图像 **/
drawImage(
image = imageBitmap,
)
})
Canvas(modifier = Modifier.padding(start = 20.dp).size(100.dp), onDraw = {
drawImage(
image = imageBitmap,
srcOffset = IntOffset(50, 50) //在源图像的 50,50 位置绘制
)
})
Canvas(modifier = Modifier.padding(start = 20.dp).size(200.dp), onDraw = {
drawImage(
image = imageBitmap,
srcOffset = IntOffset(50, 50),
dstOffset = IntOffset(50, 50), // 设置画布的偏移
)
})
}
}
|
drawOval
用于绘制一个椭圆, 其实也可以用来绘制一个完整的圆
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Preview
@Composable
fun DrawOvalTest() {
Canvas(modifier = Modifier.size(200.dp)) {
// drawCircle() 画一个完美的圆, 区别是 半径和中心点可以自定义
// drawArc 函数用于绘制一个弧或扇形
// drawOval 函数用于绘制一个椭圆, 但是也可以用来绘制一个完整的圆
drawOval(
color = Color.Red, // 颜色
topLeft = Offset(20f, 40f), // 椭圆左上角的位置
size = Size(100f, 50f) // 椭圆的大小
)
}
}
|
drawOval 函数也比较简单, 画一个椭圆, topLeft 是距离左上角的位置, size 是绘制椭圆的大小, 它也可以绘制一个完成的圆, 只要设置 size 的 x, y 坐标相等即可,
这里需要注意一下 drawCircle 和 drawArc drawOval 我们上面的示例都用过了, 他们都可以绘制一个完整的圆, 但是他们的使用场景或者是他们本身的定义是不一样的, 需要注意
drawCircle 画一个完美的圆, 区别是半径和中心点可以自定义
drawArc 用于绘制一个弧或扇形
drawOval 用于绘制一个椭圆, 但是也可以用来绘制一个完整的圆, 设置的 size 相等就可以
DrawPoints
用于绘制一组点的函数 它允许你在画布上绘制多个点,并且可以通过不同的参数来配置这些点的样式和位置
1
2
3
4
5
6
7
8
9
10
11
|
fun drawPoints(
points: List<Offset>, // 表示要绘制的点的坐标
pointMode: PointMode, // 一个 PointMode 枚举值,决定点的连接方式
color: Color,
strokeWidth: Float = Stroke.HairlineWidth,
cap: StrokeCap = StrokeCap.Butt,
pathEffect: PathEffect? = null,
@FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
)
|
有一些参数上面都介绍过了, 就不再解释了, 看下这里出现的一些新的参数
PointMode
Points: 绘制独立的点
Lines: PointMode.Lines 两坐标点形成线段,不够两坐标最后一个坐标点自动省略不显示
Polygon: 多边形,多坐标点连线,和 drawPath 绘制路径类似, 连接点列表中的所有点
strokeWidth: 点的宽度或线条的宽度
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
|
@Composable
fun DrawPointsTest() {
Column {
Row {
Canvas(modifier = Modifier.size(100.dp)) {
drawPoints(
points = listOf(
Offset(50f, 50f),
Offset(150f, 150f),
Offset(100f, 350f)
),
pointMode = PointMode.Points,
color = Color.Red,
strokeWidth = 50f,
cap = StrokeCap.Round
)
}
Canvas(modifier = Modifier.size(100.dp)) {
drawPoints(
points = listOf(
Offset(50f, 50f),
Offset(150f, 150f),
Offset(100f, 350f)
),
pointMode = PointMode.Points,
color = Color.Red,
strokeWidth = 50f,
cap = StrokeCap.Butt
)
}
Canvas(modifier = Modifier.size(100.dp)) {
drawPoints(
points = listOf(
Offset(50f, 50f),
Offset(150f, 150f),
Offset(100f, 350f)
),
pointMode = PointMode.Points,
color = Color.Red,
strokeWidth = 50f,
cap = StrokeCap.Square
)
}
}
Row {
Canvas(
modifier = Modifier
.padding(top = 90.dp)
.size(100.dp)
) {
drawPoints(
points = listOf(
Offset(50f, 50f),
Offset(150f, 150f),
Offset(100f, 350f)
),
pointMode = PointMode.Lines,
color = Color.Red,
strokeWidth = 50f
)
}
Canvas(
modifier = Modifier
.padding(top = 90.dp)
.size(100.dp)
) {
drawPoints(
points = listOf(
Offset(50f, 50f),
Offset(150f, 150f),
Offset(100f, 350f)
),
pointMode = PointMode.Lines,
color = Color.Red,
strokeWidth = 50f,
cap = StrokeCap.Butt
)
}
Canvas(
modifier = Modifier
.padding(top = 90.dp)
.size(100.dp)
) {
drawPoints(
points = listOf(
Offset(50f, 50f),
Offset(150f, 150f),
Offset(100f, 350f)
),
pointMode = PointMode.Lines,
color = Color.Red,
strokeWidth = 50f,
cap = StrokeCap.Square // 会更长一点, 会延伸 strokeWidth 宽度的一半
)
}
}
Canvas(modifier = Modifier.size(100.dp)) {
drawPoints(
points = listOf(
Offset(50f, 50f),
Offset(150f, 150f),
Offset(100f, 350f)
),
pointMode = PointMode.Polygon,
color = Color.Red,
strokeWidth = 50f
)
}
}
}
|
上述代码主要是配置 pointMode 和 cap 参数不同, 看看他们之间的区别, 其余的基本都一致, 代码运行后效果是这样的
DrawPath
根据路径可绘制复杂的图案
1
2
3
4
5
6
7
8
|
fun drawPath(
path: Path
color: Color,
@FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
style: DrawStyle = Fill, // 填充模式
colorFilter: ColorFilter? = null, // 颜色过滤器
blendMode: BlendMode = DefaultBlendMode
)
|
DrawPath 的可以配置参数不多, 因为大部分主要是根据 Path 类的路径来绘制的, 配置基本都在 Path 这个类里面 addxxx 相关的方法, 先来看看一个简单的例子, 绘制一个三角形
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
fun DrawPathTest() {
Canvas(modifier = Modifier.fillMaxSize()) {
// 绘制一个三角形
val path = Path().apply {
moveTo(50f, 50f)
lineTo(150f, 50f)
lineTo(100f, 150f)
close() // 将路径的终点 (100, 150) 连接到起点 (50, 50),形成一个封闭的三角形
}
drawPath(path, color = Color.Red, style = Stroke(width = 4f))
}
}
|
这里使用 Path 绘制一段三角形的路径, 代码比较简单, 主要是 moveTo 和 lineTo 的运用, 它们是什么意思呢 ?
moveTo(x, y):将当前绘图点移动到指定的 (x, y) 坐标,而不绘制任何线段。这通常用于设置起始点或跳过某些区域
lineTo(x, y):从当前绘图点绘制一条直线到指定的 (x, y) 坐标,并将当前绘图点更新为新的 (x, y) 坐标
close: 关闭当前路径, 将路径的最后一个点连接到路径的第一个点,从而形成一个封闭的形状
然后我们再来看看 Path 类中, 其他的一些方法, 一个一个解析, 并看看他们是如何使用的
路径操作方法
reset() 重置路径,清除所有的内容,使路径变为空
rewind() 重置路径,但保留内部数据结构以便重用,效率比 reset() 高
transform(matrix: Matrix) 应用一个矩阵变换到路径上,可以用于旋转、缩放、平移等操作
fillType 设置路径的填充类型,可以是 Winding 或 EvenOdd(),决定路径的填充规则
isConvex 检查路径是否为凸形状
isEmpty 检查路径是否为空
添加形状方法
addArc(rect: Rect, startAngle: Float, sweepAngle: Float) 添加一个圆弧到路径中,指定矩形区域、起始角度和扫过的角度
addArcRad(rect: Rect, startAngleRad: Float, sweepAngleRad: Float) 与
addArc 类似,但使用弧度而不是角度
addOval(rect: Rect) 添加一个椭圆到路径中,指定矩形区域
addRect(rect: Rect) 添加一个矩形到路径中
addPath(path: Path) 将另一个路径添加到当前路径中
addRoundRect(rect: Rect) 添加一个圆角矩形到路径中,指定矩形区域和圆角半径
addOutline(outline: Outline) 添加一个轮廓到路径中
曲线和弧线方法
arcTo(rect: Rect, startAngle: Float, sweepAngle: Float, forceMoveTo: Boolean) 添加一个圆弧到路径中,指定矩形区域、起始角度和扫过的角度,forceMoveTo 决定是否移动到圆弧的起点
arcToRad(rect: Rect, startAngleRad: Float, sweepAngleRad: Float, forceMoveTo: Boolean) 与 arcTo 类似,但使用弧度而不是角度
cubicTo(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float) 添加一个三次贝塞尔曲线到路径中,指定控制点和终点
quadraticBezierTo(x1: Float, y1: Float, x2: Float, y2: Float) 添加一个二次贝塞尔曲线到路径中,指定控制点和终点
相对路径方法
relativeCubicTo(dx1: Float, dy1: Float, dx2: Float, dy2: Float, dx3: Float, dy3: Float) 添加一个相对三次贝塞尔曲线到路径中,指定相对于当前点的控制点和终点
relativeLineTo(dx: Float, dy: Float) 添加一条相对直线到路径中,指定相对于当前点的终点
relativeMoveTo(dx: Float, dy: Float) 将当前点相对移动到新的位置
relativeQuadraticBezierTo(dx1: Float, dy1: Float, dx2: Float, dy2: Float) 添加一个相对二次贝塞尔曲线到路径中,指定相对于当前点的控制点和终点
其他方法
op(path1: Path, path2: Path, operation: PathOperation) 对两个路径执行布尔操作,如联合、交集、差集等
translate(dx: Float, dy: Float) 将路径平移指定的距离
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Preview
@Composable
private fun transformTest() {
Canvas(modifier = Modifier.size(200.dp)) {
val matrix = Matrix().apply {
scale(x = 1.5f) // x 缩放 1.5 倍数
// 绕 Z 轴旋转 20 度
this.rotateZ(20f) // 也可以做旋转, 但是一般用于3D图形变换
}
val path = Path().apply {
addRect(Rect(50f, 50f, 150f, 150f)) // 添加坐标点
transform(matrix) // 应用旋转变换
}
// rotate 是 drawScope 中的方法, 是旋转画布的意思
rotate(64f, pivot = Offset(100f, 100f)) { // pivot 设置旋转中心点
drawPath(path, color = Color.Blue)
}
}
}
|
这部分代码是使用 Matrix 创建矩阵, 对图片缩放 1.5 倍, scale 内部有 3个参数,x, y , z 上述填写一个参数就是对 x 轴放大 1.5 倍数, 然后针对 Z 轴旋转, 再应用到 transform 中, 再绘制出 path 路径, 需要注意的是,这里有两个旋转的方法,一个是 rotateZ 是针对图片的,一个 rotate 是针对画布的,效果就是这样的
addArc
添加一个圆弧到路径中,指定矩形区域、起始角度和扫过的角度 (使用的是角度)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Preview
@Composable
private fun PathAddArc() {
Canvas(modifier = Modifier.size(200.dp)) {
// 绘制圆弧或椭圆弧
val path = Path().apply {
// 画一个椭圆路径
val oval = Rect(50f, 50f, 150f, 150f)
addArc(oval = oval, startAngleDegrees = 0f, sweepAngleDegrees = 240f)
}
drawPath(path, color = Color.Red)
}
}
|
参数说明
oval
: 定义弧线所在的矩形区域。弧线将被绘制在这个矩形的内切圆或椭圆上
startAngleRadians
: 弧线的起始角度,以弧度为单位。0 弧度表示从矩形的右侧中点开始,角度按顺时针方向增加
sweepAngleRadians
: 弧线的扫过角度,以弧度为单位。正值表示顺时针方向,负值表示逆时针方向
addArcRad
与 addArc 类似,但使用弧度而不是角度
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Preview
@Composable
private fun PathAddArcRad() {
Canvas(modifier = Modifier.size(200.dp)) {
val path = Path().apply {
// 定义弧线所在的矩形区域
val oval = Rect(50f, 50f, 150f, 150f)
// 添加弧线,起始角度为0弧度,扫过角度为π弧度(180度)
addArcRad(oval, 0f, Math.PI.toFloat())
}
drawPath(path, color = Color.Red)
}
}
|
弧度和角度的区别
参数说明
oval
: 定义弧线所在的矩形区域。弧线将被绘制在这个矩形的内切圆或椭圆上
startAngleDegrees
: 弧线的起始角度,以度为单位。0 度表示从矩形的右侧中点开始,角度按顺时针方向增加
sweepAngleDegrees
: 弧线的扫过角度,以度为单位。正值表示顺时针方向,负值表示逆时针方向
AddOval
指定矩形区域, 添加一个椭圆到路径中
1
2
3
4
5
6
7
8
9
10
11
|
@Preview
@Composable
private fun PathOvalTest() {
Canvas(modifier = Modifier.size(200.dp).background(Color.Yellow)) {
val path = Path().apply {
val oval = Rect(50f, 50f, 150f, 150f)
addOval(oval)
}
drawPath(path, color = Color.Red)
}
}
|
效果如下,可以看出是一个完整的圆形,通过 addOval
, 和 addArc
, addArcRad
的绘制,发现都可以绘制一个完整的圆,但是他们还是有一些区分的
addOval
: 添加一个椭圆路径,Oval
是代表椭圆的意思,它设置的是一个矩形 Rect
对象,如果设置这个矩形是一个正方形,那么绘制的出来的刚好是一个完整的圆
addArc
: 添加一个弧形路径,arc
是代表弧的意思,除了也需要设置 Rect
参数,还有另外两个参数,设置的是角度
addArcRad
: 和 addArcRad
类似, 也是添加一个弧形路径,但是设置的是弧度,其中 Rad
是 Radian
的缩写,代表的是弧度,一个 π 弧度表示 180°
addOutline
将一个轮廓添加到路径中
- 矩形轮廓 (
Outline.Rectangle
)
- 圆角矩形轮廓(
Outline.Rounded
)
通用路径 (Outline.Generic
)
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
|
@Preview
@Composable
fun PathOutline() {
Canvas(modifier = Modifier.padding(10.dp).size(300.dp)) {
val path = Path().apply {
// 添加矩形轮廓
addOutline(Outline.Rectangle(Rect(0f, 0f, 100f, 100f)))
// 添加圆角矩形轮廓
addOutline(Outline.Rounded(RoundRect(Rect(110f, 0f, 210f, 100f), 16f, 16f)))
// 添加通用路径轮廓
val customPath = Path().apply {
moveTo(220f, 0f)
lineTo(270f, 50f)
lineTo(220f, 100f)
close()
}
addOutline(Outline.Generic(customPath))
}
// 使用 path 进行绘制(也就是上面添加好的路径)
drawPath(path, color = Color.Black)
}
}
|
上述代码分别通过 addOutline 添加了一个矩形路径, 圆角路径,和自定义的路径,其实 addOutline 的方法实现是这样的
1
2
3
4
5
|
fun Path.addOutline(outline: Outline) = when (outline) {
is Outline.Rectangle -> addRect(outline.rect)
is Outline.Rounded -> addRoundRect(outline.roundRect)
is Outline.Generic -> addPath(outline.path)
}
|
可以发现本质上也还是调用 addRect addRoundRect addPath 这些我们之前介绍过的方法,它只是做了再一层的封装而已,其实你也可以直接使用这 3个 Api,其实还有 DrawScope 中,也有一个类似一个方法叫 drawOutLine 基本也是这样的套路,上述代码效果如下
addPath
将另一个路径添加到当前路径中,可用于组合路径
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
|
@Preview
@Composable
private fun PathAddRect() {
Canvas(
modifier = Modifier.size(400.dp)
) {
val path1 = Path().apply {
moveTo(10f, 10f)
lineTo(90f, 10f)
lineTo(90f, 90f)
lineTo(10f, 90f)
close()
}
val path2 = Path().apply {
moveTo(50f, 50f)
lineTo(150f, 50f)
lineTo(150f, 150f)
lineTo(50f, 150f)
close()
}
// 将路径1和路径2合并为一个路径
val combinedPath = Path().apply {
addPath(path1, offset = Offset(x = 10f, y = 10f))
addPath(path2)
}
// 使用 combinedPath 进行绘制
drawPath(combinedPath, color = Color.Black)
}
}
|
好了,至此,关于 compose 中所有关于绘制的方法,都讲完了,也基本都使用示例演示了一遍,基本上所有的方法都在 DrawScope 和 Path 类当中,现在你可以回顾一下里面的方法都是什么意思,它们都有什么样的能力,如果你都掌握了这些,基本对这个绘制应该问题大不了