除了 Canvas
可组合项外,Compose 还包含几个有用的图形 Modifiers
,有助于绘制自定义内容。这些修饰符很有用,因为它们可以应用于任何可组合项。
绘制修饰符
所有绘制命令都通过 Compose 中的绘制修饰符完成。Compose 中有三个主要的绘制修饰符
绘制的基础修饰符是 drawWithContent
,您可以使用它来决定 Composable 的绘制顺序以及在修饰符中发出的绘制命令。 drawBehind
是 drawWithContent
的一个便捷包装器,它将绘制顺序设置为位于可组合内容的后面。 drawWithCache
在其内部调用 onDrawBehind
或 onDrawWithContent
- 并提供一种机制来缓存其中创建的对象。
Modifier.drawWithContent
:选择绘制顺序
Modifier.drawWithContent
允许您在可组合内容之前或之后执行 DrawScope
操作。 确保调用 drawContent
来呈现可组合的实际内容。 使用此修饰符,您可以决定操作的顺序,即是否希望您的内容在自定义绘制操作之前或之后绘制。
例如,如果您希望在内容之上渲染径向渐变以在 UI 上创建手电筒钥匙孔效果,您可以执行以下操作
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 } )
平移
可以使用 graphicsLayer
更改 translationX
和 translationY
,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 } )
裁剪和形状
当 clip = true
时,Shape 指定内容裁剪到的轮廓。 在此示例中,我们将两个框设置为具有两个不同的裁剪 - 一个使用 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 [email protected]() } 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 渲染效率更高。但是,它可能为重叠内容提供不同的结果。对于那些事先知道内容不会重叠的用例,这可以提供比 CompositingStrategy.Auto
在 alpha 值小于 1 时更好的性能。
以下是不同组合策略的另一个示例 - 将不同的 alpha 应用于可组合的不同部分,并应用 Modulate
策略。
@Preview @Composable fun CompositingStratgey_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 [email protected]() } // 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) { [email protected]() } } } fun Modifier.flipped() = this.then(FlippedModifier())
然后使用应用于 Text
的此翻转修饰符。
Text( "Hello Compose!", modifier = Modifier .flipped() )
其他资源
有关使用 graphicsLayer
和自定义绘制的更多示例,请查看以下资源
为您推荐
- 注意:当 JavaScript 关闭时,链接文本会显示。
- Compose 中的图形
- 自定义图像 {:#customize-image}
- Jetpack Compose 的 Kotlin