Pędzel: gradienty i cieniowanie

Element Brush w sekcji Utwórz opisuje sposób rysowania elementów na ekranie.Określa on kolory, które zostaną rysowane w obszarze rysowania (tj. okrąg, kwadrat, ścieżkę). Dostępnych jest kilka wbudowanych pędzli, które przydają się do rysowania, np. LinearGradient, RadialGradient i zwykły pędzel SolidColor.

Aby zastosować styl malowania do rysowanych treści, możesz używać pędzli z elementami Modifier.background(), TextStyle i DrawScope.

Na przykład do narysowania okręgu w aplikacji DrawScope można użyć pędzla poziomego gradientu:

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

Okrąg narysowany z gradientem poziomym
Rysunek 1. Okrąg narysowany z gradientem poziomym

Pędzle w kolorze gradientu

Wbudowanych jest wiele pędzli gradientu, których można używać do uzyskiwania różnych efektów gradientu. Te pędzle pozwalają określić listę kolorów, z których chcesz utworzyć gradient.

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

Rodzaj pędzla gradientowego Odpowiedź
Brush.horizontalGradient(colorList) Gradient poziomy
Brush.linearGradient(colorList) Gradient liniowy
Brush.verticalGradient(colorList) Gradient pionowy
Brush.sweepGradient(colorList)
Uwaga: aby zapewnić płynne przejście między kolorami, ustaw ostatni kolor na kolor początkowy.
Gradient przeciśnięcia
Brush.radialGradient(colorList) Gradient promieniowy

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

Aby dostosować sposób wyświetlania kolorów w gradiencie, możesz dostosować wartość colorStops w każdym z nich. Wartość colorStops należy podawać jako ułamek z zakresu od 0 do 1. Jeśli podasz wartość większą niż 1, kolory te nie będą renderowane w ramach gradientu.

Możesz skonfigurować różne ilości kolorów, 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 w podanym przesunięciach, zgodnie z wartością pary colorStop. Kolor jest mniej żółty niż czerwony i niebieski.

Pędzel skonfigurowany z różnymi ograniczeniami kolorów
Rysunek 2.: Pędzel skonfigurowany z różnymi odcieniami

Powtórz wzór w aplikacji TileMode

Każdy pędzel gradientu ma opcję ustawienia TileMode. Jeśli nie ustawisz początku i końca gradientu, możesz nie zauważyć elementu TileMode, ponieważ domyślnie wypełni on cały obszar. TileMode umieszcza gradient obok siebie 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 z opisem działania różnych trybów kafelków w powyższym przykładzie HorizontalGradient:

Tryb kafelków Odpowiedź
TileMode.Repeated: krawędź powtarza się od ostatniego koloru do pierwszego. Powtórzenie trybu TileMode
TileMode.Mirror: krawędź jest powielana od ostatniego koloru do pierwszego. Lustro TileMode
TileMode.Clamp: krawędź jest przycięta do ostatecznego koloru. Następnie maluje najbliższy kolor pozostałym regionowi. Zacisk kafelka
TileMode.Decal: renderowanie do wartości granicznych. TileMode.Decal wykorzystuje przezroczystą czerń, aby próbkować treści poza pierwotnymi granicami, a TileMode.Clamp próbkuje kolor krawędzi. Naklejka z trybem kafelków

Funkcja TileMode działa w podobny sposób w przypadku innych gradientów kierunkowych. Różnica polega na tym, gdzie następuje powtarzanie.

Zmień rozmiar pędzla

Jeśli znasz rozmiar obszaru, w którym zostanie narysowany pędzel, możesz ustawić kafelek endX, tak jak w sekcji TileMode. Jeśli znajdujesz się w lokalizacji DrawScope, możesz użyć jej właściwości size, aby poznać wielkość obszaru.

Jeśli nie znasz rozmiaru obszaru rysowania (np. jeśli element Brush jest przypisany do tekstu), możesz rozszerzyć zakres Shader i użyć rozmiaru obszaru rysowania w funkcji createShader.

W tym przykładzie podziel rozmiar przez 4, aby powtórzyć wzorzec 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 cienia podzielony przez 4
Rysunek 3. Rozmiar cienia 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 obszaru DrawScope, a sam środek gradientu promieniowego zajmie środek tych granic: DrawScope. W ten sposób środek gradientu promieniowego wyświetla się po środku mniejszego wymiaru (szerokości lub wysokości):

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

Ustawiono gradient promieniowy bez zmian rozmiaru
Rys. 4. Zestaw gradientu promieniowego bez zmian rozmiaru

Po zmianie gradientu promieniowego w celu ustawienia rozmiaru promienia na wymiar maksymalny widać, że zapewnia to 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 w zależności od wielkości obszaru.
Rysunek 5. Większy promień w gradientie promieniowym w zależności od wielkości obszaru

Warto zauważyć, że rzeczywisty rozmiar przekazywany do tworzenia cienia jest określany w miejscu, w którym jest wywoływany. Domyślnie Brush przeniesie wewnętrznie element Shader, jeśli jego rozmiar będzie inny niż przy ostatnim utworzonym elemencie Brush lub jeśli zmieni się obiekt stanu używany do utworzenia programu do cieniowania.

Poniższy kod powoduje utworzenie cieniowania 3 razy o różnych rozmiarach, gdy 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ć 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 można stosować do różnych rodzajów rysunków: tła, tekstu i obszaru roboczego. Uzyskane wyniki:

Różne sposoby pędzla ImageShader
Rys. 6. Rysowanie tła za pomocą pędzla ImageShader, tekstu oraz okręgu

Zwróć uwagę, że tekst jest teraz renderowany za pomocą elementu ImageBitmap do malowania pikseli w tekście.

Przykład zaawansowany: pędzel niestandardowy

Pędzel AGSL RuntimeShader

AGSL oferuje podzbiór funkcji Shadera w GLSL. Shadery można pisać w AGSL, a potem używać pędzla w tworzeniu wiadomości.

Aby utworzyć pędzel do cieniowania, najpierw zdefiniuj pędzel do cieniowania jako ciąg cieniowania 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 kreator stosuje 2 kolory wejściowe, oblicza odległość od lewego dolnego rogu obszaru rysowania (vec2(0, 1)) i wykona mix między 2 kolorami na podstawie odległości. Powoduje to efekt gradientu.

Następnie utwórz Pędzel cienia i ustaw uniformy dla elementu resolution, czyli rozmiaru obszaru rysowania, oraz elementów color i color2, których chcesz używać 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)
    )
}

Po jego uruchomieniu zobaczysz wyrenderowane na ekranie te elementy:

Niestandardowy Shader AGSL uruchomiony w Compose
Rysunek 7. Niestandardowy obiekt Shader AGSL uruchomiony w sekcji Compose

Warto zauważyć, że cieniowanie to o wiele więcej niż gradienty – wszystkie obliczenia opierają się na obliczeniach matematycznych. Więcej informacji o AGSL znajdziesz w dokumentacji AGSL.

Dodatkowe materiały

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