Pincel: gradientes e sombreadores

Um Brush (pincel) no Compose descreve como algo é desenhado na tela. Ele determina as cores que estão na área de desenho (ou seja, um círculo, quadrado ou caminho). Alguns pincéis integrados são úteis para desenhar, como LinearGradient, RadialGradient ou um pincel SolidColor simples.

Os pincéis podem ser usados com chamadas de desenho Modifier.background(), TextStyle ou DrawScope para aplicar o estilo de pintura ao conteúdo que está sendo desenhado.

Por exemplo, um pincel de gradiente horizontal pode ser aplicado para desenhar um círculo em DrawScope:

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

Círculo desenhado com gradiente horizontal
Figura 1. Círculo desenhado com gradiente horizontal.

Pincéis de gradiente

Vários pincéis de gradiente integrados podem ser usados para alcançar diferentes efeitos de gradiente. Eles pincéis permitem especificar a lista de cores com a qual você quer criar um gradiente.

A seguir, incluímos uma lista dos pincéis de gradiente disponíveis e a saída correspondente:

Tipo de pincel de gradiente Saída
Brush.horizontalGradient(colorList) Gradiente horizontal
Brush.linearGradient(colorList) Gradiente linear
Brush.verticalGradient(colorList) Gradiente vertical
Brush.sweepGradient(colorList)
Observação: para fazer uma transição suave entre cores, defina a última cor como a inicial.
Gradiente em varredura
Brush.radialGradient(colorList) Gradiente radial

Mudar a distribuição de cores com colorStops

Para personalizar a forma como as cores aparecem no gradiente, ajuste o valor colorStops de cada uma. Ele precisa ser especificado como uma fração, entre 0 e 1. Valores maiores que 1 fazem com que essas cores não sejam renderizadas como parte do gradiente.

Você pode configurar as paradas de cor para terem quantidades diferentes (por exemplo, menos ou mais de uma cor):

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))
)

As cores são dispersadas na compensação fornecida conforme definido no par de colorStop, com menos amarelo do que vermelho e azul.

Pincel configurado com diferentes paradas de cor
Figura 2. Pincel configurado com diferentes paradas de cor.

Repetir um padrão com TileMode

Cada pincel de gradiente tem a opção de definir um TileMode. Se você não definiu um início e um fim para o gradiente, talvez não perceba o TileMode, já que o padrão é preencher toda a área. Um TileMode só vai agrupar o gradiente se o tamanho da área for maior que o do pincel.

O código a seguir repetirá o padrão do gradiente quatro vezes, já que endX está definido como 50.dp e o tamanho está definido como 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
            )
        )
)

Confira uma tabela que detalha o que os diferentes modos de bloco fazem no exemplo de HorizontalGradient acima:

TileMode Saída
TileMode.Repeated: a borda é repetida da última cor para a primeira. TileMode repetido
TileMode.Mirror: a borda é espelhada da última cor para a primeira. Espelhamento de TileMode
TileMode.Clamp: a borda é limitada à cor final. Então, a cor mais próxima é usada para o restante da região. Limitação do TileMode
TileMode.Decal: renderiza apenas até os limites. O TileMode.Decal usa a cor preta transparente para aproveitar o conteúdo fora dos limites originais, enquanto o TileMode.Clamp utiliza a cor das bordas Decalque do TileMode

O TileMode funciona de maneira semelhante para os outros gradientes direcionais. A diferença está na direção em que a repetição ocorre.

Mudar o tamanho do pincel

Se você sabe o tamanho da área em que o pincel vai ser desenhado, defina o bloco endX como vimos acima na seção TileMode. Se você está em um DrawScope, pode usar a propriedade size para saber o tamanho da área.

Caso não saiba o tamanho da área de desenho (por exemplo, se o Brush estiver atribuído a "Text"), estenda o Shader e use o tamanho da área na função createShader.

Neste exemplo, divida o tamanho por quatro para repetir o padrão quatro vezes:

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)
)

Tamanho do sombreador dividido por quatro
Figura 3. Tamanho do sombreador dividido por quatro.

Também é possível mudar o tamanho do pincel de qualquer outro gradiente, como gradientes radiais. Se você não especificar um tamanho e um centro, o gradiente ocupará os limites completos do DrawScope, e o centro do gradiente radial vai assumir como padrão o centro dos limites do DrawScope. O resultado é o centro do gradiente radial aparecendo como o centro da dimensão menor (largura ou altura):

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

Gradiente radial definido sem mudanças de tamanho
Figura 4. Gradiente radial definido sem mudanças de tamanho.

Quando o gradiente radial é mudado para usar a dimensão máxima como o tamanho do raio, é possível notar que um efeito melhor é produzido:

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)
)

Raio maior no gradiente radial com base no tamanho da área
Figura 5. Raio maior no gradiente radial com base no tamanho da área.

É importante observar que o tamanho real transmitido para a criação do sombreador depende de onde ele é invocado. Por padrão, o Brush realoca o Shader internamente quando o tamanho é diferente da última criação do Brush ou se um objeto de estado usado na criação do sombreador mudou.

O código abaixo cria o sombreador três vezes com tamanhos diferentes, conforme o tamanho da área de desenho muda:

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)
                }
            }
        }
)

Usar uma imagem como pincel

Para usar uma ImageBitmap como um Brush, carregue a imagem como uma ImageBitmap e crie um pincel 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))

O pincel é usado em alguns tipos diferentes de desenho: fundo, texto e tela. Isso resulta no seguinte:

Pincel ImageShader usado de maneiras diferentes
Figura 6. Uso do pincel ImageShader para desenhar um fundo, um texto e um círculo.

O texto agora também é renderizado usando a ImageBitmap para pintar os pixels dele.

Exemplo avançado: pincel personalizado

Pincel RuntimeShader AGSL

A AGSL oferece um subconjunto de recursos sombreadores da GLSL. Os sombreadores podem ser programados em AGSL e usados com um pincel no Compose.

Para criar esse pincel, primeiro defina o sombreador como uma string de sombreador da 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()

O sombreador acima usa duas cores de entrada, calcula a distância em relação ao canto inferior esquerdo (vec2(0, 1)) da área de desenho e faz um mix entre as duas cores com base na distância. Isso produz um efeito de gradiente.

Em seguida, crie o pincel sombreador e defina os uniformes para a resolution, que é o tamanho da área de desenho, e a color e color2 que você quer usar como entrada para o gradiente personalizado:

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)
    )
}

Com isso, a tela mostra o seguinte:

Sombreador personalizado da AGSL em execução no Compose
Figura 7. Sombreador personalizado da AGSL em execução no Compose.

Vale ressaltar que é possível fazer muito mais com sombreadores do que apenas gradientes, porque eles são cálculos matemáticos. Para mais informações, consulte a documentação da AGSL.

Outros recursos

Para mais exemplos de como usar o pincel no Compose, consulte estes recursos: