Pędzel: gradienty i cieniowanie

Brush w Compose opisuje sposób rysowania czegoś na ekranie: określa kolory, które są rysowane w obszarze rysowania (np. koło, kwadrat, ścieżka). Dostępnych jest kilka wbudowanych pędzli, które przydają się do rysowania, np. LinearGradient, RadialGradient lub zwykły pędzel SolidColor.

Pędzle można stosować w przypadku wywołań rysowania Modifier.background(), TextStyle lub DrawScope, aby zastosować styl malowania do rysowanej treści.

Na przykład pędzla z poziomym gradientem można użyć do narysowania okręgu w DrawScope:

val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
Canvas(
    modifier = Modifier.size(200.dp),
    onDraw = {
        drawCircle(brush)
    }
)
Okrąg narysowany za pomocą gradientu poziomego
Rysunek 1. Okrąg narysowany za pomocą gradientu poziomego

Pędzle gradientowe

Istnieje wiele wbudowanych pędzli gradientowych, które można wykorzystać do uzyskania różnych efektów gradientu. Te pędzle umożliwiają określenie listy kolorów, z których chcesz utworzyć gradient.

Lista dostępnych pędzli gradientowych i odpowiadających im wyników:

Typ pędzla gradientowego Urządzenie wyjściowe
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, ustaw ostatni kolor jako kolor początkowy.
Gradient kołowy
Brush.radialGradient(colorList) Gradient promieniowy

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

Aby dostosować wygląd kolorów w gradiencie, możesz zmienić colorStopswartość każdego z nich. colorStops należy podać jako ułamek z zakresu od 0 do 1. Wartości większe niż 1 spowodują, że te kolory nie będą renderowane jako część gradientu.

Możesz skonfigurować punkty kolorów tak, aby zawierały różne ilości, np. mniej 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 z podanym przesunięciem, zgodnie z definicją w colorStop parze, mniej żółte niż czerwone i niebieskie.

Pędzel skonfigurowany z różnymi punktami kolorów
Ilustracja 2. Pędzel skonfigurowany z różnymi punktami kolorów

Powtórz wzór za pomocą TileMode

Każdy pędzel gradientowy ma opcję ustawienia na nim TileMode. Jeśli nie ustawisz początku i końca gradientu, możesz nie zauważyć ikony TileMode, ponieważ domyślnie wypełni ona cały obszar. Wzór TileMode będzie powielany tylko wtedy, gdy rozmiar obszaru jest większy niż rozmiar pędzla.

Poniższy kod powtórzy wzór gradientu 4 razy, ponieważ endX ma wartość 50.dp, a rozmiar ma wartość 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 poniżej znajdziesz szczegółowe informacje o tym, jak poszczególne tryby kafelków działają w przypadku HorizontalGradientprzykładu powyżej:

Tryb kafelków Urządzenie wyjściowe
TileMode.Repeated: krawędź jest powtarzana od ostatniego do pierwszego koloru. TileMode Repeated
TileMode.Mirror: krawędź jest odzwierciedlana od ostatniego koloru do pierwszego. TileMode Mirror
TileMode.Clamp: krawędź jest przycinana do koloru końcowego. Następnie pomaluje pozostałą część regionu najbliższym kolorem. Zacisk w trybie kafelków
TileMode.Decal: renderowanie tylko do rozmiaru granic. TileMode.Decal wykorzystuje przezroczystą czerń do próbkowania treści poza oryginalnymi granicami, a TileMode.Clamp próbkuje kolor krawędzi. Naklejka w trybie kafelków

TileMode działa podobnie w przypadku innych gradientów kierunkowych, z tą różnicą, że powtórzenie występuje w innym kierunku.

Zmiana rozmiaru pędzla

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

Jeśli nie znasz rozmiaru obszaru rysowania (np. jeśli do elementu Brush przypisany jest tekst), możesz rozszerzyć 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 shadera podzielony przez 4
Ilustracja 3. Rozmiar shadera podzielony przez 4

Możesz też zmienić rozmiar pędzla w przypadku innych gradientów, np. gradientów promieniowych. Jeśli nie określisz rozmiaru i środka, gradient zajmie całą przestrzeń DrawScope, a środek gradientu promieniowego będzie domyślnie znajdować się w środku przestrzeni DrawScope. W rezultacie środek gradientu promienistego będzie się znajdować na środku mniejszego wymiaru (szerokości lub wysokości):

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

Zestaw gradientu promieniowego bez zmian rozmiaru
Rysunek 4. Zestaw gradientu promieniowego bez zmian rozmiaru

Gdy zmienisz gradient promienisty, aby ustawić maksymalny rozmiar promienia, zobaczysz, że daje on lepszy efekt:

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ń gradientu promieniowego na podstawie rozmiaru obszaru
Ilustracja 5. Większy promień gradientu promieniowego na podstawie rozmiaru obszaru

Warto zauważyć, że rzeczywisty rozmiar przekazywany do tworzenia cieniowania jest określany na podstawie miejsca, z którego jest wywoływany. Domyślnie funkcja Brush wewnętrznie ponownie przydzieli pamięć Shader, jeśli rozmiar będzie inny niż ostatnia kreacja Brush lub jeśli obiekt stanu użyty do utworzenia shadera ulegnie zmianie.

Poniższy kod tworzy shader 3 razy z różnymi rozmiarami, ponieważ zmienia się rozmiar 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ć obiektu 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 jest stosowany w przypadku kilku różnych typów rysowania: tła, tekstu i obszaru rysowania. Wynik będzie wyglądać tak:

Pędzel ImageShader używany na różne sposoby
Rysunek 6. Używanie pędzla ImageShader do rysowania tła, tekstu i okręgu

Zwróć uwagę, że tekst jest teraz renderowany za pomocą parametru ImageBitmap, który służy do rysowania pikseli tekstu.

Zaawansowany przykład: pędzel niestandardowy

Pędzel AGSL RuntimeShader

AGSL oferuje podzbiór funkcji cieniowania GLSL. Shadery można pisać w języku AGSL i używać ich z pędzlem w Compose.

Aby utworzyć pędzel Shader, najpierw zdefiniuj Shader jako ciąg znaków Shadera AGSL:

@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 shader przyjmuje 2 kolory wejściowe, oblicza odległość od lewego dolnego rogu (vec2(0, 1)) obszaru rysowania i wykonuje mix między tymi 2 kolorami na podstawie odległości. Daje to efekt gradientu.

Następnie utwórz pędzel cieniujący i ustaw zmienne jednolite dla resolution – rozmiaru obszaru rysowania oraz colorcolor2, które chcesz wykorzystać jako dane wejściowe dla 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)
    )
}

Po uruchomieniu tego kodu na ekranie zobaczysz:

Niestandardowy shader AGSL działający w Compose
Rysunek 7. Niestandardowy shader AGSL działający w Compose

Warto zauważyć, że za pomocą shaderów można robić znacznie więcej niż tylko gradienty, ponieważ wszystko opiera się na obliczeniach matematycznych. Więcej informacji o AGSL znajdziesz w dokumentacji tego języka.

Dodatkowe materiały

Więcej przykładów użycia klasy Brush w Compose znajdziesz w tych materiałach: