Compose 中的图形

利用 Jetpack Compose 可更轻松地处理自定义图形。许多应用需要精确控制屏幕上绘制的准确内容。这种控制有可能只是将一个框或圆形放到屏幕的正确位置上,也有可能是精心布置许多不同样式的图形元素。借助 Compose 的声明性方法,所有图形配置都将在一个地方进行,而不必在方法调用和 Paint 辅助对象之间来回切换。Compose 负责高效地创建和更新所需的对象。

使用 Compose 的声明性图形

Compose 将其声明性方法扩展到了处理图形的方式中。Compose 的方法有许多优点:

  • Compose 可最大限度地减少图形元素中的 state,有助于避免状态的编程困难
  • 当您绘制图形时,所有选项都位于可组合函数中的恰当位置。
  • Compose 的图形 API 负责以高效的方式创建和释放对象。

Canvas

自定义图形的核心可组合项是 Canvas。在布局中放置 Canvas 的方式与放置其他 Compose 界面元素相同。在 Canvas 中,您可以通过精确控制元素的样式和位置来绘制元素。

例如,以下代码将创建一个 Canvas 可组合项,用于填充其父元素中的所有可用空间:

Canvas(modifier = Modifier.fillMaxSize()) {
}

Canvas 会自动提供 DrawScope(一个维护自身状态且限定了作用域的绘图环境)。这让您可以为一组图形元素设置参数。DrawScope 提供了几个有用的字段,例如 size,一个指定 DrawScope 的当前维度和最大维度的 Size 对象。

例如,假设您想绘制一条从画布右上角到左下角的对角线。这可以通过添加 drawLine 可组合项来实现:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height

    drawLine(
        start = Offset(x = canvasWidth, y = 0f),
        end = Offset(x = 0f, y = canvasHeight),
        color = Color.Blue
    )
}

手机屏幕上绘制了一条细对角线。

图 1. 使用 drawLine 在画布上画一条线。该代码会设置线条的颜色,但会使用默认宽度。

您可以使用其他参数来自定义绘图。例如,默认情况下,线条以细线宽度绘制,无论绘图的比例大小如何,该线条均显示为一个像素宽。您可以通过设置 strokeWidth 值来替换默认值:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height

    drawLine(
        start = Offset(x = canvasWidth, y = 0f),
        end = Offset(x = 0f, y = canvasHeight),
        color = Color.Blue,
        strokeWidth = 5F
    )
}

手机屏幕上绘制了一条较粗的对角线。

图 2. 通过替换默认宽度,修改图 1 中的线条。

还有许多其他简单的绘图函数,比如 drawRectdrawCircle。例如,以下代码用于在画布中央绘制一个实心圆,其直径等于画布短边的一半:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawCircle(
        color = Color.Blue,
        center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
        radius = size.minDimension / 4
    )
}

手机屏幕中央绘制了一个蓝色圆形。

图 3. 使用 drawCircle 将圆形放在画布中央。默认情况下,drawCircle 用于绘制一个实心圆,因此不必明确指定该设置。

绘图函数有许多有用的默认参数。例如,默认情况下,drawRectangle() 用于填充其整个父作用域,drawCircle() 的半径等于其父项短边的一半。跟以往一样,使用 Kotlin 时,您可以通过利用默认参数值并仅设置需要更改的参数,使代码变得更加简单明了。您可以通过为 DrawScope 绘图方法提供显式参数来利用这一点,因为您绘制的元素的默认设置将基于父作用域的设置。

DrawScope

如上所述,每个 Compose Canvas 提供一个 DrawScope(限定了作用域的绘图环境),您可以在其中实际发出绘图命令。

例如,以下代码用于在画布左上角绘制一个矩形:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Green,
        size = canvasQuadrantSize
    )
}

您可以使用 DrawScope.inset() 函数来调整当前作用域的默认参数,以更改绘图边界并相应地转换绘图。inset() 之类的操作适用于相应 lambda 中的所有绘图操作:

val canvasQuadrantSize = size / 2F
inset(50F, 30F) {
    drawRect(
        color = Color.Green,
        size = canvasQuadrantSize
    )
}

DrawScope 还提供其他简单的转换,例如 rotate()。例如,以下代码用于在画布中央绘制一个占据九分之一空间的矩形:

val canvasSize = size
val canvasWidth = size.width
val canvasHeight = size.height
drawRect(
    color = Color.Gray,
    topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
    size = canvasSize / 3F
)

手机屏幕中央绘制了一个实心矩形。

图 4. 使用 drawRect 在屏幕中央绘制一个实心矩形。

您可以通过对矩形的 DrawScope 应用旋转来旋转矩形:

rotate(degrees = 45F) {
    drawRect(
        color = Color.Gray,
        topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
        size = canvasSize / 3F
    )
}

手机屏幕中央绘制了一个旋转 45 度的矩形。

图 5. 我们使用 rotate() 对当前绘图作用域应用旋转,将矩形旋转 45 度。

如需对绘图应用多个转换,最佳方法不是创建嵌套的 DrawScope 环境,而是使用 withTransform() 函数,该函数用于创建并应用整合了您所需的所有更改的单一转换。与对各个转换进行嵌套调用相比,使用 withTransform() 更有效,因为这种情况下所有转换都在单个操作中一起执行,Compose 不必计算并保存每个嵌套转换。

例如,以下代码用于向矩形同时应用平移和旋转:

withTransform({
    translate(left = canvasWidth / 5F)
    rotate(degrees = 45F)
}) {
    drawRect(
        color = Color.Gray,
        topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
        size = canvasSize / 3F
    )
}

手机屏幕上的矩形旋转了一定角度并移向屏幕一侧。

图 6. 在这里,我们使用 withTransform 同时应用旋转和平移,以便旋转矩形并将其向左移动。