Brush:渐变和着色器

Compose 中的 Brush 用于描述内容在屏幕上的绘制方式:它可以确定要在绘制区域(即圆形、方形、路径)中绘制的颜色。有一些内置 Brush 对绘制非常有用,例如 LinearGradientRadialGradient 或普通的 SolidColor Brush。

Brush 可与 Modifier.background()TextStyleDrawScope 绘制调用搭配使用,以将绘制样式应用于要绘制的内容。

例如,可以在 DrawScope 中应用横向渐变 Brush 以绘制圆形:

val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
Canvas(
    modifier = Modifier.size(200.dp),
    onDraw = {
        drawCircle(brush)
    }
)

使用横向渐变绘制的圆形
图 1:使用横向渐变绘制的圆形

渐变 Brush

您可以使用许多内置渐变 Brush 来实现不同的渐变效果。借助这些 Brush,您可以指定颜色列表,以使用这些颜色创建渐变。

可用的渐变 Brush 及其对应输出的列表:

渐变 Brush 类型 输出
Brush.horizontalGradient(colorList) 横向渐变
Brush.linearGradient(colorList) 线性渐变
Brush.verticalGradient(colorList) 纵向渐变
Brush.sweepGradient(colorList)
注意:为了在不同颜色之间实现顺畅自然的过渡,请将最后一种颜色设为起始颜色。
扫描渐变
Brush.radialGradient(colorList) 径向渐变

使用 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
图 2:配置了不同颜色停止点的 Brush

使用 TileMode 重复图案

对于每个渐变 Brush,您都可以选择为其设置一个 TileMode。如果您尚未设置渐变的起始值和结束值,则可能不会注意到 TileMode,因为在默认情况下,它会填满整个区域。只有当区域大小大于 Brush 大小时,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
            )
        )
)

下表详细说明了不同 Tile Mode 对上述 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 的运作方式也是如此,区别在于重复显示沿哪个方向发生。

更改笔刷大小

如果您知道 Brush 要绘制的区域的具体大小,则可以按照 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)
)

着色器大小值除以 4
图 3:着色器大小值除以 4

您还可以更改任何其他渐变的 Brush 大小,例如径向渐变。如果您未指定大小和中心,渐变将占据 DrawScope 的整个边界,径向渐变的中心也将默认设置为 DrawScope 边界的中心。这会导致径向渐变的中心显示为较小维度(宽度或高度)的中心:

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

在大小不变的情况下设置的径向渐变
图 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)
)

对径向渐变应用更大的半径(基于区域大小)
图 5:对径向渐变应用更大的半径(基于区域大小)

值得注意的是,为创建着色器而传递的实际尺寸是由调用着色器的位置决定的。默认情况下,如果大小与上次创建 Brush 时不同,或者创建着色器时使用的状态对象发生更改,Brush 会在内部重新分配其 Shader

以下代码会随着绘制区域大小的变化,按照不同的大小创建 3 次着色器:

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

使用图片作为 Brush

如需使用 ImageBitmap 作为 Brush,请以 ImageBitmap 的形式加载相应图片,然后创建 ImageShader Brush:

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

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

以不同方式使用的 ImageShader Brush
图 6:使用 ImageShader Brush 绘制背景、文字和圆形

请注意,现在,系统在渲染相应文字时,同样会使用 ImageBitmap 来绘制文字的像素。

高级示例:自定义 Brush

AGSL RuntimeShader 笔刷

AGSL 提供了一部分 GLSL 着色器功能。着色器可以使用 AGSL 编写,并在 Compose 中与 Brush 搭配使用。

如需创建着色器 Brush,请先将着色器定义为 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。这样即可生成渐变效果。

然后,创建着色器 Brush,并设置 resolution 的 uniform,即绘制区域的大小,以及要用作自定义渐变色的输入的 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)
    )
}

运行以上代码后,您会看到屏幕上将渲染以下内容:

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

值得注意的是,除了实现渐变以外,着色器还有许多其他用途,因为着色器的本质是基于数学的计算。如需详细了解 AGSL,请参阅 AGSL 文档

其他资源

如需查看在 Compose 中使用 Brush 的更多示例,请参阅以下资源: