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 px,向上移动 300 px

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 中绘图的更多信息,请查看以下资源