Pędzel: gradienty i cieniowanie

Element Brush w sekcji „Utwórz” określa, jak coś jest rysowane na ekranie: określa kolory rysowane w obszarze rysowania (np. koło, kwadrat, ścieżka). Do rysowania możesz używać kilku wbudowanych pędzli, takich jak LinearGradient, RadialGradient lub zwykły pędzel SolidColor.

Pędzli można używać w połączeniu z metodami rysowania Modifier.background(), TextStyle lub DrawScope, aby zastosować styl rysunku do rysowanej treści.

Na przykład pędzel z poziomym gradientem można zastosować do narysowania koła:DrawScope

val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
Canvas(
    modifier = Modifier.size(200.dp),
    onDraw = {
        drawCircle(brush)
    }
)
Kółko narysowane za pomocą gradientu poziomego
Rysunek 1.: koło narysowane za pomocą gradientu poziomego

Pędzle gradientowe

Dostępnych jest wiele wbudowanych pędzli gradientów, które można wykorzystać do uzyskania różnych efektów 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 gradientu Urządzenie wyjściowe
Brush.horizontalGradient(colorList) Gradient poziomy
Brush.linearGradient(colorList) Gradient liniowy
Brush.verticalGradient(colorList) Pionowy gradient
Brush.sweepGradient(colorList)
Uwaga: aby uzyskać płynne przejście między kolorami, jako ostatni ustaw kolor początkowy.
Sweep Gradient
Brush.radialGradient(colorList) Gradient promienisty

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ć podana jako ułamek w zakresie 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 koloru, aby miały różne wartości, na przykład 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 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

Powtarzanie wzoru za pomocą 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łni on cały obszar. TileMode będzie stosować gradient 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
            )
        )
)

Oto tabela, która szczegółowo opisuje, jak różne tryby kafelków działają w przypadku przykładu HorizontalGradient:

TileMode Urządzenie wyjściowe
TileMode.Repeated: krawędź jest powtarzana od ostatniego koloru do pierwszego. Powtórzenie TileMode
TileMode.Mirror: odbicie lustrzane krawędzi od ostatniego koloru do pierwszego. TileMode Mirror
TileMode.Clamp: krawędzie są przycinane do ostatecznego koloru. Następnie wypełnia resztę regionu najbliższym kolorem. Tryb płytki
TileMode.Decal: renderowanie tylko do rozmiaru granic. TileMode.Decal wykorzystuje przezroczystą czerń do próbkowania treści poza oryginalnymi granicami, podczas gdy TileMode.Clamp próbkuje kolor krawędzi. Naklejka w trybie płytki

Funkcja TileMode działa w podobny sposób 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, na którym ma działać pędzel, możesz ustawić płytkę endX tak, 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 rysunku (np. jeśli Brush jest przypisany do tekstu), możesz rozszerzyć Shader i wykorzystać rozmiar obszaru rysunku 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 dowolnego innego gradientu, np. 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 znajdować się w środku mniejszego wymiaru (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. Gradient promieniowy bez zmian rozmiaru

Gdy gradient promieniowy zostanie zmieniony tak, aby rozmiar promienia odpowiadał maksymalnej wartości wymiaru, 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ń w gradientie promieniowym, w zależności od rozmiaru obszaru.
Ilustracja 5. Większy promień gradientu promieniowego na podstawie rozmiaru obszaru

Warto zauważyć, że rzeczywisty rozmiar przekazywany podczas tworzenia shadera jest określany w miejscu jego wywołania. Domyślnie Brush przydzieli wewnętrznie Shader, jeśli rozmiar jest inny niż podczas ostatniego utworzenia Brush lub jeśli obiekt stanu użyty do utworzenia shadera uległ zmianie.

Podany niżej kod tworzy shader 3 razy w różnych rozmiarach, gdy zmienia się obszar 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, załaduj 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 do kilku różnych typów rysunków: tła, tekstu i płótna. Wynik:

Pędzel ImageShader używany na różne sposoby
Rysunek 6. Za pomocą pędzla ImageShader rysowanie tła, tekstu i okręgu

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

Przykład zaawansowany: niestandardowy pędzel

Pędzel AGSL RuntimeShader

AGSL udostępnia podzbiór funkcji GLSL Shader. Programiści mogą pisać shadery 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()

Podany powyżej shader przyjmuje 2 kolory wejściowe, oblicza odległość od dolnej lewej krawędzi (vec2(0, 1)) obszaru rysunku i wykonywuje mix między tymi 2 kolorami na podstawie odległości. Powoduje to efekt gradientu.

Następnie utwórz pędzel Shader i ustaw wartości uniformów dla resolution – rozmiaru obszaru rysunku oraz colorcolor2, które chcesz użyć jako dane wejściowe do gradientu niestandardowego:

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 zobaczysz na ekranie następujące elementy:

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

Warto pamiętać, że shadery mogą służyć do innych celów niż tylko do tworzenia gradientów, ponieważ są to obliczenia oparte na matematyce. Więcej informacji o AGSL znajdziesz w dokumentacji dotyczącej tego tematu.

Dodatkowe materiały

Więcej przykładów korzystania z narzędzia Pędzel w usłudze Compose znajdziesz w tych materiałach: