Compose 中的图形

许多应用都需要能够精确控制屏幕上所绘制的内容。这种控制有可能只是将方框或圆形放到屏幕上的正确位置,也有可能是精心布置许多不同样式的图形元素。

使用修饰符和 DrawScope 的基本绘制

在 Compose 中绘制自定义内容的核心方法是使用修饰符,例如 Modifier.drawWithContentModifier.drawBehindModifier.drawWithCache

例如,如需在可组合项后面绘制内容,您可以使用 drawBehind 修饰符开始执行绘制命令:

Spacer(
    modifier = Modifier
        .fillMaxSize()
        .drawBehind {
            // this = DrawScope
        }
)

如果您只需使用可组合项进行绘制,可使用 Canvas 可组合项。Canvas 可组合项是一个包裹 Modifier.drawBehind 的便利封装容器。在布局中放置 Canvas 的方式与放置其他 Compose UI 元素相同。在 Canvas 中,您可以通过精确控制元素的样式和位置来绘制元素。

所有绘制修饰符都会提供 DrawScope(一个维护自身状态且限定了作用域的绘制环境),可供您为一组图形元素设置参数。DrawScope 提供了几个有用的字段,例如 size(用于指定 DrawScope 的当前尺寸的 Size 对象)。

如需绘制内容,您可以使用 DrawScope 上的众多绘制函数之一。例如,以下代码会在屏幕的左上角绘制一个矩形:

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

在白色背景上绘制的粉色矩形,占据了屏幕的四分之一
图 1. 在 Compose 中使用 Canvas 绘制的矩形。

如需详细了解各种绘制修饰符,请参阅图形修饰符文档。

坐标系

如需在屏幕上绘制内容,您需要知道相应项目的偏移量(xy)和大小。对于 DrawScope 上的许多绘制方法,位置和尺寸由默认参数值提供。默认参数通常会将项目放置在画布的 [0, 0] 点上,并提供填充整个绘制区域的默认 size,如上例所示,您可以看到矩形被放置在左上角。若要调整项目的尺寸和位置,您需要了解 Compose 中的坐标系。

坐标系的原点 ([0,0]) 位于绘制区域最左上角的像素处。x 会随着向右移动而增加,y 则会随着向下移动而增加。

显示坐标系的网格:左上角显示的是 [0,0],右下角显示的是 [width,height]
图 2. 绘制坐标系/绘制网格。

例如,如果想要绘制一条从画布区域右上角到左下角的对角线,您可以使用 DrawScope.drawLine() 函数,并指定具有对应 x 和 y 位置的起始和结束偏移量:

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
    )
}

基本转换

DrawScope 会提供一些用于更改绘制命令执行位置或方式的转换。

缩放

DrawScope.scale() 可用于按系数来增加绘制操作的大小。scale() 之类的操作适用于相应 lambda 中的所有绘图操作。例如,以下代码会将 scaleX 的大小增加 10 倍,并将 scaleY 的大小增加 15 倍:

Canvas(modifier = Modifier.fillMaxSize()) {
    scale(scaleX = 10f, scaleY = 15f) {
        drawCircle(Color.Blue, radius = 20.dp.toPx())
    }
}

非均匀缩放的圆形
图 3. 在 Canvas 上对圆形应用缩放操作。

平移

DrawScope.translate() 可用于向上、向下、向左或向右移动绘制操作。例如,以下代码会将绘制操作向右移动 100 像素,同时向上移动 300 像素:

Canvas(modifier = Modifier.fillMaxSize()) {
    translate(left = 100f, top = -300f) {
        drawCircle(Color.Blue, radius = 200.dp.toPx())
    }
}

偏离中心的圆形
图 4. 在 Canvas 上对圆形应用平移操作。

旋转

DrawScope.rotate() 可用于围绕某个轴心点旋转绘制操作。例如,以下代码会将矩形旋转 45 度:

Canvas(modifier = Modifier.fillMaxSize()) {
    rotate(degrees = 45F) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

手机屏幕中央绘制了一个旋转 45 度的矩形
图 5. 使用 rotate() 对当前绘制作用域应用旋转,将矩形旋转 45 度。

边衬区

DrawScope.inset() 可用于调整当前 DrawScope 的默认参数,相应地更改绘制边界和平移绘制操作:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    inset(horizontal = 50f, vertical = 30f) {
        drawRect(color = Color.Green, size = canvasQuadrantSize)
    }
}

此代码可向绘制命令有效添加内边距:

四周都有内边距的矩形
图 6. 对绘制命令应用边衬区。

多个转换

如需对绘制操作应用多个转换,请使用 DrawScope.withTransform() 函数,该函数会创建和应用单个转换来合并您需要的所有更改。与对各个转换进行嵌套调用相比,使用 withTransform() 效率更高,因为这种情况下所有转换都在单个操作中一起执行,Compose 不必计算并保存每个嵌套转换。

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

Canvas(modifier = Modifier.fillMaxSize()) {
    withTransform({
        translate(left = size.width / 5F)
        rotate(degrees = 45F)
    }) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

手机屏幕上的矩形旋转了一定角度并移向屏幕一侧
图 7. 使用 withTransform 同时应用旋转和平移,让矩形旋转并向左移动。

常用绘制操作

绘制文本

如需在 Compose 中绘制文本,您一般可以使用 Text 可组合项。不过,如果您使用的是 DrawScope,或想通过自定义设置手动绘制文本,则可以使用 DrawScope.drawText() 方法。

如需绘制文本,请使用 rememberTextMeasurer 创建 TextMeasurer,然后使用 Measurer 调用 drawText

val textMeasurer = rememberTextMeasurer()

Canvas(modifier = Modifier.fillMaxSize()) {
    drawText(textMeasurer, "Hello")
}

显示了在 Canvas 上绘制的 Hello
图 8. 在 Canvas 上绘制文本。

测量文本

绘制文本的方式与其他绘制命令略有不同。通常情况下,您会在绘制命令中指定绘制形状/图片所需的尺寸(宽度和高度)。在绘制文本时,您可以通过几个参数来控制所呈现文本的大小,例如字体大小、字体、连字和字母间距。

在 Compose 中,您可以使用 TextMeasurer 来获取根据上述因素测量的文本大小。如果您想在文本后面绘制背景,可以通过测量的信息获知文本所占区域的大小:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixedWidth((size.width * 2f / 3f).toInt()),
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

上述代码段会在文本上生成粉色背景:

占整个区域 ⅔ 大小的多行文本,带有矩形背景
图 9. 占整个区域 ⅔ 大小的多行文本,带有矩形背景。

如果调整约束条件、字体大小或任何会影响测量尺寸的属性,系统就会报告新的尺寸。您可以为 widthheight 设置固定大小,然后文本会遵循设定的 TextOverflow。例如,以下代码会在可组合项区域的 1⁄3 高度和 1⁄3 宽度内呈现文本,并将 TextOverflow 设置为 TextOverflow.Ellipsis

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixed(
                        width = (size.width / 3f).toInt(),
                        height = (size.height / 3f).toInt()
                    ),
                    overflow = TextOverflow.Ellipsis,
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

文本现在按照约束条件绘制,末尾带有省略号:

在粉色背景上绘制的文本,并用省略号截断该文本。
图 10. 测量文本中采用固定约束条件和 TextOverflow.Ellipsis

绘制图片

如需使用 DrawScope 绘制 ImageBitmap,请使用 ImageBitmap.imageResource() 加载图片,然后调用 drawImage

val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    drawImage(dogImage)
})

在 Canvas 上绘制的狗狗图片
图 11. 在 Canvas 上绘制 ImageBitmap

绘制基本形状

DrawScope 上有许多形状绘制函数。如需绘制形状,请使用其中一个预定义的绘制函数,例如 drawCircle

val purpleColor = Color(0xFFBA68C8)
Canvas(
    modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
    onDraw = {
        drawCircle(purpleColor)
    }
)

API

输出

drawCircle()

绘制圆形

drawRect()

绘制长方形

drawRoundedRect()

绘制圆角矩形

drawLine()

绘制直线

drawOval()

绘制卵形

drawArc()

绘制弧形

drawPoints()

绘制多个点

绘制路径

路径是一系列数学指令,一旦执行便会生成绘图。DrawScope 可以使用 DrawScope.drawPath() 方法绘制路径。

例如,假设您想绘制一个三角形。您可以使用 lineTo()moveTo() 等函数根据绘制区域的尺寸生成路径,然后使用新创建的路径调用 drawPath() 来获得一个三角形。

Spacer(
    modifier = Modifier
        .drawWithCache {
            val path = Path()
            path.moveTo(0f, 0f)
            path.lineTo(size.width / 2f, size.height / 2f)
            path.lineTo(size.width, 0f)
            path.close()
            onDrawBehind {
                drawPath(path, Color.Magenta, style = Stroke(width = 10f))
            }
        }
        .fillMaxSize()
)

在 Compose 上绘制的一个上下颠倒的紫色三角形路径
图 12. 在 Compose 中创建和绘制 Path

访问 Canvas 对象

使用 DrawScope 时,您无法直接访问 Canvas 对象。您可以使用 DrawScope.drawIntoCanvas() 获取 Canvas 对象本身的访问权限,以调用函数。

例如,如果您有一个要在画布上绘制的自定义 Drawable,可以访问画布并调用 Drawable#draw(),并传入 Canvas 对象:

val drawable = ShapeDrawable(OvalShape())
Spacer(
    modifier = Modifier
        .drawWithContent {
            drawIntoCanvas { canvas ->
                drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
                drawable.draw(canvas.nativeCanvas)
            }
        }
        .fillMaxSize()
)

占据整个屏幕尺寸的卵形黑色 ShapeDrawable
图 13. 访问画布以绘制 Drawable

了解更多内容

如需详细了解如何在 Compose 中绘制,请参阅以下资源: