画笔:渐变和着色器

Compose 中的 Brush 描述了如何在屏幕上绘制某个内容:它决定了在绘制区域(例如圆形、正方形、路径)中绘制的颜色。有一些内置的画笔可用于绘制,例如 LinearGradientRadialGradient 或简单的 SolidColor 画笔。

画笔可以与 Modifier.background()TextStyleDrawScope 绘制调用一起使用,以将绘制样式应用于正在绘制的内容。

例如,水平渐变画笔可以应用于在 DrawScope 中绘制圆形。

val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
Canvas(
    modifier = Modifier.size(200.dp),
    onDraw = {
        drawCircle(brush)
    }
)
Circle drawn with Horizontal Gradient
图 1:使用水平渐变绘制的圆形

渐变画笔

有很多内置的渐变画笔可用于实现不同的渐变效果。这些画笔允许您指定要从中创建渐变的颜色列表。

可用渐变画笔及其对应输出的列表

渐变画笔类型 输出
Brush.horizontalGradient(colorList) Horizontal Gradient
Brush.linearGradient(colorList) Linear Gradient
Brush.verticalGradient(colorList) Vertical Gradient
Brush.sweepGradient(colorList)
注意:要获得颜色之间的平滑过渡,请将最后一个颜色设置为起始颜色。
Sweep Gradient
Brush.radialGradient(colorList) Radial Gradient

使用 colorStops 更改颜色的分布

要自定义颜色在渐变中的显示方式,您可以调整每个颜色的 colorStops 值。colorStops 应指定为介于 0 和 1 之间的分数。大于 1 的值会导致这些颜色不会作为渐变的一部分呈现。

您可以配置颜色停止点以具有不同的数量,例如一种颜色少一些或多一些。

val colorStops = arrayOf(
    0.0f to Color.Yellow,
    0.2f to Color.Red,
    1f to Color.Blue
)
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(Brush.horizontalGradient(colorStops = colorStops))
)

颜色分散在 colorStop 对中定义的提供的偏移量处,黄色比红色和蓝色少。

Brush configured with different color stops
图 2:使用不同颜色停止点配置的画笔

使用 TileMode 重复图案

每个渐变画笔可以选择在其上设置 TileMode。如果您没有为渐变设置起始和结束位置,您可能不会注意到 TileMode,因为它默认为填充整个区域。TileMode 仅当区域的大小大于画笔大小时才会平铺渐变。

以下代码将渐变图案重复 4 次,因为 endX 设置为 50.dp,而大小设置为 200.dp

val listColors = listOf(Color.Yellow, Color.Red, Color.Blue)
val tileSize = with(LocalDensity.current) {
    50.dp.toPx()
}
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(
            Brush.horizontalGradient(
                listColors,
                endX = tileSize,
                tileMode = TileMode.Repeated
            )
        )
)

下表详细说明了上述 HorizontalGradient 示例中不同平铺模式的作用

TileMode 输出
TileMode.Repeated:边缘从最后一个颜色重复到第一个颜色。 TileMode Repeated
TileMode.Mirror:边缘从最后一个颜色镜像到第一个颜色。 TileMode Mirror
TileMode.Clamp:边缘固定到最终颜色。然后它将为该区域的其余部分绘制最接近的颜色。 Tile Mode Clamp
TileMode.Decal:仅渲染到边界的尺寸。 TileMode.Decal 利用透明黑色来采样原始边界之外的内容,而 TileMode.Clamp 则采样边缘颜色。 Tile Mode Decal

TileMode 对其他方向渐变的工作方式类似,区别在于重复发生的方向。

更改画笔大小

如果您知道画笔将绘制到的区域的大小,则可以像我们在上面 TileMode 部分中看到的那样设置平铺 endX。如果您位于 DrawScope 中,则可以使用其 size 属性获取区域的大小。

如果您不知道绘图区域的大小(例如,如果 Brush 分配给文本),则可以扩展 Shader 并在 createShader 函数中利用绘图区域的大小。

在此示例中,将大小除以 4 以重复图案 4 次

val listColors = listOf(Color.Yellow, Color.Red, Color.Blue)
val customBrush = remember {
    object : ShaderBrush() {
        override fun createShader(size: Size): Shader {
            return LinearGradientShader(
                colors = listColors,
                from = Offset.Zero,
                to = Offset(size.width / 4f, 0f),
                tileMode = TileMode.Mirror
            )
        }
    }
}
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(customBrush)
)

Shader size divided by 4
图 3:着色器大小除以 4

您还可以更改任何其他渐变的画笔大小,例如径向渐变。如果您未指定大小和中心,则渐变将占据 DrawScope 的完整边界,并且径向渐变的中心默认为 DrawScope 边界的中心。这会导致径向渐变的中心显示为较小尺寸(宽度或高度)的中心。

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(
            Brush.radialGradient(
                listOf(Color(0xFF2be4dc), Color(0xFF243484))
            )
        )
)

Radial Gradient set without size changes
图 4:未更改大小的径向渐变

当径向渐变更改为将半径大小设置为最大尺寸时,您可以看到它产生了更好的径向渐变效果。

val largeRadialGradient = object : ShaderBrush() {
    override fun createShader(size: Size): Shader {
        val biggerDimension = maxOf(size.height, size.width)
        return RadialGradientShader(
            colors = listOf(Color(0xFF2be4dc), Color(0xFF243484)),
            center = size.center,
            radius = biggerDimension / 2f,
            colorStops = listOf(0f, 0.95f)
        )
    }
}

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(largeRadialGradient)
)

Bigger radius on radial gradient, based on size of area
图 5:径向渐变上的较大半径,基于区域大小

值得注意的是,传递到着色器创建中的实际大小取决于其调用的位置。默认情况下,如果大小与 Brush 的上次创建不同,或者着色器创建中使用的状态对象已更改,则 Brush 将在内部重新分配其 Shader

以下代码使用不同的尺寸创建了三次着色器,因为绘图区域的大小发生了变化

val colorStops = arrayOf(
    0.0f to Color.Yellow,
    0.2f to Color.Red,
    1f to Color.Blue
)
val brush = Brush.horizontalGradient(colorStops = colorStops)
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .drawBehind {
            drawRect(brush = brush) // will allocate a shader to occupy the 200 x 200 dp drawing area
            inset(10f) {
      /* Will allocate a shader to occupy the 180 x 180 dp drawing area as the
       inset scope reduces the drawing  area by 10 pixels on the left, top, right,
      bottom sides */
                drawRect(brush = brush)
                inset(5f) {
        /* will allocate a shader to occupy the 170 x 170 dp drawing area as the
         inset scope reduces the  drawing area by 5 pixels on the left, top,
         right, bottom sides */
                    drawRect(brush = brush)
                }
            }
        }
)

使用图像作为画笔

要将 ImageBitmap 用作 Brush,请将图像加载为 ImageBitmap,并创建一个 ImageShader 画笔。

val imageBrush =
    ShaderBrush(ImageShader(ImageBitmap.imageResource(id = R.drawable.dog)))

// Use ImageShader Brush with background
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(imageBrush)
)

// Use ImageShader Brush with TextStyle
Text(
    text = "Hello Android!",
    style = TextStyle(
        brush = imageBrush,
        fontWeight = FontWeight.ExtraBold,
        fontSize = 36.sp
    )
)

// Use ImageShader Brush with DrawScope#drawCircle()
Canvas(onDraw = {
    drawCircle(imageBrush)
}, modifier = Modifier.size(200.dp))

画笔应用于几种不同类型的绘制:背景、文本和画布。这输出以下内容

ImageShader Brush used in different ways
图 6:使用 ImageShader 画笔绘制背景、绘制文本和绘制圆形

请注意,文本现在也使用 ImageBitmap 来绘制文本的像素。

高级示例:自定义画笔

AGSL RuntimeShader 画笔

AGSL 提供了 GLSL 着色器功能的子集。着色器可以用 AGSL 编写,并与 Compose 中的画笔一起使用。

要创建着色器画笔,首先将着色器定义为 AGSL 着色器字符串

@Language("AGSL")
val CUSTOM_SHADER = """
    uniform float2 resolution;
    layout(color) uniform half4 color;
    layout(color) uniform half4 color2;

    half4 main(in float2 fragCoord) {
        float2 uv = fragCoord/resolution.xy;

        float mixValue = distance(uv, vec2(0, 1));
        return mix(color, color2, mixValue);
    }
""".trimIndent()

上面的着色器采用两种输入颜色,计算距绘图区域左下角 (vec2(0, 1)) 的距离,并根据距离在两种颜色之间进行 mix。这会产生渐变效果。

然后,创建着色器画笔,并为 resolution(绘图区域的大小)以及要作为输入用于自定义渐变的 colorcolor2 设置统一变量。

val Coral = Color(0xFFF3A397)
val LightYellow = Color(0xFFF8EE94)

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
@Preview
fun ShaderBrushExample() {
    Box(
        modifier = Modifier
            .drawWithCache {
                val shader = RuntimeShader(CUSTOM_SHADER)
                val shaderBrush = ShaderBrush(shader)
                shader.setFloatUniform("resolution", size.width, size.height)
                onDrawBehind {
                    shader.setColorUniform(
                        "color",
                        android.graphics.Color.valueOf(
                            LightYellow.red, LightYellow.green,
                            LightYellow
                                .blue,
                            LightYellow.alpha
                        )
                    )
                    shader.setColorUniform(
                        "color2",
                        android.graphics.Color.valueOf(
                            Coral.red,
                            Coral.green,
                            Coral.blue,
                            Coral.alpha
                        )
                    )
                    drawRect(shaderBrush)
                }
            }
            .fillMaxWidth()
            .height(200.dp)
    )
}

运行此操作后,您可以在屏幕上看到以下渲染内容

Custom AGSL Shader running in Compose
图 7:在 Compose 中运行的自定义 AGSL 着色器

值得注意的是,您可以使用着色器执行比渐变更多的事情,因为它都是基于数学的计算。有关 AGSL 的更多信息,请查看 AGSL 文档

其他资源

有关在 Compose 中使用画笔的更多示例,请查看以下资源