Brush: gradientes y sombreadores

Un Brush en Compose describe cómo se dibuja un elemento en la pantalla: determina los colores que se trazan en el área de dibujo (es decir, un círculo, un cuadrado, una ruta). Hay algunos modificadores Brush integrados que son útiles para dibujar, como LinearGradient, RadialGradient o un Brush SolidColor sin formato.

Los modificadores Brush se pueden usar con Modifier.background(), TextStyle o llamadas de dibujo DrawScope para aplicar un estilo de pintura al contenido que se dibuja.

Por ejemplo, se puede aplicar un pincel de gradiente horizontal para dibujar un círculo en DrawScope:

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

Círculo dibujado con gradiente horizontal
Figura 1: Círculo dibujado con gradiente horizontal

Pinceles de gradiente

Hay muchos modificadores Brush de gradiente integrados que se pueden usar para lograr diferentes efectos de gradiente. Estos permiten especificar la lista de colores a partir de la cual te gustaría crear un gradiente.

La siguiente es una lista de los modificadores Brush de gradiente disponibles y su resultado correspondiente:

Tipo de Brush de gradiente Resultado
Brush.horizontalGradient(colorList) Gradiente horizontal
Brush.linearGradient(colorList) Gradiente lineal
Brush.verticalGradient(colorList) Gradiente vertical
Brush.sweepGradient(colorList)
Nota: Para lograr una transición fluida entre colores, establece el último color como el color de inicio.
Gradiente de barrido
Brush.radialGradient(colorList) Gradiente radial

Cambia la distribución de los colores con colorStops

Para personalizar la forma en que aparecen los colores en el gradiente, puedes ajustar el valor de colorStops de cada uno. colorStops se debe especificar como una fracción, entre 0 y 1. Los valores superiores a 1 harán que esos colores no se procesen como parte del gradiente.

Puedes configurar elementos stop a los colores para que tengan diferentes cantidades, como menos o más de un color:

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

Los colores están dispersos en el desplazamiento proporcionado, según se define en el par colorStop, menos amarillo que rojo y azul.

Brush configurado con diferentes elementos stop de color
Figura 2: Brush configurado con diferentes elementos stop de color

Repite un patrón con TileMode

Cada Brush de gradiente tiene la opción de establecer un TileMode en él. Es posible que no notes el elemento TileMode si no estableciste un inicio y un final para la gradiente, ya que llenará toda el área de manera predeterminada. Un elemento TileMode solo dividirá el gradiente si el tamaño del área es mayor que el Brush.

En el siguiente código, se repetirá el patrón de gradiente 4 veces, ya que endX se establece en 50.dp y el tamaño se establece en 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
            )
        )
)

A continuación, se muestra una tabla en la que se detalla lo que hacen los diferentes modos de Tile para el ejemplo anterior de HorizontalGradient:

TileMode Salida
TileMode.Repeated: El borde se repite desde el último color hasta el primero. TileMode repetido
TileMode.Mirror: El borde se duplica desde el último color hasta el primero. Replicación de TileMode
TileMode.Clamp: El borde se fijó en el color final. Luego, se aplicará el color más cercano al resto de la región. Restricción de TileMode
TileMode.Decal: Renderiza solo hasta el tamaño de los límites. TileMode.Decal aprovecha el contenido transparente negro para hacer un muestreo del contenido fuera de los límites originales, mientras que TileMode.Clamp muestra el color del borde. Calcomanía de TileMode

TileMode funciona de manera similar para los otros gradientes direccionales. La diferencia es la dirección en la que se repite.

Cambiar el tamaño del pincel

Si conoces el tamaño del área en la que se dibujará tu modificador Brush, puedes configurar la tarjeta endX como vimos antes en la sección TileMode. Si estás en un DrawScope, puedes usar su propiedad size para obtener el tamaño del área.

Si no conoces el tamaño del área de dibujo (por ejemplo, si Brush está asignado a texto), puedes extender Shader y usar el tamaño del área de dibujo en la función createShader.

En este ejemplo, se divide el tamaño por 4 para repetir el patrón 4 veces:

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

Tamaño del sombreador dividido por 4
Figura 3: Tamaño del sombreador dividido por 4

También puedes cambiar el tamaño del modificador Brush de cualquier otro gradiente, como los radiales. Si no especificas un tamaño y un centro, el gradiente ocupará los límites completos del DrawScope, y el centro del gradiente radial se establecerá de forma predeterminada en el centro de los límites del DrawScope. Esto hace que el centro del gradiente radial aparezca como el centro de la dimensión más pequeña (ya sea de ancho o de altura):

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

Se estableció un gradiente radial sin cambios de tamaño
Figura 4: Conjunto de gradientes radial sin cambios de tamaño

Cuando se modifica el gradiente radial para establecer el tamaño del radio en la dimensión máxima, puedes ver que produce un mejor efecto de gradiente radial:

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

Radio más grande en gradiente radial, según el tamaño de área
Figura 5: Radio más grande en gradiente radial, según el tamaño de área

Vale la pena señalar que el tamaño real que se pasa a la creación del sombreador se determina desde el lugar en el que se invoca. De forma predeterminada, Brush reasignará su Shader internamente si el tamaño es diferente de la creación más reciente del Brush, o bien si cambió algún objeto de estado en la creación del sombreador.

En el siguiente código, se crea el sombreador tres veces diferentes con tamaños diferentes, ya que el tamaño del área de dibujo cambia:

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 una imagen como pincel

Para usar un objeto ImageBitmap como Brush, carga la imagen como ImageBitmap y crea un modificador Brush 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))

Brush se aplica a diferentes tipos de dibujos: un fondo, el texto y el lienzo. Esto da como resultado lo siguiente:

Diferentes usos de ImageShader Brush
Figura 6: Uso del modificador Brush ImageShader para dibujar un fondo, texto y un círculo

Ten en cuenta que ahora el texto también se procesa con el objeto ImageBitmap para pintar los píxeles del texto.

Ejemplo avanzado: Pincel personalizado

Pincel AGSL RuntimeShader

AGSL ofrece un subconjunto de capacidades de sombreador GLSL. Los sombreadores se pueden escribir en AGSL y usar con un modificador Brush en Compose.

Para crear un Brush de sombreador, primero define la string de sombreador de 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()

El sombreador anterior toma dos colores de entrada, calcula la distancia desde la parte inferior izquierda (vec2(0, 1)) del área de dibujo y realiza un mix entre los dos colores según la distancia. Esto produce un efecto de gradiente.

Luego, crea el Brush de sombreador y establece los uniformes de resolution: el tamaño del área de dibujo, y el color y el color2 que deseas usar como entrada para tu gradiente:

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

Cuando lo ejecutes, verás lo siguiente en la pantalla:

Sombreador de AGSL personalizado que se ejecuta en Compose
Figura 7: Sombreador de AGSL personalizado que se ejecuta en Compose

Ten en cuenta que puedes hacer mucho más con sombreadores que con gradientes, ya que se trata de cálculos matemáticos. Para obtener más información sobre AGSL, consulta la documentación de AGSL.

Recursos adicionales

Para obtener más ejemplos de cómo usar Brush en Compose, consulta los siguientes recursos: