Pinsel: Farbverläufe und Shader

Ein Brush in Compose beschreibt, wie etwas auf dem Bildschirm gezeichnet wird. Es bestimmt die Farbe(n), die im Zeichenbereich (z. B. ein Kreis, ein Quadrat oder ein Pfad) gezeichnet werden. Es gibt einige integrierte Pinsel, die sich gut zum Zeichnen eignen, z. B. LinearGradient, RadialGradient oder ein einfacher SolidColor-Pinsel.

Pinsel können mit Modifier.background()-, TextStyle- oder DrawScope-Zeichnungsaufrufen verwendet werden, um den Malstil auf die gezeichneten Inhalte anzuwenden.

Eine horizontale Farbverlaufspinsel kann beispielsweise zum Zeichnen eines Kreises in DrawScope verwendet werden:

val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
Canvas(
    modifier = Modifier.size(200.dp),
    onDraw = {
        drawCircle(brush)
    }
)
Kreis mit horizontalem Farbverlauf
Abbildung 1: Kreis mit horizontalem Farbverlauf

Verlaufspinsel

Es gibt viele integrierte Verlaufs-Pinsel, mit denen sich verschiedene Verlaufs-Effekte erzielen lassen. Mit diesen Pinseln können Sie die Liste der Farben angeben, aus denen Sie einen Verlauf erstellen möchten.

Eine Liste der verfügbaren Farbverlaufs-Pinsel und der entsprechenden Ausgabe:

Art der Farbverlaufspinsel Ausgabe
Brush.horizontalGradient(colorList) Horizontaler Farbverlauf
Brush.linearGradient(colorList) Linearer Farbverlauf
Brush.verticalGradient(colorList) Vertikaler Farbverlauf
Brush.sweepGradient(colorList)
Hinweis: Wenn Sie einen sanften Übergang zwischen den Farben wünschen, legen Sie die letzte Farbe auf die Startfarbe fest.
Sweep-Verlauf
Brush.radialGradient(colorList) Radialer Farbverlauf

Verteilung der Farben mit colorStops ändern

Wenn Sie anpassen möchten, wie die Farben im Farbverlauf dargestellt werden, können Sie den colorStops-Wert für jede Farbe ändern. colorStops sollte als Bruch zwischen 0 und 1 angegeben werden. Werte über 1 führen dazu, dass diese Farben nicht als Teil des Farbverlaufs gerendert werden.

Sie können die Farbunterbrechungen so konfigurieren, dass sie unterschiedliche Mengen einer Farbe enthalten:

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

Die Farben werden mit dem angegebenen Offset verteilt, wie im colorStop-Paar definiert. Sie sind weniger gelb als rot und blau.

Pinsel mit verschiedenen Farbstopps
Abbildung 2: Pinsel mit verschiedenen Farbstopps

Muster mit TileMode wiederholen

Für jede Verlaufs-Pinselspitze kann ein TileMode festgelegt werden. Wenn Sie keinen Start- und Endpunkt für den Farbverlauf festgelegt haben, wird TileMode möglicherweise nicht angezeigt, da standardmäßig der gesamte Bereich ausgefüllt wird. Ein TileMode kachelt den Farbverlauf nur, wenn die Größe des Bereichs größer als die Pinselgröße ist.

Im folgenden Code wird das Gradientenmuster viermal wiederholt, da endX auf 50.dp und die Größe auf 200.dp festgelegt ist:

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

In der folgenden Tabelle wird beschrieben, was die verschiedenen Kachelmodi für das Beispiel HorizontalGradient oben bewirken:

TileMode Ausgabe
TileMode.Repeated: Die Kante wird von der letzten zur ersten Farbe wiederholt. TileMode.Repeated
TileMode.Mirror: Die Kante wird von der letzten zur ersten Farbe gespiegelt. Kachelmodus – Spiegeln
TileMode.Clamp: Die Kante wird auf die endgültige Farbe begrenzt. Anschließend wird für den Rest der Region die nächstgelegene Farbe verwendet. Klemme für den Kachelmodus
TileMode.Decal: Nur bis zur Größe der Grenzen rendern. Bei TileMode.Decal wird transparentes Schwarz verwendet, um Inhalte außerhalb der ursprünglichen Grenzen zu erfassen, während bei TileMode.Clamp die Kantenfarbe erfasst wird. Aufkleber für den Kachelmodus

TileMode funktioniert für die anderen gerichteten Verläufe ähnlich. Der Unterschied besteht in der Richtung, in der die Wiederholung erfolgt.

Pinselgröße ändern

Wenn Sie die Größe des Bereichs kennen, in dem der Pinsel verwendet wird, können Sie die Kachel endX wie oben im Abschnitt TileMode beschrieben festlegen. Wenn Sie sich in einem DrawScope befinden, können Sie die Größe des Bereichs mit der Eigenschaft size abrufen.

Wenn Sie die Größe des Zeichenbereichs nicht kennen (z. B. wenn Brush dem Text zugewiesen ist), können Sie Shader erweitern und die Größe des Zeichenbereichs in der Funktion createShader verwenden.

In diesem Beispiel wird die Größe durch 4 geteilt, um das Muster viermal zu wiederholen:

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-Größe geteilt durch 4
Abbildung 3: Shader-Größe durch 4 geteilt

Sie können auch die Pinselgröße anderer Farbverläufe ändern, z. B. bei radialen Farbverläufen. Wenn Sie keine Größe und keinen Mittelpunkt angeben, nimmt der Farbverlauf die gesamte Begrenzung des DrawScope ein und der Mittelpunkt des radialen Farbverlaufs wird standardmäßig auf den Mittelpunkt der DrawScope-Begrenzung festgelegt. Dadurch wird der Mittelpunkt des radialen Verlaufs als Mittelpunkt der kleineren Dimension (entweder Breite oder Höhe) angezeigt:

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

Radialer Farbverlauf ohne Größenänderungen
Abbildung 4: Radialer Farbverlauf ohne Größenänderungen

Wenn der radiale Farbverlauf geändert wird, um die Radiusgröße auf die maximale Dimension festzulegen, wird ein besserer radialer Farbverlaufseffekt erzielt:

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

Größerer Radius für radialen Farbverlauf basierend auf der Größe des Bereichs
Abbildung 5: Größerer Radius für radialen Farbverlauf basierend auf der Größe des Bereichs

Die tatsächliche Größe, die beim Erstellen des Shaders übergeben wird, hängt davon ab, wo er aufgerufen wird. Standardmäßig wird Brush intern neu zugewiesen, wenn die Größe sich von der letzten Erstellung von Brush unterscheidet oder wenn sich ein Statusobjekt, das bei der Erstellung des Shaders verwendet wird, geändert hat.Shader

Mit dem folgenden Code wird der Shader dreimal mit unterschiedlichen Größen erstellt, da sich die Größe des Zeichenbereichs ändert:

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

Bild als Pinsel verwenden

Wenn Sie ein ImageBitmap als Brush verwenden möchten, laden Sie das Bild als ImageBitmap und erstellen Sie einen ImageShader-Pinsel:

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

Der Pinsel wird auf verschiedene Arten von Zeichnungen angewendet: einen Hintergrund, den Text und das Canvas. Dadurch wird Folgendes ausgegeben:

ImageShader-Pinsel auf unterschiedliche Weise verwendet
Abbildung 6: Mit dem ImageShader-Pinsel einen Hintergrund, Text und einen Kreis zeichnen

Der Text wird jetzt auch mit dem ImageBitmap gerendert, um die Pixel für den Text zu malen.

Erweitertes Beispiel: Benutzerdefinierter Pinsel

AGSL RuntimeShader-Pinsel

AGSL bietet eine Teilmenge der GLSL-Shaderfunktionen. Shader können in AGSL geschrieben und mit einem Pinsel in Compose verwendet werden.

Um einen Shader-Pinsel zu erstellen, definieren Sie den Shader zuerst als AGSL-Shader-String:

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

Der Shader oben verwendet zwei Eingabefarben, berechnet den Abstand von der unteren linken Ecke (vec2(0, 1)) des Zeichenbereichs und führt basierend auf dem Abstand eine mix zwischen den beiden Farben aus. Dadurch entsteht ein Farbverlauf.

Erstellen Sie dann den Shader-Pinsel und legen Sie die Uniformen für resolution – die Größe des Zeichenbereichs – sowie für color und color2 fest, die Sie als Eingabe für Ihren benutzerdefinierten Farbverlauf verwenden möchten:

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

Wenn Sie diesen Code ausführen, wird Folgendes auf dem Bildschirm gerendert:

Benutzerdefinierter AGSL-Shader, der in Compose ausgeführt wird
Abbildung 7: Benutzerdefinierter AGSL-Shader, der in Compose ausgeführt wird

Mit Shadern lässt sich viel mehr als nur Farbverläufe erstellen, da sie auf mathematischen Berechnungen basieren. Weitere Informationen zu AGSL finden Sie in der AGSL-Dokumentation.

Zusätzliche Ressourcen

Weitere Beispiele für die Verwendung von „Brush“ in Compose finden Sie in den folgenden Ressourcen: