除了 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 } )
平移
可通过 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
,然后将其用作转换的发生点。到目前为止,所有示例都使用了位于 (0.5f, 0.5f)
的 TransformOrigin.Center
。如果将原点指定为 (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
时裁剪内容的轮廓。在本例中,我们将两个框设为具有两种不同的裁剪:一个使用 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 值将此屏幕外缓冲区绘制到目标位置。无论 CompositingStrategy
如何设置,设置 RenderEffect
或滚动回弹始终会将内容渲染到屏幕外缓冲区。
屏幕外
可组合项的内容始终会先光栅化为屏幕外纹理或位图,然后再渲染到目标位置。这对于应用 BlendMode
操作来遮盖内容,以及在渲染复杂的绘制指令集时提高性能非常有用。
BlendModes
就是使用 CompositingStrategy.Offscreen
的一个例子。如下例所示,假设您想通过发出使用 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}
- Kotlin 对 Jetpack Compose 的支持