Éléments graphiques dans Compose

De nombreuses applications doivent pouvoir contrôler avec précision ce qui apparaît à l'écran, que ce soit simplement pour aligner un cadre ou un cercle, ou pour minutieusement agencer toute une série d'éléments graphiques aux styles variés.

Dessin de base avec modificateurs et DrawScope

La méthode principale pour dessiner des éléments personnalisés dans Compose consiste à utiliser des modificateurs tels que Modifier.drawWithContent, Modifier.drawBehind et Modifier.drawWithCache.

Par exemple, pour dessiner un objet derrière un composable, vous pouvez utiliser le modificateur drawBehind pour commencer à exécuter des commandes de dessin :

Spacer(
    modifier = Modifier
        .fillMaxSize()
        .drawBehind {
            // this = DrawScope
        }
)

Si vous n'avez besoin que d'un composable qui dessine, vous pouvez utiliser Canvas. Le composable Canvas est un wrapper pratique pour Modifier.drawBehind. Vous positionnez le Canvas dans votre mise en page de la même manière que n'importe quel autre élément d'interface utilisateur avec Compose. Dans le Canvas, vous pouvez dessiner des éléments avec un contrôle précis sur leur style et leur emplacement.

Tous les modificateurs de dessin exposent un DrawScope, un environnement de dessin cloisonné qui gère son propre état. Cela vous permet de définir les paramètres d'un groupe d'éléments graphiques. DrawScope fournit plusieurs champs utiles, tels que size, un objet Size spécifiant les dimensions actuelles de DrawScope.

Pour dessiner un objet, vous pouvez utiliser l'une des nombreuses fonctions de dessin sur DrawScope. Par exemple, le code suivant dessine un rectangle dans l'angle supérieur gauche de l'écran :

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Magenta,
        size = canvasQuadrantSize
    )
}

Rectangle rose dessiné sur fond blanc et occupant un quart de l'écran
Figure 1 : Rectangle dessiné à l'aide d'un canevas dans Compose

Pour en savoir plus sur les différents modificateurs de dessin, consultez la documentation sur les modificateurs graphiques.

Système de coordonnées

Pour dessiner un élément à l'écran, vous devez connaître son décalage (x et y) et sa taille. Avec de nombreuses méthodes de dessin sur DrawScope, la position et la taille sont fournies par les valeurs de paramètres par défaut. Les paramètres par défaut placent généralement l'élément sur le point [0, 0] du canevas et indiquent une taille (size) par défaut qui remplit toute la zone de dessin, comme dans l'exemple ci-dessus. Le rectangle est positionné en haut à gauche. Pour ajuster la taille et la position de votre élément, vous devez comprendre le système de coordonnées de Compose.

L'origine du système de coordonnées ([0,0]) se trouve tout en haut à gauche de la zone de dessin. x augmente à mesure qu'il se déplace vers la droite, et y augmente à mesure qu'il descend.

Grille montrant le système de coordonnées en haut à gauche [0, 0] et en bas à droite [largeur, hauteur]
Figure 2 : Système de coordonnées dessiné/ Grille de dessin

Par exemple, si vous souhaitez dessiner une ligne diagonale entre l'angle supérieur droit de la zone de canevas et l'angle inférieur gauche, vous pouvez utiliser la fonction DrawScope.drawLine() et spécifier un décalage de début et de fin avec les positions x et y correspondantes :

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawLine(
        start = Offset(x = canvasWidth, y = 0f),
        end = Offset(x = 0f, y = canvasHeight),
        color = Color.Blue
    )
}

Transformations de base

DrawScope propose des transformations pour modifier la façon dont les commandes de dessin sont exécutées, ou à quel emplacement.

Mise à l'échelle

Utilisez DrawScope.scale() pour augmenter d'un facteur la taille de vos opérations de dessin. Les opérations telles que scale() s'appliquent à toutes les opérations de dessin dans le lambda correspondant. Par exemple, le code suivant multiplie scaleX par 10 et scaleY par 15 :

Canvas(modifier = Modifier.fillMaxSize()) {
    scale(scaleX = 10f, scaleY = 15f) {
        drawCircle(Color.Blue, radius = 20.dp.toPx())
    }
}

Cercle mis à l'échelle de manière non uniforme
Figure 3 : Application d'une opération de mise à l'échelle à un cercle sur un canevas

Translation

Utilisez DrawScope.translate() pour déplacer vos opérations de dessin vers le haut, le bas, la gauche ou la droite. Par exemple, le code suivant déplace le dessin de 100 pixels vers la droite et de 300 pixels vers le haut :

Canvas(modifier = Modifier.fillMaxSize()) {
    translate(left = 100f, top = -300f) {
        drawCircle(Color.Blue, radius = 200.dp.toPx())
    }
}

Cercle qui s'est déplacé depuis le centre
Figure 4 : Application d'une opération de translation à un cercle dans le canevas

Rotation

Utilisez DrawScope.rotate() pour faire pivoter vos opérations de dessin autour d'un point pivot. Par exemple, le code suivant fait pivoter un rectangle à 45 degrés :

Canvas(modifier = Modifier.fillMaxSize()) {
    rotate(degrees = 45F) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

Téléphone affichant un rectangle incliné à 45 degrés au centre de l'écran
Figure 5 : L'opération rotate() nous permet d'appliquer une rotation à l'environnement d'affichage actuel, ce qui fait pivoter le rectangle de 45 degrés

Encart

Utilisez DrawScope.inset() pour ajuster les paramètres par défaut du DrawScope actuel afin de modifier les limites de tracé et de translater les éléments graphiques en conséquence :

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    inset(horizontal = 50f, vertical = 30f) {
        drawRect(color = Color.Green, size = canvasQuadrantSize)
    }
}

Ce code ajoute une marge intérieure aux commandes de dessin :

Rectangle entouré d'une marge intérieure
Figure 6 : Appliquer un encart aux commandes de dessin

Plusieurs transformations

Pour appliquer plusieurs transformations à vos dessins, utilisez la fonction DrawScope.withTransform(), qui crée et applique une seule transformation combinant toutes les modifications souhaitées. Il est plus efficace d'utiliser withTransform() que de passer des appels imbriqués pour des transformations individuelles, car toutes les transformations sont effectuées ensemble en une seule opération, ce qui évite à Compose de devoir calculer et enregistrer chaque étape imbriquée.

Par exemple, le code suivant applique une translation et une rotation au rectangle :

Canvas(modifier = Modifier.fillMaxSize()) {
    withTransform({
        translate(left = size.width / 5F)
        rotate(degrees = 45F)
    }) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

Téléphone avec un rectangle incliné et décalé vers le côté de l'écran
Figure 7 : Utilisation de withTransform pour appliquer à la fois une rotation et une translation, pour faire pivoter le rectangle et le décaler vers la gauche

Opérations de dessin courantes

Dessiner du texte

Pour dessiner du texte dans Compose, vous pouvez généralement utiliser le composable Text. Toutefois, si vous êtes dans un DrawScope ou si vous souhaitez dessiner le texte manuellement avec la personnalisation, vous pouvez opter pour la méthode DrawScope.drawText().

Pour dessiner du texte, créez un TextMeasurer à l'aide de rememberTextMeasurer et appelez drawText avec l'outil de mesure :

val textMeasurer = rememberTextMeasurer()

Canvas(modifier = Modifier.fillMaxSize()) {
    drawText(textMeasurer, "Hello")
}

Message "Hello" dessiné sur un canevas
Figure 8 : Dessiner du texte sur un canevas

Mesurer le texte

Le dessin de texte fonctionne légèrement différemment des autres commandes de dessin. Normalement, vous devez indiquer à la commande de dessin la taille (largeur et hauteur) requise pour dessiner la forme ou l'image. Avec le texte, plusieurs paramètres contrôlent la taille du texte affiché, tels que la taille de la police, la police elle-même, les ligatures et l'espacement entre les lettres.

Avec Compose, vous pouvez utiliser un TextMeasurer pour accéder à la taille de texte mesurée, en fonction des facteurs ci-dessus. Si vous souhaitez dessiner un arrière-plan derrière le texte, vous pouvez utiliser les informations mesurées pour obtenir la taille de la zone occupée par le texte :

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixedWidth((size.width * 2f / 3f).toInt()),
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

Cet extrait de code crée un arrière-plan rose pour le texte :

Texte multiligne occupant 2⁄3 de la surface totale, avec un rectangle en arrière-plan
Figure 9 : Texte multiligne occupant 2⁄3 de la surface totale, avec un rectangle en arrière-plan

Si vous ajustez les contraintes, la taille de la police ou toute autre propriété qui affecte la taille mesurée, une nouvelle taille est signalée. Vous pouvez définir une taille fixe pour width et height. Le texte suit alors le TextOverflow spécifié. Par exemple, le code suivant affiche le texte dans un tiers de la hauteur et dans un tiers de la largeur de la zone du composable, et définit TextOverflow sur TextOverflow.Ellipsis :

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixed(
                        width = (size.width / 3f).toInt(),
                        height = (size.height / 3f).toInt()
                    ),
                    overflow = TextOverflow.Ellipsis,
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

Le texte est maintenant dessiné dans les limites spécifiées, avec des points de suspension à la fin :

Texte dessiné sur fond rose, avec des points de suspension qui indiquent que le texte a été tronqué
Figure 10 : TextOverflow.Ellipsis avec des contraintes fixes sur la mesure du texte

Dessiner une image

Pour dessiner un ImageBitmap avec DrawScope, chargez l'image à l'aide d'ImageBitmap.imageResource(), puis appelez drawImage :

val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    drawImage(dogImage)
})

Image d'un chien dessiné sur un canevas
Figure 11 : Dessin d'un ImageBitmap sur un canevas

Dessiner des formes de base

De nombreuses fonctions permettent de dessiner des formes sur DrawScope. Pour dessiner une forme, utilisez l'une des fonctions de dessin prédéfinies, telles que drawCircle :

val purpleColor = Color(0xFFBA68C8)
Canvas(
    modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
    onDraw = {
        drawCircle(purpleColor)
    }
)

API

Sortie

drawCircle()

dessiner un cercle

drawRect()

dessiner un rectangle

drawRoundedRect()

dessiner un rectangle aux coins arrondis

drawLine()

dessiner une ligne

drawOval()

dessiner une ellipse

drawArc()

dessiner un arc

drawPoints()

dessiner des points

Dessiner un tracé

Un tracé est une série d'instructions mathématiques qui permettent de générer un dessin une fois qu'elles sont exécutées. DrawScope peut dessiner un tracé à l'aide de la méthode DrawScope.drawPath().

Imaginons que vous souhaitiez dessiner un triangle. Vous pouvez générer un tracé avec des fonctions telles que lineTo() et moveTo() en utilisant la taille de la zone de dessin. Appelez ensuite drawPath() avec ce tracé nouvellement créé pour obtenir un triangle.

Spacer(
    modifier = Modifier
        .drawWithCache {
            val path = Path()
            path.moveTo(0f, 0f)
            path.lineTo(size.width / 2f, size.height / 2f)
            path.lineTo(size.width, 0f)
            path.close()
            onDrawBehind {
                drawPath(path, Color.Magenta, style = Stroke(width = 10f))
            }
        }
        .fillMaxSize()
)

Triangle violet à l'envers dessiné dans Compose
Figure 12 : Créer et dessiner un Path dans Compose

Accéder à l'objet Canvas

Avec DrawScope, vous n'avez pas d'accès direct à un objet Canvas. Vous pouvez utiliser DrawScope.drawIntoCanvas() pour accéder à l'objet Canvas lui-même au niveau duquel vous pouvez appeler des fonctions.

Par exemple, si vous disposez d'un Drawable personnalisé que vous souhaitez dessiner sur le canevas, vous pouvez accéder au canevas et appeler Drawable#draw(), en transmettant l'objet Canvas :

val drawable = ShapeDrawable(OvalShape())
Spacer(
    modifier = Modifier
        .drawWithContent {
            drawIntoCanvas { canvas ->
                drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
                drawable.draw(canvas.nativeCanvas)
            }
        }
        .fillMaxSize()
)

Élément ShapeDrawable noir ovale occupant la taille complète
Figure 13 : Accès au canevas pour dessiner un élément Drawable

En savoir plus

Pour en savoir plus sur le dessin dans Compose, consultez les ressources suivantes :