Pędzel: gradienty i cieniowanie

Symbol Brush w narzędziu Compose opisuje, jak coś jest rysowane na ekranie: określa kolory w obszarze rysowania (np. koło, kwadrat, ścieżkę). Dostępnych jest kilka wbudowanych pędzli, które przydają się do rysowania, na przykład LinearGradient, RadialGradient oraz zwykły pędzel SolidColor.

Z wywołaniami rysowania pędzli Modifier.background(), TextStyle i DrawScope można stosować styl malowania do rysowanych treści.

Na przykład za pomocą pędzla z gradientem w poziomie można rysować koło w trybie DrawScope:

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

Koło narysowane z gradientem poziomym
Rysunek 1. Koło narysowane przy użyciu gradientu poziomego

Pędzle gradientowe

Dostępnych jest wiele wbudowanych pędzli gradientowych, za pomocą których można uzyskać różne efekty gradientu. Pozwalają one określić listę kolorów, z których chcesz utworzyć gradient.

Lista dostępnych pędzli gradientu i odpowiadające im dane wyjściowe:

Typ pędzla gradientowego Odpowiedź
Brush.horizontalGradient(colorList) Gradient poziomy
Brush.linearGradient(colorList) Gradient liniowy
Brush.verticalGradient(colorList) Gradient pionowy
Brush.sweepGradient(colorList)
Uwaga: aby uzyskać płynne przejście między kolorami, jako ostatni ustaw kolor początkowy.
Gradient typu „zamach”
Brush.radialGradient(colorList) Gradient promieniowy

Zmień rozkład kolorów za pomocą funkcji colorStops

Aby dostosować sposób wyświetlania kolorów w gradientie, możesz dostosować wartość colorStops każdego z nich. Wartość colorStops powinna być określona jako ułamek między 0 a 1. Jeśli ustawisz wartość większą niż 1, kolory nie będą renderowane w ramach gradientu.

Możesz skonfigurować liczbę stopni, aby miały różne wartości, np. mniejszy lub więcej jednego koloru:

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

Kolory są rozproszone przy podanym przesunięciu zgodnie z definicją w parze colorStop, mniej żółty niż czerwony i niebieski.

Skonfigurowano pędzlem z różnymi kolorami
Rysunek 2. Szczoteczka skonfigurowana z różnymi stopniami kolorów

Powtórz wzór za pomocą funkcji TileMode

Każdy pędzel gradientowy ma opcję ustawienia TileMode. Możesz nie zauważyć TileMode, jeśli nie ustawiono początku i końca gradientu, ponieważ domyślnie wypełnia on cały obszar. Element TileMode ułoży gradient tylko wtedy, gdy rozmiar obszaru jest większy niż rozmiar pędzla.

Ten kod powtórzy wzorzec gradientu 4 razy, ponieważ endX ma wartość 50.dp, a rozmiar 200.dp:

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

W tabeli znajdziesz szczegółowe informacje o tym, jak różne tryby kafelka działają w powyższym przykładzie HorizontalGradient:

Tryb kafelka Odpowiedź
TileMode.Repeated: krawędź jest powtarzana od ostatniego do pierwszego koloru. Powtórzenie TileMode
TileMode.Mirror: odbicie lustrzane krawędzi od ostatniego koloru do pierwszego. Lustro TileMode
TileMode.Clamp: krawędź jest przycięta do ostatecznego koloru. Reszta obszaru zostanie pomalowana na najbliższy kolor. Zacisk trybu kafelka
TileMode.Decal: renderuj tylko do rozmiaru granic. W modelu TileMode.Decal użyto przezroczystej czerni do próbkowania treści poza pierwotnymi granicami, a TileMode.Clamp – kolor krawędzi. Naklejka trybu kafelka

Funkcja TileMode działa podobnie w przypadku innych gradientów kierunkowych, ale różnica jest kierunkiem powtórzenia.

Zmień rozmiar pędzla

Jeśli znasz rozmiar obszaru, w którym będzie narysowany pędzel, możesz ustawić kafelek endX w taki sposób, jak pokazano powyżej w sekcji TileMode. Jeśli znajdujesz się w regionie DrawScope, możesz użyć jego właściwości size, aby poznać rozmiar obszaru.

Jeśli nie znasz rozmiaru obszaru rysowania (np. jeśli do pola Brush jest przypisany tekst), możesz wydłużyć Shader i wykorzystać rozmiar obszaru rysowania w funkcji createShader.

W tym przykładzie podziel rozmiar przez 4, aby powtórzyć wzór 4 razy:

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

Rozmiar cieniowania podzielony przez 4
Rysunek 3. Rozmiar cienia podzielony przez 4

Możesz też zmienić rozmiar pędzla dowolnego innego gradientu, na przykład gradientu promieniowego. Jeśli nie określisz rozmiaru i środka, gradient zajmie pełne granice obiektu DrawScope, a środek gradientu promieniowego domyślnie znajdzie się w środku tych granic DrawScope. W efekcie środek gradientu promieniowego będzie widoczny jako środek mniejszego wymiaru (czyli szerokości lub wysokości):

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

Ustawiono gradient promieniowy bez zmiany rozmiaru
Rysunek 4. Zestaw gradientów promieniowych bez zmian rozmiaru

Widać, że po zmianie gradientu promieniowego na maksymalny wymiar gradientu uzyskania lepszego efektu:

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

Większy promień w gradientie promieniowym, w zależności od rozmiaru obszaru.
Rysunek 5. Większy promień w przypadku gradientu promieniowego i rozmiar powierzchni

Warto zauważyć, że rzeczywisty rozmiar przekazywany podczas tworzenia shadera jest określany w miejscu jego wywołania. Domyślnie Brush zmieni lokalizację elementu Shader wewnętrznie, jeśli jego rozmiar będzie się różnić od ostatniego utworzenia obiektu Brush lub zmieni się obiekt stanu używany do tworzenia cieniowania.

Ten kod tworzy cieniowanie 3 razy z różnymi rozmiarami odpowiednio do wielkości obszaru rysowania:

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

Używanie obrazu jako pędzla

Aby użyć ImageBitmap jako Brush, wczytaj obraz jako ImageBitmap i utwórz pędzel ImageShader:

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

Pędzel można stosować do kilku różnych rodzajów rysowania: tła, tekstu i obszaru roboczego. Zwrócony wynik:

Różne sposoby używania pędzla ImageShader
Rysunek 6. Rysowanie tła, rysowanie tekstu i kreślenie okręgu za pomocą pędzla ImageShader

Zwróć uwagę, że tekst jest teraz również renderowany za pomocą ImageBitmap do malowania pikseli tekstu.

Przykład zaawansowany: niestandardowy pędzel

Szczoteczka AGSL RuntimeShader

AGSL udostępnia podzbiór funkcji Shadera GLSL. Shader można pisać w języku AGSL i używać za pomocą pędzla w tworzeniu wiadomości.

Aby utworzyć pędzel do cieniowania, najpierw zdefiniuj ciąg Shader jako ciąg AGSL do cieniowania:

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

Powyższy cieniowanie przyjmuje 2 kolory wejściowe, oblicza odległość od lewego dolnego obszaru rysowania (vec2(0, 1)) i na podstawie odległości robi mix między tymi dwoma kolorami. Daje to efekt gradientu.

Następnie utwórz pędzel do cieniowania i ustaw jednolity resolution – rozmiar obszaru rysowania oraz color i color2, których chcesz użyć jako danych wejściowych do niestandardowego gradientu:

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

Gdy go uruchomisz, na ekranie zobaczysz taki kod:

Niestandardowy Shader AGSL działający w funkcji Compose
Rysunek 7. Niestandardowy Shader AGSL uruchomiony w trakcie tworzenia wiadomości

Trzeba pamiętać, że cieniowanie pozwalają o wiele więcej zrobić niż tylko gradienty, bo wszystkie obliczenia są oparte na matematyce. Więcej informacji o AGSL znajdziesz w dokumentacji tego języka.

Dodatkowe materiały

Więcej przykładów użycia pędzla w funkcji tworzenia wiadomości znajdziesz w tych materiałach: