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). Jest kilka wbudowanych pędzli, które przydają się do rysowania. na przykład 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)
    }
)
Koło narysowane z gradientem poziomym
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 gradientowego 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.
Gradient typu „zamach”
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 kolory nie będą renderowane. jako część gradientu.

Możesz skonfigurować liczbę stopni, aby różniły się wielkością, np. mniejsze lub więcej z 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 pędzel gradientowy ma opcję ustawienia TileMode. Nie możesz zwróć uwagę na TileMode, jeśli nie ustawiono początku i końca gradientu. domyślnie wypełnia 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 działają różne tryby kafelków w przypadku powyższego przykładu:

Tryb kafelka Urządzenie wyjściowe
TileMode.Repeated: krawędź jest powtarzana od ostatniego do pierwszego koloru. TileMode Repeated
TileMode.Mirror: krawędź jest lustrzanym odbiciem ostatniego koloru na pierwszy. Lustro TileMode
TileMode.Clamp: krawędzie są przycinane do ostatecznego koloru. Reszta obszaru zostanie pomalowana na najbliższy kolor. Tryb płytki
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 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 jesteś w w tabeli DrawScope, możesz użyć jej właściwości size, by określić wielkość obszaru.

Jeśli nie znasz rozmiaru obszaru rysowania (na przykład, jeśli Brush jest przypisany do tekstu), możesz rozszerzyć zakres Shader i wykorzystać rozmiar obszar 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 dowolnego innego gradientu, na przykład promieniowego. gradientów. 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 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ń gradientu promieniowego na podstawie rozmiaru obszaru
Rysunek 5. Większy promień w przypadku gradientu promieniowego (w zależności od wielkości powierzchni)

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, 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, i Canvas. Wynik:

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

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

Przykład zaawansowany: pędzel niestandardowy

Szczoteczka AGSL RuntimeShader

AGSL udostępnia podzbiór funkcji Shadera GLSL. 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()

Powyższy cieniowanie przyjmuje 2 kolory wejściowe, oblicza odległość od dołu po lewej (vec2(0, 1)) obszaru rysowania i wykonuje mix między dwoma 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 funkcji Compose
Rysunek 7. Niestandardowy Shader AGSL działający podczas tworzenia wiadomości

Warto zauważyć, że z cieniowaniem można zrobić znacznie więcej niż tylko za pomocą gradientów. Wszystkie obliczenia są 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:

.