Pinsel: Farbverläufe und Shader

Ein Brush in der Funktion „Compose“ beschreibt, wie etwas auf dem Bildschirm dargestellt wird. Es bestimmt die Farbe(n), die im Zeichnungsbereich gezeichnet wird (z. B. ein Kreis, ein Quadrat oder ein Pfad). Es gibt einige integrierte Pinsel, die sich zum Zeichnen eignen, z. B. LinearGradient, RadialGradient oder ein einfacher Pinsel für SolidColor.

Pinsel kann mit den Zeichenaufrufen Modifier.background(), TextStyle oder DrawScope verwendet werden, um den Zeichenstil auf die gezeichneten Inhalte anzuwenden.

Beispielsweise kann ein horizontaler Farbverlaufspinsel auf das Zeichnen eines Kreises in DrawScope angewendet werden:

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

Farbverlaufpinsel

Es gibt viele integrierte Farbverlaufspinsel, mit denen sich verschiedene Farbverlaufseffekte erzielen lassen. Mit diesen Pinseln kannst du eine Liste der Farben angeben, aus denen du einen Farbverlauf erstellen möchtest.

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

Farbverlaufpinseltyp Ausgang
Brush.horizontalGradient(colorList) Horizontaler Farbverlauf
Brush.linearGradient(colorList) Linearer Verlauf
Brush.verticalGradient(colorList) Vertikaler Farbverlauf
Brush.sweepGradient(colorList)
Hinweis: Für einen weichen Übergang zwischen den Farben sollten Sie als Startfarbe die letzte Farbe festlegen.
Farbverlauf
Brush.radialGradient(colorList) Radialverlauf

Farbverteilung mit colorStops ändern

Um die Darstellung der Farben im Farbverlauf anzupassen, können Sie den colorStops-Wert 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 für eine 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 mit dem für das Paar colorStop definierten Offset verteilt, also weniger Gelb als Rot und Blau.

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

Muster mit TileMode wiederholen

Für jeden Farbverlaufspinsel kann ein TileMode festgelegt werden. Sie bemerken TileMode möglicherweise nicht, wenn Sie keinen Start und kein Ende für den Farbverlauf festgelegt haben, da er standardmäßig den gesamten Bereich ausfüllt. TileMode kachelt den Farbverlauf nur dann, wenn die Größe des Bereichs größer als die Pinselgröße ist.

Mit dem folgenden Code wird das Farbverlaufsmuster 4-mal wiederholt, da endX auf 50.dp und die 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 wird die Wirkung der verschiedenen Kachelmodi für das obige HorizontalGradient-Beispiel beschrieben:

Kachelmodus Ausgang
TileMode.Repeated: Der Rand wird von der letzten Farbe zur ersten Farbe wiederholt. TileMode (wiederholt)
TileMode.Mirror: Der Rand wird von der letzten zur ersten Farbe gespiegelt. TileMode-Spiegel
TileMode.Clamp: Rand wird an die endgültige Farbe gebunden. Dann wird die nächstgelegene Farbe für den Rest der Region verwendet. Kachelmodus-Clamp
TileMode.Decal: Nur bis zur Größe der Grenzen wird gerendert. Bei TileMode.Decal wird transparentes Schwarz verwendet, um Inhalte außerhalb der ursprünglichen Begrenzungen zu erfassen. Bei TileMode.Clamp hingegen wird die Kantenfarbe entnommen. Kachelmodus-Aufkleber

TileMode funktioniert auf ähnliche Weise für die anderen richtungsweisenden Gradienten, wobei der Unterschied in der Richtung der Wiederholung liegt.

Zeichenwerkzeug ändern

Wenn Sie die Größe des Bereichs kennen, in dem der Pinsel gezeichnet wird, können Sie die endX der Kachel festlegen, wie oben im Abschnitt TileMode gezeigt. Wenn du dich in einer DrawScope befindest, kannst du deren size-Eigenschaft verwenden, um die Größe des Bereichs abzurufen.

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

Teilen Sie in diesem Beispiel 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 eines anderen Farbverlaufs ändern, z. B. radiale Farbverläufe. Wenn Sie keine Größe und keinen Mittelpunkt angeben, belegt der Farbverlauf die vollständigen Grenzen von DrawScope und der Mittelpunkt des radialen Farbverlaufs liegt standardmäßig im Mittelpunkt der DrawScope-Grenzen. Dies führt dazu, dass der Mittelpunkt 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))
            )
        )
)

Radialverlauf ohne Größenänderungen festgelegt
Abbildung 4: Radialverlauf ohne Größenänderungen

Wenn Sie den radialen Farbverlauf ändern, um die Radiusgröße auf die maximale Dimension zu setzen, erzielen Sie einen besseren radialen Farbverlauf:

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 des Bereichs
Abbildung 5: Größerer Radius auf dem radialen Farbverlauf basierend auf der Größe der Fläche

Beachten Sie, dass die tatsächliche Größe, die bei der Erstellung des Shaders übergeben wird, dort bestimmt wird, wo sie 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 Statusobjekt, das zum Erstellen des Shaders verwendet wurde, geändert hat.

Mit dem folgenden Code wird der Shader drei verschiedene Male mit unterschiedlichen Größen erstellt, wenn 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 du eine ImageBitmap als Brush verwenden möchtest, lade 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 Arten von Zeichnungen angewendet: Hintergrund, Text und Canvas. Dadurch wird Folgendes ausgegeben:

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

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

Erweitertes Beispiel: Benutzerdefinierter Pinsel

AGSL: RuntimeShader-Pinsel

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

Definieren Sie zum Erstellen eines Shader-Pinsels zuerst den Shader 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 nimmt zwei Eingabefarben, berechnet den Abstand von unten links (vec2(0, 1)) des Zeichnungsbereichs und führt basierend auf der Entfernung eine mix zwischen den beiden Farben durch. Dadurch entsteht ein Farbverlaufseffekt.

Erstellen Sie dann den Shader-Pinsel und legen Sie die Uniformen für resolution fest. Das ist 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)
    )
}

Dadurch wird Folgendes auf dem Bildschirm gerendert:

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

Es ist zu beachten, dass Sie mit Shadern viel mehr als nur mit Farbverläufen tun können, da es sich um alles Mathematik-basierte Berechnungen handelt. Weitere Informationen zu AGSL finden Sie in der AGSL-Dokumentation.

Weitere Informationen

Weitere Beispiele für die Verwendung des Pinsels in der Funktion „Compose“ finden Sie in den folgenden Ressourcen: