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 zum Zeichnen wie LinearGradient, RadialGradient oder eine einfache SolidColor-Pinsel

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

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

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 gezeichnet

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.

Eine Liste der verfügbaren Farbverlaufspinsel und ihrer 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 der Funktion "Erledigen"
Brush.radialGradient(colorList) Radialer Farbverlauf

Verteilung der Farben mit colorStops ändern

Um die Darstellung der Farben im Farbverlauf anzupassen, können Sie colorStops Wert pro Element. colorStops muss als Bruch zwischen 0 und 1 angegeben werden. Werte über 1 führen dazu, dass diese Farben nicht als Teil des Farbverlaufs.

Sie können die Farbstopps so konfigurieren, dass sie unterschiedlich groß sind, z. B. weniger oder mehrere Farben:

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
Abbildung 2: Pinsel mit verschiedenen Farbstopps konfiguriert

Muster mit TileMode wiederholen

Für jeden Farbverlaufs-Pinsel kann ein TileMode festgelegt werden. Wenn Sie keinen Anfang und kein Ende für den Farbverlauf festgelegt haben, ist das Symbol TileMode möglicherweise nicht zu sehen, da der Farbverlauf standardmäßig den gesamten Bereich füllt. Mit TileMode wird nur der Farbverlauf in Kacheln umgewandelt. wenn der Bereich größer als der Pinsel ist.

Mit dem folgenden Code wird das Farbverlaufsmuster viermal wiederholt, da endX gleich auf 50.dp und die Größe auf 200.dp festgelegt:

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, welche Funktion die verschiedenen Kachelmodi HorizontalGradient-Beispiel oben:

Kachelmodus Ausgabe
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: Die Kante wird auf die endgültige Farbe begrenzt. 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 für die anderen Richtungsverläufe, den die Richtung der Wiederholung.

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. In diesem Fall eine DrawScope haben, können Sie ihre size-Eigenschaft verwenden, um die Größe des Bereichs abzurufen.

Wenn Sie die Größe des Zeichenbereichs nicht kennen (z. B. Brush ist „Text“ zugewiesen, können Sie Shader erweitern und die in den Zeichenbereich in der Funktion createShader ein.

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: Shadergröß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. Daraus ergibt sich die Mittelpunkt, der als Mittelpunkt der kleineren Dimension (entweder Breite oder Höhe):

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

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

Wenn Sie den Radialverlauf ändern, um den Radius auf die maximale Dimension festzulegen, sehen Sie, dass ein besserer Radialverlauf entsteht:

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 beim radialen Farbverlauf, basierend auf der Größe der Fläche

Die tatsächliche Größe, die bei der Erstellung des Shader wird von dem Ort ermittelt, an dem er 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)
                }
            }
        }
)

Bilder als Pinsel verwenden

Um eine ImageBitmap als Brush zu verwenden, laden Sie das Bild als ImageBitmap hoch. und erstelle 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 Zeichenarten angewendet: auf den Hintergrund, den Text und Canvas. Dies führt zu folgender Ausgabe:

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

Beachten Sie, dass der Text jetzt ebenfalls mit ImageBitmap gerendert wird, um das Pixel für den Text.

Erweitertes Beispiel: Benutzerdefinierter Pinsel

AGSL-Pinsel "RuntimeShader"

AGSL bietet einen Teil der GLSL-Shader-Funktionen an. Shader können in AGSL geschrieben und mit einem Pinsel in Compose verwendet.

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 Farbverlaufseffekt.

Erstelle dann den Shader-Pinsel und lege die Uniformen für resolution fest – die Größe des Zeichenbereichs und die color und color2, die Sie als Eingabe für Ihren benutzerdefinierten Farbverlauf:

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: Benutzerdefinierter AGSL-Shader, der in Compose ausgeführt wird

Mit Shadern lassen sich neben Farbverläufen noch viel mehr. weil es alles um mathematische Berechnungen geht. Weitere Informationen zu AGSL finden Sie in der AGSL-Dokumentation.

Weitere Informationen

Weitere Beispiele für die Verwendung des Zeichentools in Compose finden Sie in den folgenden Ressourcen: