Compose 中的图形

许多应用需要能够精确控制屏幕上绘制的内容。这可能小到在屏幕上恰到好处地放置一个方框或圆形,也可能是在许多不同样式中精心安排图形元素。

使用修饰符和 DrawScope 进行基本绘制

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

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

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

如果您只需要一个可绘制的可组合项,您可以使用 Canvas 可组合项。Canvas 可组合项是对 Modifier.drawBehind 的便捷封装。您可以像放置任何其他 Compose UI 元素一样,在布局中放置 Canvas。在 Canvas 中,您可以精确控制元素的样式和位置来绘制它们。

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

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

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

Pink rectangle drawn on a white background that takes up a quarter of the screen
图 1. 在 Compose 中使用 Canvas 绘制的矩形。

要详细了解不同的绘制修饰符,请参阅《图形修饰符》文档。

坐标系

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

坐标系的原点([0,0])位于绘图区域的左上角像素。当向右移动时,x 值增大;当向下移动时,y 值增大。

A grid showing the coordinate system showing the top left [0, 0] and bottom right [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())
    }
}

A circle scaled non-uniformly
图 3. 将缩放操作应用于 Canvas 上的圆形。

平移

使用 DrawScope.translate() 向上、向下、向左或向右移动绘制操作。例如,以下代码将绘制内容向右移动 100 像素,向上移动 300 像素。

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

A circle that has moved off center
图 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
        )
    }
}

A phone with a rectangle rotated by 45 degrees in the center of the screen
图 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)
    }
}

此代码有效地为绘制命令添加了内边距。

A rectangle that has been padded all around it
图 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
        )
    }
}

A phone with a rotated rectangle shifted to the side of the screen
图 7. 使用 withTransform 同时应用旋转和平移,旋转矩形并将其向左移动。

常见绘制操作

绘制文本

在 Compose 中绘制文本,通常可以使用 Text 可组合项。但是,如果您在 DrawScope 中或想手动自定义绘制文本,可以使用 DrawScope.drawText() 方法。

要绘制文本,请使用 rememberTextMeasurer 创建一个 TextMeasurer,然后使用该测量器调用 drawText

val textMeasurer = rememberTextMeasurer()

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

Showing a Hello drawn on Canvas
图 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()
)

此代码段会在文本上生成粉红色背景。

Multi-line text taking up ⅔ size of the full area, with a background rectangle
图 9. 占完整区域 ⅔ 大小的多行文本,带背景矩形。

调整约束、字体大小或任何影响测量大小的属性都会导致报告新的大小。您可以为 widthheight 设置固定大小,然后文本会遵循设置的 TextOverflow。例如,以下代码将文本渲染为可组合区域高度的 ⅓ 和宽度的 ⅓,并将 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()
)

文本现在在约束中绘制,末尾带有省略号。

Text drawn on pink background, with ellipsis cutting off the text.
图 10. TextOverflow.Ellipsis 在测量文本时具有固定约束。

绘制图像

要使用 DrawScope 绘制 ImageBitmap,请使用 ImageBitmap.imageResource() 加载图像,然后调用 drawImage

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

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

An image of a dog drawn on Canvas
图 11. 在 Canvas 上绘制 ImageBitmap

绘制基本形状

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

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

API

输出

drawCircle()

draw circle

drawRect()

draw rect

drawRoundedRect()

draw rounded rect

drawLine()

draw line

drawOval()

draw oval

drawArc()

draw arc

drawPoints()

draw points

绘制路径

路径是一系列数学指令,执行后会生成一个图形。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()
)

An upside-down purple path triangle drawn on 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()
)

An oval black ShapeDrawable taking up full size
图 13. 访问画布以绘制 Drawable

了解详情

有关 Compose 中绘制的更多信息,请查看以下资源: