除了 Canvas
可组合项之外,Compose 还提供了几个有用的图形 Modifiers
,它们有助于绘制自定义内容。这些修饰符非常有用,因为它们可以应用于任何可组合项。
绘制修饰符
所有绘制命令都通过 Compose 中的绘制修饰符完成。Compose 中有三个主要的绘制修饰符
绘制的基础修饰符是 drawWithContent
,您可以在其中决定可组合项的绘制顺序以及修饰符内部发出的绘制命令。drawBehind
是 drawWithContent
的便捷包装器,其绘制顺序设置为在可组合项内容之后。drawWithCache
在其内部调用 onDrawBehind
或 onDrawWithContent
——并提供一种机制来缓存其中创建的对象。
Modifier.drawWithContent
:选择绘制顺序
Modifier.drawWithContent
允许您在可组合项内容之前或之后执行 DrawScope
操作。请务必调用 drawContent
以便渲染可组合项的实际内容。使用此修饰符,您可以决定操作的顺序,如果您希望内容在自定义绘制操作之前或之后绘制。
例如,如果您希望在内容顶部渲染径向渐变以在界面上创建手电筒锁孔效果,您可以执行以下操作
var pointerOffset by remember { mutableStateOf(Offset(0f, 0f)) } Column( modifier = Modifier .fillMaxSize() .pointerInput("dragging") { detectDragGestures { change, dragAmount -> pointerOffset += dragAmount } } .onSizeChanged { pointerOffset = Offset(it.width / 2f, it.height / 2f) } .drawWithContent { drawContent() // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI. drawRect( Brush.radialGradient( listOf(Color.Transparent, Color.Black), center = pointerOffset, radius = 100.dp.toPx(), ) ) } ) { // Your composables here }
Modifier.drawBehind
:在可组合项后面绘制
Modifier.drawBehind
允许您在屏幕上绘制的可组合项内容后面执行 DrawScope
操作。如果您查看 Canvas
的实现,您可能会注意到它只是 Modifier.drawBehind
的一个便捷包装器。
要在 Text
后面绘制一个圆角矩形
Text( "Hello Compose!", modifier = Modifier .drawBehind { drawRoundRect( Color(0xFFBBAAEE), cornerRadius = CornerRadius(10.dp.toPx()) ) } .padding(4.dp) )
生成以下结果

Modifier.drawWithCache
:绘制和缓存绘制对象
Modifier.drawWithCache
会将其内部创建的对象进行缓存。只要绘制区域的大小相同,或者读取的任何状态对象没有改变,这些对象就会被缓存。此修饰符对于提高绘制调用的性能非常有用,因为它避免了重新分配在绘制时创建的对象(例如:Brush, Shader, Path
等)的需求。
或者,您也可以在修饰符外部使用 remember
来缓存对象。但是,这并不总是可行的,因为您并不总是能够访问组合。如果对象仅用于绘制,使用 drawWithCache
可能更高效。
例如,如果您创建一个 Brush
以在 Text
后面绘制渐变,使用 drawWithCache
会缓存 Brush
对象,直到绘制区域的大小改变
Text( "Hello Compose!", modifier = Modifier .drawWithCache { val brush = Brush.linearGradient( listOf( Color(0xFF9E82F0), Color(0xFF42A5F5) ) ) onDrawBehind { drawRoundRect( brush, cornerRadius = CornerRadius(10.dp.toPx()) ) } } )

图形修饰符
Modifier.graphicsLayer
:对可组合项应用变换
Modifier.graphicsLayer
是一个修饰符,它使可组合项的内容绘制到一个绘制层中。一个层提供了几个不同的功能,例如
- 隔离其绘制指令(类似于
RenderNode
)。作为层一部分捕获的绘制指令可以由渲染管道高效地重新发出,而无需重新执行应用程序代码。 - 应用于层内所有绘制指令的变换。
- 用于合成功能的栅格化。当一个层被栅格化时,其绘制指令被执行,并且输出被捕获到一个离屏缓冲区。 合成这样的缓冲区用于后续帧比执行单个指令更快,但是当应用缩放或旋转等变换时,它将表现为位图。
变换
Modifier.graphicsLayer
提供了对其绘制指令的隔离;例如,可以使用 Modifier.graphicsLayer
应用各种变换。这些变换可以动画化或修改,而无需重新执行绘制 lambda。
Modifier.graphicsLayer
不会改变可组合项的测量大小或放置位置,因为它只影响绘制阶段。这意味着如果您的可组合项最终绘制在其布局边界之外,它可能会与其他可组合项重叠。
可以使用此修饰符应用以下变换
缩放 - 增大尺寸
scaleX
和 scaleY
分别在水平或垂直方向上放大或缩小内容。值为 1.0f
表示大小不变,值为 0.5f
表示尺寸减半。
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.scaleX = 1.2f this.scaleY = 0.8f } )
平移
translationX
和 translationY
可以通过 graphicsLayer
更改,translationX
将可组合项向左或向右移动。translationY
将可组合项向上或向下移动。
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.translationX = 100.dp.toPx() this.translationY = 10.dp.toPx() } )
旋转
设置 rotationX
进行水平旋转,rotationY
进行垂直旋转,rotationZ
进行 Z 轴旋转(标准旋转)。此值以度数 (0-360) 指定。
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
原点
可以指定一个 transformOrigin
。然后它被用作变换发生的点。到目前为止的所有示例都使用了 TransformOrigin.Center
,它位于 (0.5f, 0.5f)
。如果您将原点指定为 (0f, 0f)
,那么变换将从可组合项的左上角开始。
如果您使用 rotationZ
变换更改原点,您会看到项目围绕可组合项的左上角旋转
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.transformOrigin = TransformOrigin(0f, 0f) this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
裁剪和形状
Shape 指定了当 clip = true
时内容裁剪到的轮廓。在此示例中,我们设置了两个框,它们具有两个不同的裁剪——一个使用 graphicsLayer
裁剪变量,另一个使用便捷包装器 Modifier.clip
。
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .size(200.dp) .graphicsLayer { clip = true shape = CircleShape } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(CircleShape) .background(Color(0xFF4DB6AC)) ) }
第一个框的内容(文本“Hello Compose”)被裁剪为圆形

如果您随后对顶部粉红色圆圈应用 translationY
,您会看到可组合项的边界仍然相同,但圆圈绘制在底部圆圈下方(并超出其边界)。

要将可组合项裁剪到其绘制区域,您可以在修饰符链的开头添加另一个 Modifier.clip(RectangleShape)
。然后内容将保留在原始边界内。
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .clip(RectangleShape) .size(200.dp) .border(2.dp, Color.Black) .graphicsLayer { clip = true shape = CircleShape translationY = 50.dp.toPx() } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(RoundedCornerShape(500.dp)) .background(Color(0xFF4DB6AC)) ) }

Alpha
Modifier.graphicsLayer
可用于为整个图层设置 alpha
(不透明度)。1.0f
表示完全不透明,0.0f
表示不可见。
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )

合成策略
处理 alpha 和透明度可能不像改变单个 alpha 值那么简单。除了改变 alpha 之外,还可以选择在 graphicsLayer
上设置 CompositingStrategy
。CompositingStrategy
决定了可组合项的内容如何与屏幕上已绘制的其他内容进行合成(组合)。
不同的策略是
自动(默认)
合成策略由 graphicsLayer
的其余参数决定。如果 alpha 小于 1.0f 或设置了 RenderEffect
,它会将图层渲染到离屏缓冲区。当 alpha 小于 1f 时,会自动创建一个合成图层来渲染内容,然后将此离屏缓冲区以相应的 alpha 绘制到目标。设置 RenderEffect
或过度滚动总是会将内容渲染到离屏缓冲区,无论设置了何种 CompositingStrategy
。
离屏
可组合项的内容在渲染到目标之前总是栅格化到离屏纹理或位图。这对于应用 BlendMode
操作来遮罩内容,以及在渲染复杂绘制指令集时提高性能非常有用。
使用 CompositingStrategy.Offscreen
的一个例子是与 BlendModes
结合使用。看看下面的例子,假设您想通过发出一个使用 BlendMode.Clear
的绘制命令来移除 Image
可组合项的一部分。如果您不将 compositingStrategy
设置为 CompositingStrategy.Offscreen
,那么 BlendMode
会与它下面的所有内容交互。
Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .size(120.dp) .aspectRatio(1f) .background( Brush.linearGradient( listOf( Color(0xFFC5E1A5), Color(0xFF80DEEA) ) ) ) .padding(8.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } .drawWithCache { val path = Path() path.addOval( Rect( topLeft = Offset.Zero, bottomRight = Offset(size.width, size.height) ) ) onDrawWithContent { clipPath(path) { // this draws the actual image - if you don't call drawContent, it wont // render anything this@onDrawWithContent.drawContent() } val dotSize = size.width / 8f // Clip a white border for the content drawCircle( Color.Black, radius = dotSize, center = Offset( x = size.width - dotSize, y = size.height - dotSize ), blendMode = BlendMode.Clear ) // draw the red circle indication drawCircle( Color(0xFFEF5350), radius = dotSize * 0.8f, center = Offset( x = size.width - dotSize, y = size.height - dotSize ) ) } } )
通过将 CompositingStrategy
设置为 Offscreen
,它会创建一个离屏纹理来执行命令(仅将 BlendMode
应用于此可组合项的内容)。然后它将其渲染到屏幕上已渲染的内容之上,而不影响已绘制的内容。

如果您不使用 CompositingStrategy.Offscreen
,应用 BlendMode.Clear
的结果会清除目标中的所有像素,无论之前设置了什么——导致窗口的渲染缓冲区(黑色)可见。许多涉及 alpha 的 BlendModes
在没有离屏缓冲区的情况下将无法按预期工作。请注意红色圆形指示器周围的黑圈

为了进一步理解这一点:如果应用有一个半透明的窗口背景,并且您没有使用 CompositingStrategy.Offscreen
,那么 BlendMode
将与整个应用交互。它会清除所有像素以显示应用或下面的壁纸,如本例所示

值得注意的是,当使用 CompositingStrategy.Offscreen
时,会创建一个与绘制区域大小相同的离屏纹理,并将其渲染回屏幕。使用此策略进行的任何绘制命令,默认情况下都会被裁剪到此区域。下面的代码片段说明了切换到使用离屏纹理时的差异
@Composable fun CompositingStrategyExamples() { Column( modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center) ) { // Does not clip content even with a graphics layer usage here. By default, graphicsLayer // does not allocate + rasterize content into a separate layer but instead is used // for isolation. That is draw invalidations made outside of this graphicsLayer will not // re-record the drawing instructions in this composable as they have not changed Canvas( modifier = Modifier .graphicsLayer() .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { // ... and drawing a size of 200 dp here outside the bounds drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx())) } Spacer(modifier = Modifier.size(300.dp)) /* Clips content as alpha usage here creates an offscreen buffer to rasterize content into first then draws to the original destination */ Canvas( modifier = Modifier // force to an offscreen buffer .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the content gets clipped */ drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx())) } } }

ModulateAlpha
此合成策略会为 graphicsLayer
中记录的每个绘制指令调制 alpha。除非设置了 RenderEffect
,否则它不会为小于 1.0f 的 alpha 创建离屏缓冲区,因此对于 alpha 渲染可能更高效。但是,它可能会为重叠内容提供不同的结果。对于已知内容不重叠的用例,这可以提供比 alpha 值小于 1 的 CompositingStrategy.Auto
更好的性能。
下面是不同合成策略的另一个示例——对可组合项的不同部分应用不同的 alpha,并应用 Modulate
策略
@Preview @Composable fun CompositingStrategy_ModulateAlpha() { Column( modifier = Modifier .fillMaxSize() .padding(32.dp) ) { // Base drawing, no alpha applied Canvas( modifier = Modifier.size(200.dp) ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // Alpha 0.5f applied to whole composable Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { alpha = 0.5f } ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // 0.75f alpha applied to each draw call when using ModulateAlpha Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.ModulateAlpha alpha = 0.75f } ) { drawSquares() } } } private fun DrawScope.drawSquares() { val size = Size(100.dp.toPx(), 100.dp.toPx()) drawRect(color = Red, size = size) drawRect( color = Purple, size = size, topLeft = Offset(size.width / 4f, size.height / 4f) ) drawRect( color = Yellow, size = size, topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f) ) } val Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350)

将可组合项内容写入位图
一个常见的用例是从可组合项创建 Bitmap
。要将可组合项的内容复制到 Bitmap
,请使用 rememberGraphicsLayer()
创建一个 GraphicsLayer
。
使用 drawWithContent()
和 graphicsLayer.record{}
将绘制命令重定向到新图层。然后使用 drawLayer
在可见画布中绘制图层
val coroutineScope = rememberCoroutineScope() val graphicsLayer = rememberGraphicsLayer() Box( modifier = Modifier .drawWithContent { // call record to capture the content in the graphics layer graphicsLayer.record { // draw the contents of the composable into the graphics layer this@drawWithContent.drawContent() } // draw the graphics layer on the visible canvas drawLayer(graphicsLayer) } .clickable { coroutineScope.launch { val bitmap = graphicsLayer.toImageBitmap() // do something with the newly acquired bitmap } } .background(Color.White) ) { Text("Hello Android", fontSize = 26.sp) }
您可以将位图保存到磁盘并共享。有关更多详细信息,请参阅完整示例代码片段。在尝试保存到磁盘之前,请务必检查设备权限。
自定义绘制修饰符
要创建您自己的自定义修饰符,请实现 DrawModifier
接口。这使您可以访问 ContentDrawScope
,它与使用 Modifier.drawWithContent()
时公开的相同。然后,您可以将常见的绘制操作提取到自定义绘制修饰符中,以清理代码并提供便捷的包装器;例如,Modifier.background()
就是一个便捷的 DrawModifier
。
例如,如果您想实现一个垂直翻转内容的 Modifier
,您可以按如下方式创建一个
class FlippedModifier : DrawModifier { override fun ContentDrawScope.draw() { scale(1f, -1f) { this@draw.drawContent() } } } fun Modifier.flipped() = this.then(FlippedModifier())
然后将此翻转修饰符应用于 Text
Text( "Hello Compose!", modifier = Modifier .flipped() )

其他资源
有关使用 graphicsLayer
和自定义绘制的更多示例,请查看以下资源
为您推荐
- 注意:当 JavaScript 关闭时,会显示链接文本
- Compose 中的图形
- 自定义图像 {:#customize-image}
- 适用于 Jetpack Compose 的 Kotlin