Pinceau : dégradés et nuanceurs

Dans Compose, un élément Brush décrit la façon dont un objet est dessiné à l'écran. Il détermine la ou les couleurs qui remplissent la zone de dessin (cercle, carré, ligne droite, etc.). Il existe quelques pinceaux intégrés utiles pour le dessin, tels que LinearGradient, RadialGradient ou un pinceau SolidColor pour les couleurs unies.

Vous pouvez utiliser les pinceaux avec les appels de dessin Modifier.background(), TextStyle ou DrawScope pour appliquer le style de peinture au contenu dessiné.

Par exemple, vous pouvez appliquer un pinceau de dégradé horizontal au dessin d'un cercle dans DrawScope :

val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
Canvas(
    modifier = Modifier.size(200.dp),
    onDraw = {
        drawCircle(brush)
    }
)
Cercle dessiné avec un dégradé horizontal
Figure 1 : Cercle dessiné avec un dégradé horizontal

Pinceaux à dégradé

Divers pinceaux intégrés permettent d'obtenir différents effets de dégradé. Ces pinceaux vous permettent de spécifier la liste des couleurs à partir desquelles vous souhaitez créer le dégradé.

Liste des pinceaux à dégradé disponibles et résultat correspondant :

Type de pinceau à dégradé Résultat
Brush.horizontalGradient(colorList) Dégradé horizontal
Brush.linearGradient(colorList) Dégradé linéaire
Brush.verticalGradient(colorList) Dégradé vertical
Brush.sweepGradient(colorList)
Remarque : Pour une transition en douceur entre les couleurs, la dernière couleur doit correspondre à la couleur de départ.
Dégradé de balayage
Brush.radialGradient(colorList) Dégradé radial

Modifier la distribution des couleurs avec colorStops

Pour personnaliser la façon dont les couleurs apparaissent dans le dégradé, vous pouvez ajuster la valeur colorStops pour chacune d'elles. colorStops doit être une fraction comprise entre 0 et 1. Si cette valeur est supérieure à 1, les couleurs ne s'afficheront pas dans le dégradé.

Vous pouvez configurer les arrêts de couleur pour définir des quantités différentes, par exemple pour utiliser davantage une couleur spécifique, ou dans une moindre mesure :

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

Les couleurs sont dispersées au niveau du décalage fourni comme spécifié dans la paire colorStop (moins de jaune que de rouge et de bleu).

Pinceau configuré avec différents arrêts de couleur
Figure 2 : Pinceau configuré avec différents arrêts de couleur

Répéter un schéma avec TileMode

Chaque pinceau de dégradé peut définir un TileMode. Vous ne remarquerez peut-être pas TileMode si vous n'avez pas défini de début et de fin pour le dégradé, car il remplira par défaut toute la surface. TileMode ne met en mosaïque le dégradé que si la zone est plus grande que la taille du pinceau.

Le code suivant répète le modèle de gradient quatre fois, car endX est défini sur 50.dp et la taille sur 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
            )
        )
)

Voici un tableau détaillant le fonctionnement des différents modes mosaïque pour l'exemple HorizontalGradient ci-dessus :

Mode mosaïque Sortie
TileMode.Repeated : la bordure est mise en miroir de la dernière couleur à la première. Mode mosaïque répété
TileMode.Mirror : la bordure est mise en miroir de la dernière couleur à la première. Mode mosaïque mise en miroir
TileMode.Clamp : la bordure est fixée à la couleur finale. La couleur la plus proche est ensuite affichée pour le reste de la région. Mode mosaïque pince
TileMode.Decal : effectue le rendu uniquement jusqu'à la taille des limites. TileMode.Decal exploite le noir transparent pour échantillonner le contenu en dehors des limites d'origine, tandis que TileMode.Clamp échantillonne la couleur des bordures. Mode mosaïque décalcomanie

TileMode fonctionne de manière similaire pour les autres dégradés directionnels, la différence étant la direction de la répétition.

Changer la taille du pinceau

Si vous connaissez la taille de la zone dans laquelle le traçage du pinceau aura lieu, vous pouvez définir la mosaïque endX comme indiqué ci-dessus dans la section TileMode. Si vous êtes dans un élément DrawScope, vous pouvez utiliser sa propriété size pour obtenir la taille de la zone.

Si vous ne connaissez pas la taille de votre zone de dessin (par exemple, si Brush est affecté au texte), vous pouvez étendre Shader et utiliser la taille de la zone de dessin dans createShader.

Dans cet exemple, divisez la taille par 4 pour répéter le modèle quatre fois :

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

Taille du nuanceur divisée par 4
Figure 3 : Taille du nuanceur divisée par 4

Vous pouvez également modifier la taille de pinceau de n'importe quel autre dégradé, tel que le dégradé radial. Si vous ne spécifiez pas de taille ni de centre, le dégradé occupera les limites complètes de DrawScope, et le centre du dégradé radial correspondra par défaut au centre des limites DrawScope. Le centre du dégradé radial apparaîtra alors au centre de la dimension la plus petite (largeur ou hauteur) :

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

Dégradé radial défini sans changement de taille
Figure 4 : Dégradé radial sans changement de taille

Lorsque le dégradé radial est modifié pour définir la taille du rayon sur la dimension maximale, vous pouvez constater qu'il produit un meilleur effet de dégradé 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)
)

Rayon plus grand sur le dégradé radial, en fonction de la taille de la zone
Figure 5 : Rayon plus grand sur le dégradé radial, en fonction de la taille de la zone

Notez que la taille réelle transmise pour créer le nuanceur est déterminée à partir de l'endroit où elle est appelée. Par défaut, Brush réaffecte son Shader en interne si la taille est différente de celle de la dernière création de l'élément Brush, ou si un objet d'état utilisé lors de la création du nuanceur a changé.

Le code suivant crée le nuanceur trois fois avec des tailles différentes, à mesure que la taille de la zone de dessin change :

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

Utiliser une image en tant que pinceau

Pour utiliser un ImageBitmap en tant que Brush, chargez l'image en tant qu'ImageBitmap et créez un pinceau 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))

Le pinceau est appliqué à différents types de dessins : arrière-plan, texte et canevas. Vous obtenez le résultat suivant :

Pinceau ImageShader utilisé de différentes manières
Figure 6 : Utilisation du pinceau ImageShader pour dessiner un arrière-plan, du texte et un cercle

Notez que le texte est désormais également affiché à l'aide d'ImageBitmap afin de peindre les pixels du texte.

Exemple avancé : pinceau personnalisé

Pinceau AGSL RuntimeShader

AGSL offre un sous-ensemble des fonctionnalités du nuanceur GLSL. Les nuanceurs peuvent être écrits en langage AGSL et utilisés avec un pinceau dans Compose.

Pour créer un pinceau de nuanceur, définissez-le d'abord comme chaîne de nuanceur 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()

Le nuanceur ci-dessus utilise deux couleurs d'entrée, calcule la distance à partir de l'angle inférieur gauche (vec2(0, 1)) de la zone de dessin et effectue un mix entre les deux couleurs en fonction de la distance. Cela produit un effet de dégradé.

Créez ensuite le pinceau du nuanceur et définissez les uniformes pour la resolution (la taille de la zone de dessin, ainsi que les éléments color et color2 que vous souhaitez utiliser comme entrées pour votre dégradé) :

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

En exécutant cette commande, vous pouvez voir ce qui suit à l'écran :

Nuanceur AGSL personnalisé exécuté dans Compose
Figure 7 : Nuanceur AGSL personnalisé exécuté dans Compose

Il est intéressant de noter que vous pouvez réaliser bien plus de tâches avec les nuanceurs qu'avec les dégradés, car ils reposent sur des calculs mathématiques. Pour en savoir plus sur AGSL, consultez cette documentation.

Autres ressources

Pour voir d'autres exemples d'utilisation du pinceau dans Compose, consultez les ressources suivantes :