Éléments graphiques dans Compose

Jetpack Compose facilite l'utilisation d'éléments graphiques personnalisés. 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. Grâce à l'approche déclarative de Compose, la configuration de tous les éléments graphiques est centralisée, plutôt que dispersée entre un appel de méthode et un objet Helper Paint. Compose crée et met à jour les objets nécessaires de manière efficace.

Éléments graphiques déclaratifs avec Compose

Compose étend son approche déclarative à la gestion des éléments graphiques. L'approche de Compose offre de nombreux avantages :

  • Compose minimise l'état dans ses éléments graphiques, ce qui vous permet d'éviter les pièges liés à la programmation des états.
  • Lorsque vous affichez un élément, toutes les options se trouvent là où elles sont attendues, dans la fonction modulable.
  • Les API graphiques de Compose se chargent efficacement de créer et de libérer des objets.

Canvas

Le composable principal pour les éléments graphiques personnalisés est Canvas. Vous positionnez votre 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 disposer des éléments avec un contrôle précis sur leur style et leur emplacement.

Par exemple, ce code crée un composable Canvas qui remplit tout l'espace disponible dans son élément parent :

Canvas(modifier = Modifier.fillMaxSize()) {
}

Canvas expose automatiquement un environnement d'affichage cloisonné DrawScope, qui maintient 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 et maximales de l'environnement DrawScope.

À titre d'exemple, supposons que vous souhaitiez dessiner une ligne diagonale reliant l'angle supérieur droit à l'angle inférieur gauche de votre Canvas. Pour ce faire, vous devez ajouter un composable drawLine :

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

Téléphone avec une ligne diagonale fine traversant l'écran.

Figure 1 : Utilisation de drawLine pour tracer une ligne sur le Canvas. Le code définit la couleur de la ligne, mais utilise la largeur par défaut.

Vous pouvez utiliser d'autres paramètres pour personnaliser l'élément graphique. Par défaut, la ligne de cet exemple est tracée avec une largeur minimale (un pixel), quelle que soit l'échelle de l'élément. Vous pouvez remplacer la valeur par défaut en définissant une valeur strokeWidth :

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,
        strokeWidth = 5F
    )
}

Téléphone avec une ligne diagonale plus épaisse traversant l'écran.

Figure 2 : Remplacement de la largeur par défaut pour modifier la ligne de la figure 1.

Il existe de nombreuses autres fonctions de dessin simples, telles que drawRect et drawCircle. Par exemple, ce code affiche un cercle plein et centré avec un diamètre égal à la moitié de la dimension la plus courte de l'environnement :

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawCircle(
        color = Color.Blue,
        center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
        radius = size.minDimension / 4
    )
}

Téléphone avec un cercle bleu au centre de l'écran.

Figure 3 : Utilisation de drawCircle pour afficher un cercle au centre de l'environnement. Par défaut, drawCircle génère un cercle plein. Il n'est pas nécessaire de spécifier ce paramètre explicitement.

Les fonctions de dessin disposent de paramètres par défaut utiles. Par exemple, par défaut, drawRectangle() remplit tout l'environnement du parent, et le rayon de drawCircle() vaut la moitié de la dimension la plus courte du parent. Comme toujours en Kotlin, vous pouvez rendre votre code beaucoup plus simple et plus clair en exploitant les valeurs de paramètre par défaut pour ne définir que les paramètres à modifier. Vous pouvez en profiter en fournissant des paramètres explicites pour les méthodes de dessin DrawScope, car vos éléments graphiques baseront leurs paramètres par défaut sur les paramètres de l'environnement parent.

DrawScope

Comme indiqué, chaque Canvas Compose expose un DrawScope, un environnement d'affichage cloisonné dans lequel sont exécutées vos commandes de dessin.

Par exemple, le code suivant affiche un rectangle en haut à gauche de l'environnement :

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

Vous pouvez utiliser la fonction DrawScope.inset() pour ajuster les paramètres par défaut de la portée actuelle afin de modifier les limites de tracé et de translater les éléments graphiques en conséquence. Les opérations telles que inset() s'appliquent à toutes les opérations de dessin dans le lambda correspondant :

val canvasQuadrantSize = size / 2F
inset(50F, 30F) {
    drawRect(
        color = Color.Green,
        size = canvasQuadrantSize
    )
}

DrawScope propose d'autres transformations simples, telles que rotate(). Par exemple, ce code dessine un rectangle centré qui occupe un neuvième de l'environnement :

val canvasSize = size
val canvasWidth = size.width
val canvasHeight = size.height
drawRect(
    color = Color.Gray,
    topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
    size = canvasSize / 3F
)

Téléphone avec un rectangle rempli au centre de l'écran.

Figure 4 : Utilisation de drawRect pour afficher un rectangle rempli au centre de l'écran.

Vous pouvez faire pivoter le rectangle en appliquant une rotation à sa DrawScope :

rotate(degrees = 45F) {
    drawRect(
        color = Color.Gray,
        topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
        size = canvasSize / 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.

Si vous souhaitez appliquer plusieurs transformations à vos éléments graphiques, la meilleure approche n'est pas de créer des environnements DrawScope imbriqués, mais d'utiliser la fonction withTransform(), qui génère 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, ce code applique une translation et une rotation au rectangle :

withTransform({
    translate(left = canvasWidth / 5F)
    rotate(degrees = 45F)
}) {
    drawRect(
        color = Color.Gray,
        topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
        size = canvasSize / 3F
    )
}

Téléphone avec un rectangle incliné et décalé vers le côté de l'écran.

Figure 6 : Ici, nous utilisons withTransform pour appliquer à la fois une rotation et une translation, pour faire pivoter le rectangle et le décaler vers la gauche.