Pędzel: gradienty i cieniowanie

A Brush w Compose opisuje sposób rysowania czegoś 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 są przydatne do rysowania, np. LinearGradient, RadialGradient lub zwykły SolidColor pędzel.

Pędzle można stosować za pomocą wywołań rysowania Modifier.background(), TextStyle lub DrawScope, aby zastosować styl malowania do rysowanej treści.

Na przykład pędzel z gradientem poziomym można zastosować do rysowania koła w 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
Ilustracja 1.: koło narysowane za pomocą gradientu poziomego

Pędzle z gradientem

Dostępnych jest wiele wbudowanych pędzli z gradientem, których można używać do uzyskiwania różnych efektów gradientu. Te pędzle umożliwiają określenie listy kolorów, z których chcesz utworzyć gradient.

Lista dostępnych pędzli z gradientem i odpowiadających im wyników:

Typ pędzla z gradientem 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 na kolor początkowy.
Gradient kołowy
Brush.radialGradient(colorList) Gradient promieniowy

Zmienianie rozkładu kolorów za pomocą colorStops

Aby dostosować sposób wyświetlania kolorów w gradiencie, możesz dostosować wartość colorStops dla każdego z nich. colorStops należy określić jako ułamek 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 zatrzymania kolorów tak, aby miał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 parze colorStop, czyli mniej żółtego niż czerwonego i niebieskiego.

Pędzel skonfigurowany z różnymi punktami kolorów
Ilustracja 2.: pędzel skonfigurowany z różnymi punktami zatrzymania kolorów

Powtarzanie wzoru za pomocą TileMode

Każdy pędzel z gradientem ma opcję ustawienia TileMode. Jeśli nie ustawisz początku i końca gradientu, możesz nie zauważyć TileMode, ponieważ domyślnie wypełni on cały obszar. TileMode będzie powtarzać 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 jest ustawiony na 50.dp, a rozmiar 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 tej tabeli znajdziesz szczegółowe informacje o tym, co robią poszczególne tryby powtarzania w przypadku powyższego przykładu HorizontalGradient:

TileMode Wyniki
TileMode.Repeated: krawędź jest powtarzana od ostatniego koloru do pierwszego. TileMode Repeated
TileMode.Mirror: krawędź jest odbijana od ostatniego koloru do pierwszego. TileMode Mirror
TileMode.Clamp: krawędź jest przycinana do koloru końcowego. Następnie będzie malować najbliższy kolor w pozostałej części regionu. 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 powtarzanie odbywa się w innym kierunku.

Zmienianie rozmiaru pędzla

Jeśli znasz rozmiar obszaru, w którym będzie rysowany pędzel, możesz ustawić endX kafelka, jak pokazano powyżej w sekcji TileMode. Jeśli jesteś w a DrawScope, możesz użyć jego size właściwości, aby uzyskać rozmiar obszaru.

Jeśli nie znasz rozmiaru obszaru rysowania (np. jeśli Brush jest przypisany do tekstu), 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 cieniowania podzielony przez 4
Ilustracja 3.: rozmiar shadera podzielony przez 4

Możesz też zmienić rozmiar pędzla dowolnego innego gradientu, np. gradientu radialnego. Jeśli nie określisz rozmiaru i środka, gradient zajmie całe granice DrawScope, a środek gradientu promieniowego domyślnie będzie znajdować się w środku granic DrawScope. W rezultacie środek gradientu radialnego 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))
            )
        )
)

Zestaw gradientu promieniowego bez zmian rozmiaru
Ilustracja 4.: gradient radialny 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 promienistego na podstawie rozmiaru obszaru
Ilustracja 5.: większy promień gradientu promieniowego na podstawie rozmiaru obszaru

Warto zauważyć, że rzeczywisty rozmiar przekazywany do tworzenia shadera jest określany na podstawie miejsca, w którym jest on wywoływany. Domyślnie Brush ponownie przydzieli swój Shader wewnętrznie, jeśli rozmiar różni się od ostatniego utworzenia Brush lub jeśli zmieni się obiekt stanu używany do tworzenia shadera.

Poniższy kod tworzy shader 3 razy z różnymi rozmiarami, ponieważ 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, 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 do kilku różnych typów rysowania: tła, tekstu i Canvas. Wynik jest następujący:

Pędzel ImageShader używany na różne sposoby
Ilustracja 6.: używanie pędzla ImageShader do rysowania tła, tekstu i koła

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

Zaawansowany przykład: pędzel niestandardowy

Pędzel AGSL RuntimeShader

AGSL oferuje podzbiór możliwości shadera GLSL. Shadery można pisać w AGSL i używać z pędzlem w Compose.

Aby utworzyć pędzel shader, najpierw zdefiniuj shader jako ciąg 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 2 kolorami na podstawie odległości. Daje to efekt gradientu.

Następnie utwórz pędzel shader i ustaw uniformy dla resolution – rozmiaru obszaru rysowania oraz color i color2, 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
Ilustracja 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ż są to obliczenia matematyczne. Więcej informacji o AGSL znajdziesz w dokumentacji AGSL.

Dodatkowe materiały

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