画笔:渐变和着色器

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 分配给 Text),您可以扩展 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))

该画笔应用于几种不同类型的绘制:背景、文本和 Canvas。输出如下

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 设置 uniforms

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 中使用 Brush 的更多示例,请查看以下资源