Modificateurs graphiques

En plus du composable Canvas, Compose dispose de plusieurs éléments graphiques Modifiers utiles qui permettent de dessiner des contenus personnalisés. Ces modificateurs sont utiles, car ils peuvent être appliqués à n'importe quel composable.

Modificateurs de dessin

Toutes les commandes de dessin sont effectuées à l'aide d'un modificateur de dessin dans Compose. Compose comprend trois principaux modificateurs de dessin :

Le modificateur de base du dessin est drawWithContent. Il permet de déterminer l'ordre de traçage de votre composable et des commandes de dessin émises dans le modificateur. drawBehind est un wrapper pratique pour drawWithContent. L'ordre de traçage est défini derrière le contenu du composable. drawWithCache appelle onDrawBehind ou onDrawWithContent à l'intérieur et fournit un mécanisme permettant de mettre en cache les objets qu'ils contiennent.

Modifier.drawWithContent: choisir l'ordre de traçage

Modifier.drawWithContent vous permet d'exécuter des opérations DrawScope avant ou après le contenu du composable. Veillez à appeler drawContent pour afficher le contenu réel du composable. Ce modificateur vous permet de déterminer l'ordre des opérations si vous souhaitez que votre contenu soit dessiné avant ou après vos opérations de dessin personnalisées.

Par exemple, si vous souhaitez afficher un dégradé radial sur votre contenu afin de créer un effet de lampe de poche, vous pouvez procéder comme suit :

var pointerOffset by remember {
    mutableStateOf(Offset(0f, 0f))
}
Column(
    modifier = Modifier
        .fillMaxSize()
        .pointerInput("dragging") {
            detectDragGestures { change, dragAmount ->
                pointerOffset += dragAmount
            }
        }
        .onSizeChanged {
            pointerOffset = Offset(it.width / 2f, it.height / 2f)
        }
        .drawWithContent {
            drawContent()
            // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI.
            drawRect(
                Brush.radialGradient(
                    listOf(Color.Transparent, Color.Black),
                    center = pointerOffset,
                    radius = 100.dp.toPx(),
                )
            )
        }
) {
    // Your composables here
}

Figure 1 : Modifier.drawWithContent utilisé sur un composable pour créer une expérience utilisateur de type lampe de poche

Modifier.drawBehind: dessiner derrière un composable

Modifier.drawBehind vous permet d'effectuer des opérations DrawScope derrière le contenu du composable qui s'affiche à l'écran. Si vous examinez l'implémentation de Canvas, vous remarquerez peut-être qu'il s'agit simplement d'un wrapper pratique pour Modifier.drawBehind.

Pour dessiner un rectangle arrondi derrière Text, procédez comme suit :

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawBehind {
            drawRoundRect(
                Color(0xFFBBAAEE),
                cornerRadius = CornerRadius(10.dp.toPx())
            )
        }
        .padding(4.dp)
)

Résultat :

Texte et arrière-plan dessinés à l'aide de Modifier.drawBehind
Figure 2 : Texte et arrière-plan dessiné à l'aide de Modifier.drawBehind

Modifier.drawWithCache: dessiner et mettre en cache des objets de dessin

Modifier.drawWithCache conserve les objets qui y sont créés dans le cache. Les objets sont mis en cache tant que la taille de la zone de dessin est identique ou que les objets d'état lus ne sont pas modifiés. Ce modificateur est utile pour améliorer les performances des appels de dessin, car il évite de devoir réaffecter des objets (tels que Brush, Shader, Path) créés lors du dessin.

Vous pouvez également mettre en cache des objets à l'aide de remember, en dehors du modificateur. Toutefois, cela n'est pas toujours possible, car vous n'avez pas toujours accès à cette composition. Il peut être plus efficace d'utiliser drawWithCache si les objets ne sont utilisés que pour le dessin.

Par exemple, si vous créez un objet Brush pour dessiner un dégradé derrière un élément Text, l'utilisation de drawWithCache met en cache l'objet Brush tant que la taille de la zone de dessin ne change pas :

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawWithCache {
            val brush = Brush.linearGradient(
                listOf(
                    Color(0xFF9E82F0),
                    Color(0xFF42A5F5)
                )
            )
            onDrawBehind {
                drawRoundRect(
                    brush,
                    cornerRadius = CornerRadius(10.dp.toPx())
                )
            }
        }
)

Mise en cache de l'objet Brush avec drawWithCache
Figure 3 : Mise en cache l'objet Brush avec drawWithCache

Modificateurs graphiques

Modifier.graphicsLayer: appliquer des transformations aux composables

Modifier.graphicsLayer est un modificateur qui transforme le contenu du dessin composable en un calque de dessin. Un calque fournit plusieurs fonctions différentes, par exemple :

  • Isolement des instructions de dessin (semblable à RenderNode). Les instructions de dessin capturées dans un calque peuvent être réémises efficacement par le pipeline de rendu sans avoir à réexécuter le code de l'application.
  • Transformations qui s'appliquent à toutes les instructions de dessin contenues dans un calque.
  • Rastérisation pour les fonctionnalités de composition. Lorsqu'un calque est rastérisé, ses instructions de dessin sont exécutées, et la sortie est capturée dans un tampon hors écran. La composition d'un tampon de ce type pour les frames suivants est plus rapide que l'exécution d'instructions individuelles. Toutefois, le comportement s'apparente à un bitmap lors de l'application de transformations telles que la mise à l'échelle ou la rotation.

Transformations

Modifier.graphicsLayer assure l'isolement pour les instructions de dessin. Par exemple, diverses transformations peuvent être appliquées à l'aide de Modifier.graphicsLayer. Elles peuvent être animées ou modifiées sans qu'il soit nécessaire d'exécuter à nouveau le dessin lambda.

Modifier.graphicsLayer ne modifie pas la taille ni l'emplacement mesurés du composable, car il affecte uniquement la phase de dessin. Cela signifie que votre composable peut en chevaucher d'autres s'il finit par dessiner des objets en dehors de ses limites de mise en page.

Les transformations suivantes peuvent être appliquées avec ce modificateur :

Mise à l'échelle : augmentation de la taille

scaleX et scaleY agrandissent ou réduisent le contenu dans le sens horizontal ou vertical, respectivement. La valeur 1.0f indique qu'il n'y a pas de changement d'échelle, tandis que la valeur 0.5f spécifie la moitié de la dimension.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.scaleX = 1.2f
            this.scaleY = 0.8f
        }
)

Figure 4 : scaleX et scaleY appliqués à un composable Image
Translation

translationX et translationY peuvent être modifiés avec graphicsLayer. translationX déplace le composable vers la gauche ou la droite. translationY déplace le composable vers le haut ou vers le bas.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.translationX = 100.dp.toPx()
            this.translationY = 10.dp.toPx()
        }
)

Figure 5 : translationX et translationY appliqués à l'image avec Modifier.graphicsLayer
Rotation

Définissez rotationX pour une rotation horizontale, rotationY pour une rotation verticale et rotationZ pour une rotation sur l'axe Z (rotation standard). Cette valeur est spécifiée en degrés (0-360).

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.rotationX = 90f
            this.rotationY = 275f
            this.rotationZ = 180f
        }
)

Figure 6 : rotationX, rotationY et rotationZ définis sur l'image par Modifier.graphicsLayer
Origine

Vous pouvez spécifier un élément transformOrigin. Il sera ainsi utilisé comme point de départ des transformations. Tous les exemples jusqu'à présent utilisaient TransformOrigin.Center, qui est défini sur (0.5f, 0.5f). Si vous spécifiez l'origine dans (0f, 0f), les transformations commencent à partir de l'angle supérieur gauche du composable.

Si vous modifiez l'origine avec une transformation rotationZ, vous constaterez que l'élément pivote en haut à gauche du composable :

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.transformOrigin = TransformOrigin(0f, 0f)
            this.rotationX = 90f
            this.rotationY = 275f
            this.rotationZ = 180f
        }
)

Figure 7 : Rotation appliquée avec TransformOrigin défini sur 0f, 0f

Rognage et forme

La forme spécifie le contour que le contenu doit rogner lorsque clip = true. Dans cet exemple, nous avons défini deux zones pour avoir deux rognages différents : un avec la variable de rognage graphicsLayer et l'autre avec le wrapper pratique Modifier.clip.

Column(modifier = Modifier.padding(16.dp)) {
    Box(
        modifier = Modifier
            .size(200.dp)
            .graphicsLayer {
                clip = true
                shape = CircleShape
            }
            .background(Color(0xFFF06292))
    ) {
        Text(
            "Hello Compose",
            style = TextStyle(color = Color.Black, fontSize = 46.sp),
            modifier = Modifier.align(Alignment.Center)
        )
    }
    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(CircleShape)
            .background(Color(0xFF4DB6AC))
    )
}

Le contenu de la première zone (texte "Hello Compose") est tronqué par rapport à la forme circulaire :

Rognage appliqué au composable Box
Figure 8 : Rognage appliqué au composable Box

Si vous appliquez ensuite un élément translationY au cercle rose supérieur, vous constaterez que les limites du composable restent identiques, mais un cercle est dessiné en dessous (et en dehors des limites).

Rognage appliqué avec une translation Y et une bordure rouge pour le contour
Figure 9 : Rognage appliqué avec la translation Y et une bordure rouge pour le contour

Pour rogner le composable dans la région dans laquelle il est dessiné, vous pouvez ajouter un autre Modifier.clip(RectangleShape) au début de la chaîne de modificateur. Le contenu restera alors dans les limites d'origine.

Column(modifier = Modifier.padding(16.dp)) {
    Box(
        modifier = Modifier
            .clip(RectangleShape)
            .size(200.dp)
            .border(2.dp, Color.Black)
            .graphicsLayer {
                clip = true
                shape = CircleShape
                translationY = 50.dp.toPx()
            }
            .background(Color(0xFFF06292))
    ) {
        Text(
            "Hello Compose",
            style = TextStyle(color = Color.Black, fontSize = 46.sp),
            modifier = Modifier.align(Alignment.Center)
        )
    }

    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(RoundedCornerShape(500.dp))
            .background(Color(0xFF4DB6AC))
    )
}

Rognage appliqué à la transformation graphicsLayer
Figure 10 : Rognage appliqué à la transformation graphicsLayer

Alpha

Modifier.graphicsLayer permet de définir un élément alpha (opacité) pour l'ensemble du calque. 1.0f est complètement opaque, et 0.0f est invisible.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "clock",
    modifier = Modifier
        .graphicsLayer {
            this.alpha = 0.5f
        }
)

Image avec la version alpha appliquée
Figure 11 : Image avec la version alpha appliquée

Stratégie de composition

L'utilisation de la valeur alpha et de la transparence ne se limite pas toujours à modifier une seule valeur alpha. En plus de modifier une valeur alpha, il est possible de définir une CompositingStrategy sur un graphicsLayer. Une CompositingStrategy détermine la manière dont le contenu du composable est composé (constitué) avec l'autre contenu déjà dessiné à l'écran.

Voici les différentes stratégies possibles :

Automatique (par défaut)

La stratégie de composition est déterminée par le reste des paramètres graphicsLayer. Elle affiche le calque dans un tampon hors écran si la valeur alpha est inférieure à 1.0f ou si un RenderEffect est défini. Chaque fois que la valeur alpha est inférieure à 1f, un calque de composition est créé automatiquement pour afficher le contenu, puis dessine ce tampon hors écran vers la destination avec la valeur alpha correspondante. Si vous définissez un RenderEffect ou un défilement supérieur, le contenu est toujours affiché dans un tampon hors écran, quel que soit la CompositingStrategy spécifiée.

Hors écran

Le contenu du composable est toujours rastérisé sur un bitmap ou une texture hors écran avant d'être affiché sur la destination. Cela est utile pour appliquer des opérations BlendMode au masquage du contenu et pour améliorer les performances lors de l'affichage d'ensembles d'instructions de dessin complexes.

BlendModes est un bon exemple d'utilisation de CompositingStrategy.Offscreen. Prenons l'exemple ci-dessous et supposons que vous souhaitiez supprimer certaines parties d'un composable Image en exécutant une commande de dessin utilisant BlendMode.Clear. Si vous ne définissez pas la compositingStrategy sur CompositingStrategy.Offscreen, BlendMode interagira avec tout le contenu sous-jacent.

Image(
    painter = painterResource(id = R.drawable.dog),
    contentDescription = "Dog",
    contentScale = ContentScale.Crop,
    modifier = Modifier
        .size(120.dp)
        .aspectRatio(1f)
        .background(
            Brush.linearGradient(
                listOf(
                    Color(0xFFC5E1A5),
                    Color(0xFF80DEEA)
                )
            )
        )
        .padding(8.dp)
        .graphicsLayer {
            compositingStrategy = CompositingStrategy.Offscreen
        }
        .drawWithCache {
            val path = Path()
            path.addOval(
                Rect(
                    topLeft = Offset.Zero,
                    bottomRight = Offset(size.width, size.height)
                )
            )
            onDrawWithContent {
                clipPath(path) {
                    // this draws the actual image - if you don't call drawContent, it wont
                    // render anything
                    this@onDrawWithContent.drawContent()
                }
                val dotSize = size.width / 8f
                // Clip a white border for the content
                drawCircle(
                    Color.Black,
                    radius = dotSize,
                    center = Offset(
                        x = size.width - dotSize,
                        y = size.height - dotSize
                    ),
                    blendMode = BlendMode.Clear
                )
                // draw the red circle indication
                drawCircle(
                    Color(0xFFEF5350), radius = dotSize * 0.8f,
                    center = Offset(
                        x = size.width - dotSize,
                        y = size.height - dotSize
                    )
                )
            }
        }
)

En définissant CompositingStrategy sur Offscreen, vous créez une texture hors écran sur laquelle les commandes peuvent être exécutées (et vous n'appliquez ainsi BlendMode qu'au contenu de ce composable). Le rendu se superpose alors au contenu déjà affiché à l'écran, sans affecter le contenu déjà dessiné.

Modifier.drawWithContent sur une image affichant une indication de cercle, avec BlendMode.Clear dans l'application
Figure 12 : Modifier.drawWithContent sur une image affichant une indication de cercle, avec BlendMode.Clear et CompositingStrategy.Offscreen dans l'application

Si vous n'utilisez pas CompositingStrategy.Offscreen, l'application de BlendMode.Clear efface tous les pixels dans la destination, quels que soient les éléments déjà définis. Le tampon de rendu (noir) de la fenêtre est alors visible. La plupart des BlendModes impliquant une valeur alpha ne fonctionnent pas comme prévu sans tampon hors écran. Notez l'anneau noir entourant l'indicateur de cercle rouge :

Modifier.drawWithContent sur une image montrant une indication de cercle, avec BlendMode.Clear et sans aucune définition de stratégie de composition
Figure 13 : Modifier.drawWithContent sur une image montrant une indication de cercle, avec BlendMode.Clear et sans aucune définition de stratégie de composition

Pour être un peu plus clair, si l'application comportait un arrière-plan translucide et que vous n'utilisiez pas CompositingStrategy.Offscreen, le BlendMode interagirait avec l'ensemble de l'application. Cela effacerait tous les pixels et afficherait l'application ou le fond d'écran en dessous, comme dans cet exemple :

Aucune stratégie de composition définie et utilisation de BlendMode.Clear avec une application dont l'arrière-plan est translucide. Le fond d'écran rose est visible dans la zone entourant le cercle rouge.
Figure 14 : Aucune stratégie de composition définie et utilisation de BlendMode.Clear avec une application dont l'arrière-plan est translucide (notez que le fond d'écran rose est visible dans la zone entourant le cercle rouge)

Notez que lorsque vous utilisez CompositingStrategy.Offscreen, une texture hors écran correspondant à la taille de la zone de dessin est créée et affichée à l'écran. Par défaut, toutes les commandes de dessin exécutées avec cette stratégie sont limitées à cette région. L'extrait de code ci-dessous illustre les différences lorsque vous passez à l'utilisation de textures hors écran :

@Composable
fun CompositingStrategyExamples() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .wrapContentSize(Alignment.Center)
    ) {
        // Does not clip content even with a graphics layer usage here. By default, graphicsLayer
        // does not allocate + rasterize content into a separate layer but instead is used
        // for isolation. That is draw invalidations made outside of this graphicsLayer will not
        // re-record the drawing instructions in this composable as they have not changed
        Canvas(
            modifier = Modifier
                .graphicsLayer()
                .size(100.dp) // Note size of 100 dp here
                .border(2.dp, color = Color.Blue)
        ) {
            // ... and drawing a size of 200 dp here outside the bounds
            drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx()))
        }

        Spacer(modifier = Modifier.size(300.dp))

        /* Clips content as alpha usage here creates an offscreen buffer to rasterize content
        into first then draws to the original destination */
        Canvas(
            modifier = Modifier
                // force to an offscreen buffer
                .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
                .size(100.dp) // Note size of 100 dp here
                .border(2.dp, color = Color.Blue)
        ) {
            /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the
            content gets clipped */
            drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx()))
        }
    }
}

CompositingStrategy.Auto ou CompositingStrategy.Offscreen : le mode hors écran affiche les rognages hors de la région, contrairement au mode automatique
Figure 15 : CompositingStrategy.Auto ou CompositingStrategy.Offscreen : le mode hors écran affiche les rognages hors de la région, contrairement au mode automatique
ModulateAlpha

Cette stratégie de composition module la valeur alpha pour chacune des instructions de dessin enregistrées dans graphicsLayer. Elle ne crée pas de tampon hors écran pour les valeurs alpha inférieures à 1.0f, sauf si un RenderEffect est défini. Elle peut donc être plus efficace pour un rendu alpha. Cependant, elle peut fournir des résultats différents en cas de chevauchement de contenu. Dans les cas où il est certain que le contenu ne se chevauchera pas, cette option peut offrir de meilleures performances que CompositingStrategy.Auto avec les valeurs alpha inférieures à 1.

Voici ci-dessous un autre exemple de différentes stratégies de composition : application de différentes valeurs alpha à différentes parties des composables et application d'une stratégie Modulate :

@Preview
@Composable
fun CompositingStrategy_ModulateAlpha() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp)
    ) {
        // Base drawing, no alpha applied
        Canvas(
            modifier = Modifier.size(200.dp)
        ) {
            drawSquares()
        }

        Spacer(modifier = Modifier.size(36.dp))

        // Alpha 0.5f applied to whole composable
        Canvas(
            modifier = Modifier
                .size(200.dp)
                .graphicsLayer {
                    alpha = 0.5f
                }
        ) {
            drawSquares()
        }
        Spacer(modifier = Modifier.size(36.dp))

        // 0.75f alpha applied to each draw call when using ModulateAlpha
        Canvas(
            modifier = Modifier
                .size(200.dp)
                .graphicsLayer {
                    compositingStrategy = CompositingStrategy.ModulateAlpha
                    alpha = 0.75f
                }
        ) {
            drawSquares()
        }
    }
}

private fun DrawScope.drawSquares() {

    val size = Size(100.dp.toPx(), 100.dp.toPx())
    drawRect(color = Red, size = size)
    drawRect(
        color = Purple, size = size,
        topLeft = Offset(size.width / 4f, size.height / 4f)
    )
    drawRect(
        color = Yellow, size = size,
        topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f)
    )
}

val Purple = Color(0xFF7E57C2)
val Yellow = Color(0xFFFFCA28)
val Red = Color(0xFFEF5350)

ModulateAlpha applique l'ensemble alpha à chaque commande de dessin
Figure 16 : ModulateAlpha applique l'ensemble alpha à chaque commande de dessin

Écrire le contenu d'un composable dans un bitmap

Un cas d'utilisation courant consiste à créer un Bitmap à partir d'un composable. Pour copier le contenu de votre composable dans un Bitmap, créez un GraphicsLayer à l'aide de rememberGraphicsLayer().

Redirigez les commandes de dessin vers le nouveau calque à l'aide de drawWithContent() et graphicsLayer.record{}. Dessinez ensuite le calque dans le canevas visible à l'aide de drawLayer:

val coroutineScope = rememberCoroutineScope()
val graphicsLayer = rememberGraphicsLayer()
Box(
    modifier = Modifier
        .drawWithContent {
            // call record to capture the content in the graphics layer
            graphicsLayer.record {
                // draw the contents of the composable into the graphics layer
                this@drawWithContent.drawContent()
            }
            // draw the graphics layer on the visible canvas
            drawLayer(graphicsLayer)
        }
        .clickable {
            coroutineScope.launch {
                val bitmap = graphicsLayer.toImageBitmap()
                // do something with the newly acquired bitmap
            }
        }
        .background(Color.White)
) {
    Text("Hello Android", fontSize = 26.sp)
}

Vous pouvez enregistrer le bitmap sur le disque et le partager. Pour en savoir plus, consultez l'exemple d'extrait complet. Veillez à vérifier les autorisations sur l'appareil avant d'essayer d'enregistrer sur le disque.

Modificateur de dessin personnalisé

Pour créer votre propre modificateur personnalisé, implémentez l'interface DrawModifier. Vous aurez ainsi accès à un ContentDrawScope, qui est identique à ce qui est exposé lorsque vous utilisez Modifier.drawWithContent(). Vous pourrez ainsi extraire les opérations de dessin courantes vers des modificateurs de dessin personnalisés afin de nettoyer le code et de fournir des wrappers pratiques. Par exemple, Modifier.background() est un DrawModifier pratique.

Si vous souhaitez implémenter un Modifier qui inverse le contenu verticalement, vous pouvez en créer un comme suit :

class FlippedModifier : DrawModifier {
    override fun ContentDrawScope.draw() {
        scale(1f, -1f) {
            this@draw.drawContent()
        }
    }
}

fun Modifier.flipped() = this.then(FlippedModifier())

Utilisez ensuite ce modificateur inversé appliqué à Text :

Text(
    "Hello Compose!",
    modifier = Modifier
        .flipped()
)

Modificateur inversé personnalisé pour le texte
Figure 17 : Modificateur inversé personnalisé pour le texte

Ressources supplémentaires

Pour voir d'autres exemples d'utilisation de graphicsLayer et de dessin personnalisé, consultez les ressources suivantes :