Jetpack Compose-Canvas常用方法及示例

前言

DrawScope点进去查看代码,主要有如下几个方法

  • drawLine 画线
  • drawRect 画矩形
  • drawRoundRect 画圆角矩形
  • drawImage 绘制图片
  • drawCircle 画圆形
  • drawOval 画椭圆形
  • drawArc 画弧度跟扇形
  • drawPath 画路径
  • drawPoints 画点 还有如下几个扩展方法
  • inset 将DrawScope坐标空间平移
  • translate 平移坐标
  • rotate(旋转坐标)讲的是旋转了多少角度,rotateRad(旋转坐标)讲的是旋转了多少弧度
  • scale 缩放坐标
  • clipRect 裁剪矩形区域,绘制在裁剪好的矩形区域内。ClipOp.Difference从当前剪辑中减去提供的矩形。
  • clipPath 裁剪路径
  • drawIntoCanvas 直接提供底层画布
  • withTransform 组合转换

图形绘制

绘制线

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
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.tooling.preview.Preview

@Preview
@Composable
fun CustomDrawView() {
Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
val w = size.width
val h = size.height
// 蓝色的线
drawLine(
start = Offset(100f, 100f),
end = Offset(w - 100f, 100f),
color = Color.Blue,
strokeWidth = 10f,
cap = StrokeCap.Square,
alpha = 1f
)

// 渐变的线
drawLine(
brush = Brush.linearGradient(
0.0f to Color.Red,
0.3f to Color.Green,
1.0f to Color.Blue,
start = Offset(100f, 150f),
end = Offset(w - 100f, 150f),
tileMode = TileMode.Repeated
),
start = Offset(100f, 150f),
end = Offset(w - 100f, 150f),
strokeWidth = 10f,
cap = StrokeCap.Round,
alpha = 1f
)

// 横向渐变的线
drawLine(
brush = Brush.horizontalGradient(
0.0f to Color.Red,
0.3f to Color.Green,
1.0f to Color.Blue,
startX = 100f,
endX = w - 100f,
tileMode = TileMode.Mirror
),
start = Offset(100f, 200f),
end = Offset(w - 100f, 200f),
strokeWidth = 10f,
cap = StrokeCap.Butt,
alpha = 1f
)

// 竖直渐变的线
drawLine(
brush = Brush.verticalGradient(
0.0f to Color.Red,
0.3f to Color.Green,
1.0f to Color.Blue,
startY = 250f,
endY = 350f,
tileMode = TileMode.Clamp
),
start = Offset(100f, 250f),
end = Offset(100f, 350f),
strokeWidth = 10f,
cap = StrokeCap.Round,
alpha = 1f
)

// 横向渐变的有 pathEffect.dashPathEffect的虚线
drawLine(
brush = Brush.horizontalGradient(
0.0f to Color.Red,
0.5f to Color.Yellow,
1.0f to Color.Blue,
startX = 100f,
endX = w - 100f,
tileMode = TileMode.Clamp
),
start = Offset(100f, 450f),
end = Offset(w - 100f, 450f),
strokeWidth = 10f,
cap = StrokeCap.Butt,
alpha = 1f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(100f, 20f), 10f)
)
})
}

效果

image-20250819144041191

绘制矩形

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
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview

@Preview
@Composable
fun CustomDrawView() {
Canvas(modifier = Modifier.fillMaxSize()) {
val w = size.width
val h = size.height
// 蓝底的矩形
drawRect(
color = Color.Blue,
topLeft = Offset(100f, 100f),
size = Size(100f, 100f),
alpha = 1f,
style = Fill
)

// 渐变的矩形
drawRect(
brush = Brush.linearGradient(
0.0f to Color.Red,
0.3f to Color.Green,
1.0f to Color.Blue,
start = Offset(300f, 100f),
end = Offset(300f, 200f),
tileMode = TileMode.Repeated
),
topLeft = Offset(300f, 100f),
size = Size(100f, 100f),
alpha = 1f,
style = Fill
)

// 线框的矩形
drawRect(
color = Color.Blue,
topLeft = Offset(100f, 300f),
size = Size(100f, 100f),
alpha = 1f,
style = Stroke(width = 1f, cap = StrokeCap.Butt)
)

// 线框的矩形
drawRoundRect(
color = Color.Blue,
topLeft = Offset(300f, 300f),
size = Size(300f, 100f),
alpha = 1f,
style = Fill,
cornerRadius = CornerRadius(10f, 10f)
)

// 渐变的矩形
drawRoundRect(
brush = Brush.linearGradient(
0.0f to Color.Red,
0.3f to Color.Green,
1.0f to Color.Blue,
start = Offset(300f, 450f),
end = Offset(600f, 550f),
tileMode = TileMode.Repeated
),
topLeft = Offset(300f, 450f),
size = Size(300f, 100f),
alpha = 1f,
style = Fill,
cornerRadius = CornerRadius(10f, 10f)
)
}
}

效果

image-20250819153412487

绘制图片

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
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import com.xhkjedu.zxs_android.R

@Preview
@Composable
fun CustomDrawView() {
val imageBitmap = ImageBitmap.imageResource(id = R.drawable.login_logo)
Canvas(modifier = Modifier.fillMaxSize()) {
val w = size.width
val h = size.height

drawImage(
image = imageBitmap,
topLeft = Offset(50f, 50f),
alpha = 1f,
style = Fill
)

drawImage(
image = imageBitmap,
srcOffset = IntOffset(0, 0),
srcSize = IntSize(imageBitmap.width, imageBitmap.height),
dstOffset = IntOffset(50, imageBitmap.height + 100),
dstSize = IntSize(200, 200),
blendMode = BlendMode.SrcOver
)
}

}

绘制圆形

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
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview

@Preview
@Composable
fun CustomDrawView() {
Canvas(modifier = Modifier.fillMaxSize()) {
val w = size.width
val h = size.height

drawCircle(
color = Color.Red,
radius = 100f,
center = Offset(150f, 150f),
alpha = 1f,
style = Fill
)

drawCircle(
color = Color.Red,
radius = 100f,
center = Offset(400f, 150f),
alpha = 1f,
style = Stroke(width = 5f)
)

drawCircle(
brush = Brush.radialGradient(
0.0f to Color.Red,
0.5f to Color.Green,
1.0f to Color.Blue,
center = Offset(150f, 360f),
radius = 100f,
tileMode = TileMode.Clamp
),
radius = 100f,
center = Offset(150f, 360f),
alpha = 1f,
style = Fill
)
}
}

效果

image-20250819154016012

绘制椭圆

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
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview

@Preview
@Composable
fun CustomDrawView() {
Canvas(modifier = Modifier.fillMaxSize()) {
val w = size.width
val h = size.height

drawOval(
color = Color.Red,
topLeft = Offset(50f, 50f),
size = Size(200f, 100f),
alpha = 1f,
style = Stroke(width = 5f)
)

drawOval(
color = Color.Red,
topLeft = Offset(300f, 50f),
size = Size(200f, 100f),
alpha = 1f,
style = Fill
)

drawOval(
brush = Brush.horizontalGradient(
0.0f to Color.Red,
0.5f to Color.Green,
1.0f to Color.Blue,
startX = 50f,
endX = 250f,
),
topLeft = Offset(50f, 180f),
size = Size(200f, 100f),
alpha = 1f,
style = Fill
)
}
}

绘制弧跟扇形

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
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview

@Preview
@Composable
fun CustomDrawView() {
Canvas(modifier = Modifier.fillMaxSize()) {
val w = size.width
val h = size.height

drawArc(
color = Color.Red,
startAngle = 90f,
sweepAngle = 100f,
useCenter = false,
alpha = 1f,
style = Fill,
topLeft = Offset(50f, 50f),
size = Size(200f, 200f)
)

drawArc(
color = Color.Red,
startAngle = 0f,
sweepAngle = 100f,
useCenter = true,
alpha = 1f,
style = Fill,
topLeft = Offset(150f, 50f),
size = Size(200f, 200f)
)

drawArc(
color = Color.Red,
startAngle = 0f,
sweepAngle = 100f,
useCenter = false,
alpha = 1f,
style = Stroke(width = 5f),
topLeft = Offset(300f, 50f),
size = Size(200f, 200f)
)

drawArc(
color = Color.Red,
startAngle = 0f,
sweepAngle = 100f,
useCenter = true,
alpha = 1f,
style = Stroke(width = 5f),
topLeft = Offset(500f, 50f),
size = Size(200f, 200f)
)

drawArc(
brush = Brush.horizontalGradient(
0.0f to Color.Red,
0.5f to Color.Green,
1.0f to Color.Blue,
startX = 50f,
endX = 250f,
),
startAngle = 0f,
sweepAngle = 100f,
useCenter = true,
alpha = 1f,
style = Stroke(width = 5f),
topLeft = Offset(50f, 350f),
size = Size(200f, 200f)
)

drawArc(
brush = Brush.horizontalGradient(
0.0f to Color.Red,
0.5f to Color.Green,
1.0f to Color.Blue,
startX = 200f,
endX = 450f,
),
startAngle = 0f,
sweepAngle = 100f,
useCenter = true,
alpha = 1f,
style = Fill,
topLeft = Offset(200f, 350f),
size = Size(200f, 200f)
)
}
}

效果

image-20250819154248179

绘制路径

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
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview

@Preview
@Composable
fun CustomDrawView() {
Canvas(modifier = Modifier.fillMaxSize()) {
val path = Path()
path.moveTo(50f, 50f)
path.lineTo(50f, 150f)
path.lineTo(150f, 50f)
path.close()
drawPath(
path = path,
color = Color.Red
)

path.moveTo(50f, 200f)
path.lineTo(50f, 350f)
path.lineTo(200f, 200f)
path.close()
drawPath(
path = path,
color = Color.Red,
style = Stroke(width = 4f)
)
}
}

效果

image-20250819154522624

移动

inset 平移

inset是将DrawScope坐标空间平移

inset的代码如下:

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
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.inset
import androidx.compose.ui.tooling.preview.Preview

@Preview
@Composable
fun CustomDrawView() {
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasQuadrantSize = Size(200f, 200f)
drawRect(
color = Color.Red,
size = canvasQuadrantSize
)

inset(canvasQuadrantSize.width, canvasQuadrantSize.height) {
drawRect(
color = Color.Green,
size = canvasQuadrantSize
)
}
}
}

效果

image-20250819155032291

translate 平移坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.tooling.preview.Preview

@Preview
@Composable
fun CustomDrawView() {
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasQuadrantSize = Size(200f, 200f)
translate(100f, 100f) {
drawRect(
color = Color.Blue,
size = canvasQuadrantSize
)
}
}
}

效果

image-20250819155432312

BlendMode混合模式

混合模式

混合是有个原图像src,一个dst目标图像,这两个图的相交区域的各种组合情况。

  • Clear 清除 (删除源图像和目标图像,不留下任何内容)
  • Src 删除目标图像,只绘制源图像
  • Dst 删除源图像,只绘制目标图像
  • SrcOver 源图和目标图合成,源图在上
  • DstOver 目标图和源图合成,目标图在上
  • SrcIn 显示源图和目标图相交的部分,并且只显示源图像
  • DstIn 显示目标图和源图相交的部分,并且只显示目标图
  • SrcOut 显示源图像和目标图不相交的部分,并且只显示源图
  • DstOut 显示目标图和源图像不相交的部分,并且只显示目标图
  • SrcAtop 显示目标图,并且在相交的地方显示源图
  • DstAtop 显示源图,并在相交的地方显示目标图
  • Xor 显示源图和目标图,但相交的位置不显示(代码注释:对源图像和目标图像应用按位异或运算符。这就使得它们重叠的地方保持透明。)
  • Plus 对源映像和目标映像的组件求和。其中一个图像的像素中的透明度降低了该图像对相应输出像素的贡献,就好像该图像中该像素的颜色较暗一样
  • Modulate 将源图像和目标图像的颜色分量相乘。这只能产生相同或较深的颜色(乘以白色,1.0,结果不变;乘以黑色(0.0,结果为黑色)。合成两个不透明图像时,这与在投影仪上重叠两个透明胶片的效果类似。对于同样乘以alpha通道的变量,请考虑乘以。
  • Screen 将源图像和目标图像的分量的逆相乘,然后求逆结果。反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。这基本上与调制混合模式相同,但是在乘法之前颜色值反转,结果在渲染之前反转回来。这只能产生相同或较浅的颜色(乘以黑色,1.0,结果不变;乘以白色(0.0,结果为白色)。类似地,在alpha通道中,它只能产生更不透明的颜色。这与两台投影仪同时在同一屏幕上显示图像的效果相似。
  • Overlay 将源图像和目标图像的分量相乘,然后调整它们以支持目标。具体来说,如果目标值较小,则将其与源值相乘,而如果源值较小,则将源值的倒数与目标值的倒数相乘,然后反转结果。反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。
  • Darken 通过从每个颜色通道中选择最低值来合成源图像和目标图像。输出图像的不透明度的计算方法与SrcOver相同。
  • Lighten 通过从每个颜色通道中选择最高值来合成源图像和目标图像。输出图像的不透明度的计算方法与SrcOver相同。
  • ColorDodge 将目标除以源的倒数。反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。注意这个BlendMode只能在androidapi级别29及以上使用
  • ColorBurn :将目标的倒数除以源的倒数,然后求结果的倒数。反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。注意这个BlendMode只能在androidapi级别29及以上使用
  • Hardlight : 将源图像和目标图像的分量相乘,然后调整它们以有利于源图像。具体来说,如果源值较小,则将其与目标值相乘,而如果目标值较小,则将目标值的倒数与源值的倒数相乘,然后反转结果。反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。注意这个BlendMode只能在androidapi级别29及以上使用
  • Softlight: 对于低于0.5的源值,使用ColorDodge;对于高于0.5的源值,使用ColorBurn。这会产生类似的效果,但比叠加效果更柔和。注意这个BlendMode只能在androidapi级别29及以上使用
  • Difference: 从每个通道的较大值中减去较小的值。合成黑没有效果;合成白色将反转其他图像的颜色。输出图像的不透明度的计算方法与SrcOver相同。注意这个BlendMode只能在androidapi级别29及以上使用这种影响类似于排斥,但更为严厉。
  • Exclusion: 从两个图像的总和中减去两个图像乘积的两倍。合成黑没有效果;合成白色将反转其他图像的颜色。输出图像的不透明度的计算方法与SrcOver相同。注意这个BlendMode只能在androidapi级别29及以上使用效果类似于差异,但更柔和。
  • Multiply: 将源图像和目标图像的分量相乘,包括alpha通道。这只能产生相同或较深的颜色(乘以白色,1.0,结果不变;乘以黑色(0.0,结果为黑色)。由于alpha通道也会相乘,因此一个图像中的完全透明像素(不透明度0.0)会导致输出中的完全透明像素。这与DstIn类似,但颜色组合在一起。
  • Hue: 获取源图像的色调,以及目标图像的饱和度和亮度。其效果是用源图像着色目标图像。输出图像的不透明度的计算方法与SrcOver相同。在源图像中完全透明的区域从目标图像获取其色调。注意这个BlendMode只能在androidapi级别29及以上使用
  • Saturation: 获取源图像的饱和度,以及目标图像的色调和亮度。输出图像的不透明度的计算方法与SrcOver相同。在源图像中完全透明的区域从目标图像获取其饱和度。注意这个BlendMode只能在androidapi级别29及以上使用
  • Color :获取源图像的色调和饱和度,以及目标图像的亮度。其效果是用源图像着色目标图像。输出图像的不透明度的计算方法与SrcOver相同。源图像中完全透明的区域从目标处获取其色调和饱和度。注意这个BlendMode只能在androidapi级别29及以上使用
  • Luminosity :获取源图像的亮度,以及目标图像的色调和饱和度。输出图像的不透明度的计算方法与SrcOver相同。在源图像中完全透明的区域从目标图像获取其亮度。注意这个BlendMode只能在androidapi级别29及以上使用

混合示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Canvas(
modifier = Modifier
.size(300.dp, 300.dp)
.align(Alignment.Center)
.graphicsLayer(alpha = 0.99f)
) {
// 作为dst,目标图
drawCircle(
color = Color.Red,
radius = 100f,
center = Offset(100f, 100f),
blendMode = BlendMode.SrcOver
)

// 作为src 源图
drawRect(
color = Color.Blue,
topLeft = Offset(150f, 100f),
size = Size(100f, 100f),
blendMode = BlendMode.Clear
)
}

注意

.graphicsLayer(alpha = 0.99f) 这个比较重要,如果没有就没有透明区,导致所有的混合模式都无效,所有擦除的区域都变成了黑色。