TextMeasurer est une API de Jetpack Compose conçue pour mesurer les dimensions du texte avant son rendu effectif à l’écran. Contrairement à l’approche classique via Paint.measureText dans Android View, TextMeasurer offre une manière native Compose d’obtenir la largeur, la hauteur et les métriques des lignes en tenant compte du style et des contraintes. Selon Android Developers Documentation (2025), TextMeasurer est activement utilisé dans les composants Layout personnalisés, les étiquettes animées et les éléments d’interface adaptatifs où la taille du texte affecte le positionnement des autres éléments.
Points Clés
TextMeasurer est une interface fonctionnelle du package androidx.compose.ui.text qui permet de mesurer du texte dans Compose sans le rendre réellement à l’écran. Il fait partie du moteur de texte de Compose et opère au niveau de Paragraph API, qui utilise Skia pour le rendu.
Le but principal de TextMeasurer est la pré-mesure. Dans Compose, la taille du texte est généralement déterminée après que le Composable est déjà placé dans le Layout. Cependant, il existe des scénarios où vous avez besoin de connaître la largeur du texte avant la mise en page — par exemple, pour décider si une chaîne tient dans un conteneur, ou pour calculer la taille d’un bouton en fonction de la longueur du texte.
TextMeasurer résout ce problème en fournissant la méthode measure(), qui prend un TextMeasureRequest (texte, style, contraintes) et retourne TextLayoutResult (métriques, lignes, caractères). Cela permet au développeur d’obtenir toutes les données du texte de manière synchrone, sans décalage d’une image.
Utilisez TextMeasurer lorsque la taille du texte affecte la taille du composant parent, lors de la création de transitions de texte animées, ou lors de l’implémentation de Layouts personnalisés où le texte coexiste avec d’autres éléments.
L’API TextMeasurer se compose de deux entités clés : rememberTextMeasurer() (création d’une instance) et measure() (exécution de la mesure). L’instance est créée une fois pour tout le cycle de vie du composant et est réutilisée pour toutes les mesures.
val textMeasurer = rememberTextMeasurer()
val result = textMeasurer.measure(
text = "Hello, Compose!",
style = MaterialTheme.typography.bodyLarge,
constraints = Constraints(maxWidth = 200, maxHeight = 50)
)
// result.size.width, result.size.height, result.lineCount
Le paramètre constraints définit la largeur et la hauteur maximales dans lesquelles le texte doit tenir. Si le texte ne tient pas dans la largeur, il passe à la ligne suivante. Le résultat de la mesure contient un TextLayoutResult, qui fournit le nombre de lignes, la position de chaque caractère, la largeur de chaque ligne et la taille totale du texte.
TextMeasurer fonctionne de manière synchrone sur le thread UI et ne déclenche pas de recomposition. Cependant, une mesure fréquente du texte avec différents styles peut créer une surcharge — donc l’instance est créée via remember et réutilisée.
Le scénario le plus simple consiste à mesurer la largeur d’un texte sur une seule ligne pour déterminer la taille du conteneur parent. Par exemple, pour un bouton dont la largeur dépend de la longueur de l’étiquette.
val textMeasurer = rememberTextMeasurer()
var label by remember { mutableStateOf("Submit") }
val textResult = remember(label) {
textMeasurer.measure(
text = label,
style = MaterialTheme.typography.labelLarge,
constraints = Constraints(maxWidth = 1000, maxHeight = 100)
)
}
Box(
modifier = Modifier
.width(textResult.size.width.dp)
.height(40.dp)
.background(Color.Blue)
.clickable { onClick() }
) {
Text(text = label, style = MaterialTheme.typography.labelLarge)
}
Dans cet exemple, remember(label) garantit que la mesure redémarre uniquement lorsque le texte change. Les contraintes sont définies avec un maxWidth large (1000) pour éviter le retour à la ligne — la contrainte réelle sera égale à la largeur du texte. La largeur du Box est définie sur la largeur mesurée du texte, ce qui donne un effet de « bouton ajusté au texte ».
Remarque : la largeur du résultat doit être convertie en dp avec .dp, car TextMeasurer retourne les dimensions en pixels, tandis que Modifier.width attend des Dp. Pour la précision, utilisez density et LocalDensity.current.
Pour le texte multiligne, TextMeasurer permet de déterminer non seulement la hauteur totale, mais aussi le nombre de lignes et la position de chaque caractère dans le paragraphe. Cela est nécessaire lors de la création de champs de texte personnalisés, de chats ou d’éditeurs.
Les principales propriétés de TextLayoutResult pour le texte multiligne : lineCount (nombre de lignes), getLineTop(index) (coordonnée Y du début de ligne), getLineBottom(index) (coordonnée Y de fin de ligne), getLineWidth(index) (largeur de ligne). En les combinant, vous pouvez positionner précisément les éléments d’interface par rapport aux lignes de texte.
val textMeasurer = rememberTextMeasurer()
val result = textMeasurer.measure(
text = "Long multiline text that needs to be measured before layout",
style = MaterialTheme.typography.bodyMedium,
constraints = Constraints(maxWidth = 150, maxHeight = 300)
)
val linesCount = result.lineCount
val firstLineWidth = result.getLineWidth(0)
val totalHeight = result.size.height
La méthode getLineWidth(lineIndex) retourne la largeur de la ligne spécifiée en pixels. Elle peut être inférieure à maxWidth si la ligne n’atteint pas la limite du conteneur. En connaissant la largeur de chaque ligne, vous pouvez implémenter un effet de justification manuellement ou placer des éléments en ligne à côté des lignes courtes.
Pour obtenir la position d’un caractère spécifique, utilisez getBoundingBox(offset), qui retourne un Rect avec les coordonnées left, top, right, bottom. Cela est utile pour positionner le curseur dans des champs de texte personnalisés ou pour mettre en surbrillance une plage de texte avec une animation.
L’application la plus puissante de TextMeasurer est les composants Layout personnalisés, où le texte coexiste avec d’autres éléments et leurs tailles sont interdépendantes. Par exemple, un widget « Icône + texte » où l’icône doit être centrée par rapport à la première ligne de texte, et non au bloc entier.
Dans un Row standard, l’icône est centrée sur toute la hauteur du Row, ce qui semble peu naturel lorsque le texte occupe 3 lignes alors que l’icône n’en fait qu’une. Avec TextMeasurer, vous pouvez mesurer la première ligne et aligner l’icône précisément sur celle-ci.
val textMeasurer = rememberTextMeasurer()
Layout(
content = {
Icon(imageVector = Icons.Default.Star, contentDescription = null)
Text(text = "Multi-line text here")
},
measurePolicy = { measurables, constraints ->
val textMeasurable = measurables[1]
val textPlaceable = textMeasurable.measure(constraints)
val iconPlaceable = measurables[0].measure(constraints)
val firstLineHeight = textMeasurer.measure(
text = "Sample",
style = MaterialTheme.typography.bodyMedium,
constraints = Constraints(maxWidth = 2000, maxHeight = 100)
).size.height
layout(width = textPlaceable.width + iconPlaceable.width,
height = maxOf(textPlaceable.height, iconPlaceable.height)) {
iconPlaceable.placeRelative(0, (firstLineHeight - iconPlaceable.height) / 2)
textPlaceable.placeRelative(iconPlaceable.width, 0)
}
}
)
Dans cet exemple, la mesure de la première ligne via TextMeasurer détermine la position de l’icône. Sans TextMeasurer, l’icône serait centrée sur toute la hauteur du texte multiligne, ce qui est visuellement incorrect. Ce motif est largement utilisé dans les chats, les listes de contacts et les cartes d’information.
TextMeasurer est une API synchrone qui s’exécute sur le thread UI. Une utilisation excessive peut entraîner des chutes d’images, surtout lors de la mesure de grands volumes de texte sur des appareils faibles.
Première règle — mettez en cache le résultat. Enveloppez toujours l’appel à measure() dans remember avec des dépendances de texte et de style. N’appelez jamais measure() à l’intérieur de CompositionLocal ou dans des chemins de recomposition à chaud.
Deuxième règle — limitez maxWidth et maxHeight. Si vous définissez maxWidth = Constraints.Infinity, TextMeasurer ne pourra pas couper les lignes et le texte sortira de l’écran. Passez toujours des contraintes réalistes, surtout pour le texte multiligne.
Troisième règle — évitez les mesures répétées avec les mêmes paramètres. Si le texte et le style n’ont pas changé, le résultat de measure() sera le même. Combinez la mesure avec Deferred ou LaunchedEffect pour les scénarios asynchrones.
Foire Aux Questions
TextMeasurer mesure le texte avant le rendu, tandis que onSizeChanged mesure après. TextMeasurer est nécessaire lorsque la taille du texte affecte la mise en page parente ; onSizeChanged convient pour la logique réactive après la composition.
Oui, TextMeasurer.measure() accepte AnnotatedString, qui inclut SpannableString avec des styles en ligne, des liens et des couleurs. Tous les spans sont pris en compte lors de la mesure.
TextMeasurer opère au niveau du rendu Skia, où toutes les dimensions sont en pixels. Convertissez via LocalDensity.current : density.run { width.toDp() }.
TextMeasurer utilise FontFamily.Default jusqu’à ce qu’une police personnalisée soit chargée. Pour une mesure précise, attendez le chargement de la police via FontResource et utilisez async/await.
Mesurer le texte dans chaque élément de LazyColumn peut réduire les FPS sur les appareils faibles. Optimisation : mettez en cache le résultat via remember et limitez la taille du texte.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi