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 gezeichnet werden (z.B. ein Kreis, Quadrat oder Pfad). Es gibt einige integrierte Pinsel, die sich zum Zeichnen eignen, z. B. LinearGradient, RadialGradient oder ein einfacher SolidColor-Pinsel.

Pinsel können mit Modifier.background()-, TextStyle- oder DrawScope-Zeichenaufrufen verwendet werden, um den Malstil auf den zu zeichnenden Inhalt anzuwenden.

Mit einem horizontalen Farbverlaufs-Pinsel können Sie beispielsweise einen Kreis in DrawScope zeichnen:

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

Farbverlaufspinsel

Es gibt viele integrierte Farbverlaufs-Pinsel, mit denen Sie verschiedene Farbverlaufseffekte erzielen können. Mit diesen Pinseln können Sie die Liste der Farben angeben, aus denen Sie einen Farbverlauf erstellen möchten.

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

Art des Farbverlaufs-Pinsels Ausgabe
Brush.horizontalGradient(colorList) Horizontaler Verlauf
Brush.linearGradient(colorList) Linearer Farbverlauf
Brush.verticalGradient(colorList) Vertikaler Farbverlauf
Brush.sweepGradient(colorList)
Hinweis: Für einen reibungslosen Übergang zwischen den Farben sollten Sie die letzte Farbe auf die Startfarbe festlegen.
Farbverlauf
Brush.radialGradient(colorList) Radialer Farbverlauf

Verteilung der Farben mit colorStops ändern

Sie können die Darstellung der Farben im Farbverlauf anpassen, indem Sie den Wert für colorStops für jede Farbe anpassen. colorStops muss 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 Farbstopps so konfigurieren, dass sie unterschiedliche Mengen haben, z. B. weniger oder mehr einer Farbe:

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 im colorStop-Paar angegebenen Offset verteilt, wobei weniger Gelb als Rot und Blau verwendet wird.

Pinsel mit verschiedenen Farbstopps konfiguriert
Abbildung 2: Mit verschiedenen Farbstopps konfigurierte Pinsel

Muster mit TileMode wiederholen

Für jeden Farbverlauf kann ein TileMode festgelegt werden. TileMode wird möglicherweise nicht bemerkt, wenn Sie keinen Anfang und kein Ende für den Farbverlauf festgelegt haben, da standardmäßig der gesamte Bereich ausgefüllt wird. Mit einem TileMode wird der Farbverlauf nur dann gekachtelt, wenn die Größe des Bereichs größer als die Größe des Zeichentools ist.

Im folgenden Code wird das Farbverlaufsmuster 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 Ansichtsmodi für das Beispiel HorizontalGradient oben bewirken:

TileMode Ausgabe
TileMode.Repeated: Der Rand wird von der letzten Farbe zur ersten wiederholt. TileMode Repeated
TileMode.Mirror: Die Kante wird von der letzten zur ersten Farbe gespiegelt. Kachelmodus-Spiegel
TileMode.Clamp: Der Rand wird auf die endgültige Farbe begrenzt. Dann wird die Farbe für den Rest der Region verwendet. Kachelmodus-Klemme
TileMode.Decal: Rendert nur bis zur Größe der Begrenzung. Bei TileMode.Decal wird transparentes Schwarz verwendet, um Inhalte außerhalb der ursprünglichen Grenzen zu erfassen, während bei TileMode.Clamp die Randfarbe erfasst wird. Aufkleber für den Kachelmodus

TileMode funktioniert für die anderen Richtungsverlä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 gezeichnet werden soll, 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 anhand der Property size ermitteln.

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

In diesem Beispiel teilen Sie die Größe durch 4, 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)
)

Shadergröße geteilt durch 4
Abbildung 3: Shader-Größe geteilt durch 4

Sie können die Pinselgröße auch für andere Farbverläufe ändern, z. B. für radiale Farbverläufe. Wenn Sie keine Größe und keinen Mittelpunkt angeben, nimmt der Farbverlauf die gesamten Grenzen des DrawScope ein. Der Mittelpunkt des radialen Farbverlaufs entspricht dann standardmäßig dem Mittelpunkt der DrawScope-Grenzen. Dadurch wird der Mittelpunkt des radialen Farbverlaufs 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 zu setzen, 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 beim radialen Farbverlauf, basierend auf der Größe der Fläche
Abbildung 5: Größerer Radius beim radialen Farbverlauf, basierend auf der Größe der Fläche

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

Im 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 eine 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: Hintergrund, Text und Canvas. Dies führt zu folgender Ausgabe:

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

Beachten Sie, dass der Text jetzt auch mit dem ImageBitmap gerendert wird, um die Pixel für den Text zu zeichnen.

Beispiel für Fortgeschrittene: Benutzerdefinierter Pinsel

AGSL-RuntimeShader-Bürste

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

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

@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 obige Shader nimmt zwei Eingabefarben, berechnet die Entfernung vom unteren linken Rand (vec2(0, 1)) des Zeichenbereichs und führt basierend auf der Entfernung einen mix zwischen den beiden Farben aus. Dadurch entsteht ein Farbverlauf.

Erstellen Sie dann den Shader-Pinsel und legen Sie die Uniformen für resolution fest: die Größe des Zeichenbereichs sowie die color und color2, die Sie als Eingabe für den 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 das ausführen, wird Folgendes auf dem Bildschirm gerendert:

Benutzerdefinierter AGSL-Shader, der in Compose ausgeführt wird
Abbildung 7: Ausführung eines benutzerdefinierten AGSL-Shaders in Compose

Mit Shadern lassen sich nicht nur Farbverläufe erstellen, da es sich um mathematische Berechnungen handelt. Weitere Informationen zu AGSL finden Sie in der AGSL-Dokumentation.

Weitere Informationen

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