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 }
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 :
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()) ) } } )
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 } )
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() } )
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 } )
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 } )
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 :
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).
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)) ) }
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 } )
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é.
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 :
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 :
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())) } } }
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)
É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() )
Ressources supplémentaires
Pour voir d'autres exemples d'utilisation de graphicsLayer
et de dessin personnalisé, consultez les ressources suivantes :
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Éléments graphiques dans Compose
- Personnaliser une image {:#customize-image}
- Kotlin pour Jetpack Compose