图形修饰符

除了 Canvas 可组合项外,Compose 还包含几个有用的图形 Modifiers,有助于绘制自定义内容。这些修饰符很有用,因为它们可以应用于任何可组合项。

绘制修饰符

所有绘制命令都通过 Compose 中的绘制修饰符完成。Compose 中有三个主要的绘制修饰符

绘制的基础修饰符是 drawWithContent,您可以使用它来决定 Composable 的绘制顺序以及在修饰符中发出的绘制命令。 drawBehinddrawWithContent 的一个便捷包装器,它将绘制顺序设置为位于可组合内容的后面。 drawWithCache 在其内部调用 onDrawBehindonDrawWithContent - 并提供一种机制来缓存其中创建的对象。

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
}

图 1:在 Composable 之上使用 Modifier.drawWithContent 创建手电筒类型的 UI 体验。

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)
)

这将产生以下结果

Text and a background drawn using Modifier.drawBehind
图 2:使用 Modifier.drawBehind 绘制的文本和背景

Modifier.drawWithCache:绘制和缓存绘制对象

Modifier.drawWithCache 会将其中创建的对象缓存起来。 只要绘制区域的大小相同,或者读取的任何状态对象都没有更改,这些对象就会被缓存。 此修饰符对于提高绘制调用的性能非常有用,因为它避免了重新分配在绘制时创建的对象(例如:Brush, Shader, Path 等)。

或者,您也可以使用 remember 在修饰符之外缓存对象。 但是,这并不总是可行的,因为您并不总是可以访问组合。 如果对象仅用于绘制,则使用 drawWithCache 可能会更有效。

例如,如果您创建一个 BrushText 后面绘制渐变,则使用 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())
                )
            }
        }
)

Caching the Brush object with drawWithCache
图 3:使用 drawWithCache 缓存 Brush 对象

图形修饰符

Modifier.graphicsLayer:将变换应用于可组合项

Modifier.graphicsLayer 是一个修饰符,它使可组合项的内容绘制到一个绘制层。 层提供了几个不同的功能,例如

  • 对其绘制指令的隔离(类似于 RenderNode)。 作为层的一部分捕获的绘制指令可以通过渲染管道有效地重新发出,而无需重新执行应用程序代码。
  • 应用于层内包含的所有绘制指令的变换。
  • 用于组合功能的栅格化。 当一个层被栅格化时,它的绘制指令会被执行,并且输出会被捕获到一个离屏缓冲区中。 组合 这样的缓冲区用于后续帧比执行单个指令更快,但当应用缩放或旋转等变换时,它的行为类似于位图。

变换

Modifier.graphicsLayer 为其绘制指令提供隔离;例如,可以使用 Modifier.graphicsLayer 应用各种变换。 这些可以被动画化或修改,而无需重新执行绘制 lambda。

Modifier.graphicsLayer 不会更改可组合项的测量大小或放置位置,因为它只影响绘制阶段。 这意味着您的可组合项可能会与其他项重叠,如果它最终绘制在布局边界之外。

可以使用此修饰符应用以下变换

缩放 - 增加大小

scaleXscaleY 分别在水平或垂直方向上放大或缩小内容。 值为 1.0f 表示没有缩放变化,值为 0.5f 表示尺寸的一半。

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.scaleX = 1.2f
            this.scaleY = 0.8f
        }
)

图 4:scaleX 和 scaleY 应用于 Image 可组合项
平移

可以使用 graphicsLayer 更改 translationXtranslationYtranslationX 将可组合项向左或向右移动。 translationY 将可组合项向上或向下移动。

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.translationX = 100.dp.toPx()
            this.translationY = 10.dp.toPx()
        }
)

图 5:使用 Modifier.graphicsLayer 将 translationX 和 translationY 应用于 Image
旋转

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
        }
)

图 6:使用 Modifier.graphicsLayer 将 rotationX、rotationY 和 rotationZ 设置为 Image
原点

可以指定 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
        }
)

图 7:使用 TransformOrigin 设置为 0f、0f 应用旋转

裁剪和形状

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”的文本)的内容被裁剪为圆形

Clip applied to Box composable
图 8:将 Clip 应用于 Box 可组合项

如果您然后将 translationY 应用于上面的粉色圆圈,您会看到可组合项的边界仍然相同,但圆圈绘制在下面的圆圈(及其边界之外)下方。

Clip applied with translationY, and red border for outline
图 9:将 Clip 应用于 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))
    )
}

Clip applied on top of graphicsLayer transformation
图 10:在 graphicsLayer 变换之上应用 Clip

Alpha

Modifier.graphicsLayer 可用于为整个层设置 alpha(不透明度)。 1.0f 表示完全不透明,0.0f 表示不可见。

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "clock",
    modifier = Modifier
        .graphicsLayer {
            this.alpha = 0.5f
        }
)

Image with alpha applied
图 11:应用了 Alpha 的 Image

合成策略

使用 alpha 和透明度可能不像更改单个 alpha 值那样简单。 除了更改 alpha 之外,还可以选择在 graphicsLayer 上设置 CompositingStrategyCompositingStrategy 决定可组合项的内容如何与已绘制在屏幕上的其他内容合成(组合在一起)。

不同的策略是

自动(默认)

合成策略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 应用于此可组合的内容)。然后,它会在屏幕上已渲染的内容之上渲染它,不会影响已绘制的内容。

Modifier.drawWithContent on an Image showing a circle indication, with the BlendMode.Clear inside app
图 12:在 Image 上使用 Modifier.drawWithContent 显示圆形指示器,在应用程序内部使用 BlendMode.Clear 和 CompositingStrategy.Offscreen

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

Modifier.drawWithContent on an Image showing a circle indication, with the BlendMode.Clear and no CompositingStrategy set
图 13:在 Image 上使用 Modifier.drawWithContent 显示圆形指示器,使用 BlendMode.Clear 但没有设置 CompositingStrategy

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

No CompositingStrategy set and using BlendMode.Clear with an app that has a translucent window background. The pink wallpaper is shown through the area around the red status circle.
图 14:没有设置 CompositingStrategy,并使用 BlendMode.Clear 和具有半透明窗口背景的应用程序。请注意,粉色壁纸是如何显示在红色状态圆圈周围的区域中的。

值得注意的是,当使用 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()))
       }
   }
}

CompositingStrategy.Auto vs CompositingStrategy.Offscreen - offscreen clips to the region, where auto doesn’t
图 15:CompositingStrategy.Auto 与 CompositingStrategy.Offscreen - 离屏剪裁到区域,而自动不会
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)

ModulateAlpha applies the alpha set to each individual draw command
图 16:ModulateAlpha 将 alpha 应用于每个单独的绘制命令

将可组合的内容写入位图

一个常见的用例是根据可组合创建 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()
)

Custom Flipped Modifier on Text
图 17:Text 上的自定义翻转修饰符

其他资源

有关使用 graphicsLayer 和自定义绘制的更多示例,请查看以下资源