Pędzel: gradienty i cieniowanie

Brush w Compose opisuje, jak coś jest rysowane 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 przydadzą 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 ten sposób: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 z użyciem gradientu poziomego

Pędzle gradientowe

Dostępnych jest wiele wbudowanych pędzli gradientowych, które pozwalają uzyskać różne efekty 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 Wyniki
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ć sposób wyświetlania 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 zdefiniowanym 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 jest ustawiony na 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 HorizontalGradientprzykładzie powyżej:

TileMode Wyniki
TileMode.Repeated: krawędź jest powtarzana od ostatniego do pierwszego koloru. TileMode.Repeated
TileMode.Mirror: krawędź jest odzwierciedlana od ostatniego do pierwszego koloru. TileMode Mirror
TileMode.Clamp: krawędź jest przycinana do koloru końcowego. Następnie wypełni pozostałą część regionu najbliższym kolorem. Zacisk w trybie kafelków
TileMode.Decal: renderuj 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 znajdować się na środku mniejszego wymiaru (szerokości lub wysokości):

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

Zestaw gradientów promieniowych bez zmian rozmiaru
Rysunek 4. Gradient promieniowy ustawiony bez zmian rozmiaru

Gdy zmienisz gradient promieniowy, aby ustawić rozmiar promienia na maksymalny wymiar, zobaczysz, że daje 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 na podstawie rozmiaru obszaru
Rysunek 5. Większy promień gradientu promieniowego w zależności od 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 różni się od ostatniego utworzenia Brush lub jeśli zmieni się obiekt stanu użyty do utworzenia shadera.

Poniższy kod tworzy shader 3 razy z różnymi rozmiarami, ponieważ rozmiar obszaru rysowania się zmienia:

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 jest stosowany w przypadku kilku różnych typów rysowania: tła, tekstu i obszaru rysowania. Wynik będzie następujący:

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. Spowoduje to efekt gradientu.

Następnie utwórz pędzel Shader Brush i ustaw zmienne uniform dla resolution – rozmiaru obszaru rysowania oraz colorcolor2, których chcesz użyć 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 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.

Dodatkowe materiały

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