Pinsel: Farbverläufe und Shader

Ein Brush in Compose beschreibt, wie etwas auf dem Bildschirm gezeichnet wird. Damit wird die Farbe festgelegt, die im Zeichenbereich gezeichnet wird (d. h. ein Kreis, ein Quadrat oder ein Pfad). Es gibt einige integrierte Pinsel, die zum Zeichnen nützlich sind, z. B. LinearGradient, RadialGradient oder einen einfachen SolidColor-Pinsel.

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

So lässt sich beispielsweise ein horizontaler Farbverlaufs-Pinsel zum Zeichnen eines Kreises in DrawScope verwenden:

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

Kreis mit horizontalem Farbverlauf gezeichnet
Abbildung 1: Kreis mit horizontalem Farbverlauf

Farbverlaufspinsel

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

Eine Liste der verfügbaren Farbverlaufspinsel und ihrer entsprechenden Ausgabe:

Pinselart des Farbverlaufs Ausgang
Brush.horizontalGradient(colorList) Horizontaler Verlauf
Brush.linearGradient(colorList) Linearer Farbverlauf
Brush.verticalGradient(colorList) Vertikaler Farbverlauf
Brush.sweepGradient(colorList)
Hinweis: Legen Sie für die letzte Farbe die Startfarbe fest, damit der Übergang zwischen den Farben gleichmäßig wird.
Farbverlauf der Funktion "Erledigen"
Brush.radialGradient(colorList) Radialer Farbverlauf

Farbverteilung mit colorStops ändern

Wenn Sie die Darstellung der Farben im Farbverlauf anpassen möchten, können Sie den colorStops-Wert für jede Farbe anpassen. colorStops muss als Bruch zwischen 0 und 1 angegeben werden. Bei Werten über 1 werden diese Farben nicht als Teil des Farbverlaufs gerendert.

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 sind am angegebenen Versatz gemäß der Definition im Paar colorStop verteilt, weniger gelb als Rot und Blau.

Pinsel mit verschiedenen Farbstopps
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 in Kacheln umgewandelt, wenn der Bereich größer als der Pinsel ist.

Mit dem folgenden Code wird das Farbverlaufsmuster viermal wiederholt, da endX auf 50.dp und Größe auf 200.dp gesetzt 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 sehen Sie, was die verschiedenen Kachelmodi im obigen Beispiel HorizontalGradient bewirken:

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

TileMode funktioniert ähnlich wie die anderen direktionalen Gradienten, allerdings besteht der Unterschied in der Richtung, in der die Wiederholung auftritt.

Pinselgröße ändern

Wenn Sie die Größe des Bereichs kennen, in dem Ihr Pinsel gezeichnet werden soll, können Sie die Kachel endX festlegen, wie wir oben im Abschnitt TileMode gesehen haben. Wenn Sie sich in einem DrawScope befinden, können Sie dessen size-Attribut verwenden, um die Größe des Bereichs abzurufen.

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

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

Sie können auch die Pinselgröße jedes anderen Farbverlaufs ändern, z. B. radiale Farbverläufe. Wenn Sie keine Größe und kein Mittelpunkt angeben, nimmt der Farbverlauf die gesamten Grenzen von DrawScope ein. Die Mitte des radialen Farbverlaufs entspricht standardmäßig der Mitte der DrawScope-Grenzen. Das führt dazu, dass das Zentrum des radialen Farbverlaufs als Mittelpunkt der kleineren Dimension (entweder Breite oder Höhe) erscheint:

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

Radialer Farbverlauf ohne Größenänderungen festgelegt
Abbildung 4: Satz des Radialverlaufs 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 auf Grundlage des radialen Farbverlaufs, basierend auf der Flächengröße
Abbildung 5: Größerer Radius eines radialen Farbverlaufs, basierend auf der Flächengröße

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

Mit dem folgenden Code wird der Shader dreimal mit unterschiedlichen Größen erstellt, je nachdem, wie 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)
                }
            }
        }
)

Bilder als Pinsel verwenden

Wenn Sie eine ImageBitmap als Brush verwenden möchten, laden Sie das Bild als ImageBitmap hoch 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 den Canvas. Dadurch wird Folgendes ausgegeben:

Verschiedene Einsatzmöglichkeiten des ImageShader-Pinsels
Abbildung 6: Mit ImageShader-Pinsel einen Hintergrund zeichnen, Text zeichnen und Kreis zeichnen

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

Beispiel für Fortgeschrittene: Benutzerdefinierter Pinsel

AGSL-Pinsel "RuntimeShader"

AGSL bietet einen Teil der GLSL-Shader-Funktionen an. Shader können in AGSL geschrieben und in Compose mit einem Pinsel 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 obige Shader verwendet zwei Eingabefarben, berechnet den Abstand von links unten (vec2(0, 1)) des Zeichenbereichs und führt einen mix zwischen den beiden Farben basierend auf der Entfernung aus. Dadurch entsteht ein Farbverlaufseffekt.

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 diesen Befehl ausführen, sehen Sie Folgendes auf dem Bildschirm:

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

Mit Shadern können Sie neben Farbverläufen noch viel mehr tun, da alles mathematisch basiert. Weitere Informationen zu AGSL finden Sie in der AGSL-Dokumentation.

Zusätzliche Ressourcen

Weitere Beispiele zur Verwendung von Prush in Compose finden Sie in den folgenden Ressourcen: