最近项目中,针对 App 首次启动新手指引做了适配,新手指引大概的效果是如下图这样的,原本项目中使用的是纯图片,下方的箭头和小圆圈是固定显示在中间的,随着项目的不断庞大,这个指引的位置不是固定的,可能在左边也可能在右边,而且更加重要的是后续需要适配平板,如果还是用图片处理就会变得非常困难,所以只能使用代码画出来了,本文将以这个需求为起始点,展示如何使用 Jetpack Compose 进行绘制。我们将从这个示例开始,逐步尝试和了解 Compose 中的各种绘制 API 和方法

开始

首先分析一下这个效果,上方是一个圆角矩形, 圆角矩形底部中间一个小三角形,这个三角形连接着一条线连接着两个重叠的小圆。要使用 Compose 纯代码绘制,起始和 Android 原生差不多,需要使用到 CanvasPath,直接上代码

 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 时,渐变在到达终点后,会镜像反转然后再重复。例如,红色到蓝色的渐变会变成蓝色到红色,然后再重复红色到蓝色,如此循环

绘制好渐变颜色后,再使用 pathaddRoundRect 添加了一个圆角矩形,配置我们刚刚编写好的渐变颜色,这样就完成了一个渐变颜色的圆角矩形

绘制三角形

接下来,我们再绘制矩形中间底部的三角形,同样也是使用 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)

效果就是这样,代码比较简单,这里我们首先了解一下 moveTolineTo 的作用

  • 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 等方法,基本使用已经大概了解了,他们分别都是位于 DrawScopePath 类中的方法,为了以后面对各种各样的绘制更加的得心应手,接下来乘着这一股劲,了解一下内部所有的方法,和方法中其他可配置的参数

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 // 颜色混合模式
)
  • cap

线条末端的样式

  • 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

线条的路径效果

  • DashPathEffect 创建虚线效果

    1
    2
    
    // 画出 10像素一段线,然后空 10 个像素, phase 表示 参数表示虚线的起始偏移量
    val dashEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), phase = 0f) 
  • CornerPathEffect 用于将路径的尖角变成圆角(针对的路径的尖角,这里说的路径是 path,用于 drawline 可能没有效果)

    1
    2
    
    // 表示将路径的尖角变成半径为 10 像素的圆角
    val cornerEffect = PathEffect.cornerPathEffect(radius = 10f)
  • ChainPathEffect 将 PathEffect 的效果连接在一起

    1
    2
    3
    
    val dashEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), phase = 0f)
    val cornerEffect = PathEffect.cornerPathEffect(radius = 10f)
    val chainEffect = PathEffect.chainPathEffect(cornerEffect, dashEffect) // 同时应用虚线和圆角效果

接下来看看 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) 将路径平移指定的距离

transform

 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)
    }
}
  • 弧度和角度的区别

    • 角度(Degrees): 角度是一个常用的角度单位,1 圈等于 360 度

    • 弧度(Radians): 弧度是一个基于圆周的角度单位,1 圈等于 2π 弧度。1 弧度约等于 57.2958 度

  • 参数说明

    • 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 类似, 也是添加一个弧形路径,但是设置的是弧度,其中 RadRadian 的缩写,代表的是弧度,一个 π 弧度表示 180°

addOutline

将一个轮廓添加到路径中

  1. 矩形轮廓 (Outline.Rectangle)
  2. 圆角矩形轮廓(Outline.Rounded
  3. 通用路径 (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 类当中,现在你可以回顾一下里面的方法都是什么意思,它们都有什么样的能力,如果你都掌握了这些,基本对这个绘制应该问题大不了