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,一个Size对象,指定DrawScope的当前尺寸。

要绘制某些内容,您可以使用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,该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 中绘制的更多信息,请查看以下资源