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. Te pędzle umożliwiają określenie listy kolorów, z których ma powstać gradient.

Lista dostępnych pędzli gradientu i ich odpowiednich wyników:

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, ustaw ostatni kolor jako kolor początkowy.
Sweep Gradient
Brush.radialGradient(colorList) Gradient promieniowy

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

Aby dostosować sposób wyświetlania kolorów w gradientach, możesz zmienić wartość parametru colorStops dla 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 z użyciem podanego przesunięcia, jak to określono w parze colorStop, mniej żółte niż czerwone i niebieskie.

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

Powtarzanie wzoru za pomocą TileMode

Każdy gradientowy pędzel ma opcję TileMode. Jeśli nie ustawisz początku ani końca gradientu, TileMode może nie być widoczne, ponieważ domyślnie wypełnia ono całą powierzchnię. 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. TileMode Repeated
TileMode.Mirror: krawędź jest lustrzanym odbiciem ostatniego koloru na pierwszy. 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ć kafelek endX tak, jak to opisaliśmy 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 elementu DrawScope, a środek gradientu promienistego będzie domyślnie ustawiony na środek granic DrawScope. W efekcie środek gradientu promienistego 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))
            )
        )
)

Gradient promieniowy bez zmian rozmiaru
Rysunek 4. Gradient promieniowy bez zmian rozmiaru

Gdy gradient promieniowy zostanie zmieniony tak, aby rozmiar promienia odpowiadał maksymalnej wymiarowi, zobaczysz, że daje on lepszy efekt gradientu promieniowego:

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 pamiętać, że rzeczywisty rozmiar przekazywany do tworzenia shadera jest określany na podstawie miejsca, w którym jest wywoływany. 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. Używanie pędzla ImageShader do rysowania tła, tekstu i okręgu

Zwróć uwagę, że tekst jest teraz renderowany za pomocą ImageBitmap, aby wypełnić piksele tekstu.

Przykład zaawansowany: pędzel niestandardowy

Pędzel AGSL RuntimeShader

AGSL oferuje 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: