La saisie de texte enrichi permet aux utilisateurs de mettre en forme ce qu'ils saisissent, par exemple en mettant un mot en gras, en ajoutant un titre, en alignant un paragraphe ou en insérant un @mention mis en surbrillance.
Dans Compose, vous créez cette fonctionnalité par-dessus les champs de texte basés sur l'état (BasicTextField et TextField). Les styles se trouvent dans TextFieldState, à côté du texte. Ils survivent donc aux modifications, aux changements de sélection et aux recompositions, et restent synchronisés lorsque l'utilisateur saisit, supprime et colle du texte.
Cette page explique comment appliquer, mettre à jour, interroger, extraire et supprimer des styles dans un TextFieldState à l'aide de TextFieldBuffer, et comment piloter une barre d'outils de mise en forme à partir de TextFieldState.textStyles.
Appliquer des styles intégrés et de paragraphe
Vous pouvez appliquer une mise en forme au niveau des caractères et des paragraphes dans n'importe quel champ d'application du récepteur TextFieldBuffer, y compris TextFieldState.edit, InputTransformation.transformInput et OutputTransformation.transformOutput.
Contrôler l'expansion des limites avec ExpandPolicy
Lorsque vous appelez addStyle, vous pouvez transmettre un ExpandPolicy pour définir le comportement de la plage stylisée lorsque l'utilisateur insère de nouveaux caractères directement à sa limite start ou end :
- Insertion dans
(start, end): étend toujours la plage stylisée. - Insertion avant
start: décale toujours les décalagesstartetendvers la droite. - Insertion directement au niveau de
startouend: contrôlée par leExpandPolicyque vous spécifiez.
Le tableau suivant décrit les options ExpandPolicy disponibles et leurs cas d'utilisation types :
| Règlement | Comportement des limites | Cas d'utilisation recommandés | Exemple |
|---|---|---|---|
ExpandPolicy.InsideOnly |
Le texte saisi au niveau de start ou end reste en dehors de la plage stylisée. |
Jetons discrets tels que @mentions, #hashtags ou les pastilles d'arrière-plan, où la saisie à côté du tag ne doit pas hériter de la couleur d'arrière-plan du tag. |
![]() |
ExpandPolicy.AtStart |
Le texte saisi à l'emplacement start hérite du style, tandis que le texte saisi à l'emplacement end reste sans style. |
Le texte saisi directement devant le texte stylisé doit également adopter la même mise en forme. | ![]() |
ExpandPolicy.AtEnd |
Le texte saisi à l'emplacement end hérite du style, tandis que le texte saisi à l'emplacement start reste sans style. |
Saisie standard dans l'éditeur de texte enrichi, où la saisie après un mot en gras ou en italique conserve cette mise en forme. | ![]() |
ExpandPolicy.AtBoth |
Le texte saisi au niveau de start ou de end étend la plage et hérite du style. |
Mises en surbrillance actives ou blocs formatés continus qui doivent s'étendre à partir de l'un des bords. | ![]() |
Appliquer la mise en forme des caractères avec SpanStyle
Utilisez addStyle avec un SpanStyle pour appliquer une mise en forme au niveau des caractères, comme la couleur du texte, la mise en surbrillance de l'arrière-plan, FontWeight, FontStyle ou le soulignement d'un TextRange spécifique dans le tampon.
Comportements clés pour les plages SpanStyle
- Ordre de superposition : les styles s'appliquent dans l'ordre dans lequel vous les ajoutez au tampon. Si plusieurs plages
SpanStylequi se chevauchent définissent la même propriété avec des valeurs différentes, le style ajouté ultérieurement s'affiche au-dessus. - Exigence de plage non vide : vous devez transmettre une plage non réduite où
start<end. L'appel deaddStylesur une sélection réduite de longueur0n'a aucun effet, et la transmission d'index inversés ou hors limites génère une exceptionIllegalArgumentException.
TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { for (i in 0 until changes.changeCount) { val range = changes.getRange(i) if (!range.collapsed) { addStyle( spanStyle = SpanStyle( fontWeight = FontWeight.ExtraBold, fontStyle = FontStyle.Italic, color = Color.Red, ), range = range, expandPolicy = ExpandPolicy.AtEnd ) } } } )
Appliquer une mise en forme de bloc avec ParagraphStyle
Utilisez addStyle avec un ParagraphStyle pour appliquer une mise en forme structurelle au niveau du bloc, telle que textAlign, lineHeight, textDirection ou textIndent, à une plage de paragraphes.
fun setParagraphAlignment(state: TextFieldState, alignment: TextAlign) { val targetRange = if (state.selection.collapsed) { TextRange(0, state.text.length) } else { state.selection } if (targetRange.collapsed) return state.edit { // 1. Remove existing paragraph styles in this range getParagraphStyles(targetRange).forEach { removeStyle(it) } // 2. Apply the new paragraph alignment addStyle( paragraphStyle = ParagraphStyle(textAlign = alignment), range = targetRange, expandPolicy = ExpandPolicy.InsideOnly ) } }
ParagraphStyle.Pièges à éviter avec ParagraphStyle
Étant donné que ParagraphStyle s'applique à des blocs de texte entiers, évitez de l'utiliser dans les scénarios suivants :
- Expansion dans les paragraphes adjacents : définissez
ExpandPolicyavec soin afin que les plagesParagraphStyleadjacentes ne se chevauchent pas après les modifications de l'utilisateur. Par exemple, si[0, 5)utiliseExpandPolicy.AtEndet[5, 10)utiliseExpandPolicy.AtStart, l'insertion de texte à l'index5étend les deux styles en un chevauchement et génère uneIllegalArgumentException. - Imbrication de plages
ParagraphStyle: bien que les plages complètement imbriquées (telles que[0, 10)et[2, 8)) soient valides dansAnnotatedString, évitez d'imbriquer des plagesParagraphStyledans un champ de texte. La mise en page divise les plages imbriquées en paragraphes distincts, et les modifications ultérieures peuvent par inadvertance transformer les plages imbriquées en chevauchement partiel.
Mettre à jour un style en place avec TrackedRange
L'appel de addStyle avec un TextRange et un ExpandPolicy renvoie un handle TrackedRange, qui peut être TrackedRange<SpanStyle> ou TrackedRange<ParagraphStyle>. Un TrackedRange est une référence dynamique à un intervalle de style dans le TextFieldBuffer qui met automatiquement à jour ses décalages de caractères start et end lorsque du texte est inséré, supprimé ou remplacé autour de lui.
Mutabilité sur place
Vous n'avez pas besoin d'appeler removeStyle et addStyle pour mettre à jour un style existant ou ajuster ses limites. Dans un champ d'application TextFieldBuffer, TrackedRange fournit des propriétés d'extension qui mettent à jour l'intervalle en place tout en préservant son ordre de superposition d'origine par rapport aux autres styles :
TrackedRange<SpanStyle>.spanStyle: lit ou remplace la charge utileSpanStyleen place sur unTrackedRange<SpanStyle>.TrackedRange<ParagraphStyle>.paragraphStyle: lit ou remplace la charge utileParagraphStyleen place sur unTrackedRange<ParagraphStyle>.TrackedRange<*>.textRange: lit ou met à jour les limitesTextRangeactuelles sur place. La définition d'une plage réduite, inversée ou hors limites génère une erreurIllegalArgumentException.TrackedRange<*>.expandPolicy: lit ou met à jour leExpandPolicysur place.TrackedRange<*>.isValid: renvoietruesi l'intervalle de style est toujours actif dans le tampon, oufalsesi des modifications ont réduit sa longueur à0ou s'il a été supprimé.
// Trim an existing bold span in place when the selection overlaps one edge val range = style.textRange if (range.start < selection.min && range.end <= selection.max) { // Selection covers the end of the span - shrink end boundary in place style.textRange = TextRange(range.start, selection.min) } else if (range.start >= selection.min && range.end > selection.max) { // Selection covers the start of the span - shrink start boundary in place style.textRange = TextRange(selection.max, range.end) }
Règles de confinement et de cycle de vie du champ d'application
Tenez compte des règles de champ d'application et de cycle de vie suivantes lorsque vous utilisez des instances TrackedRange :
- Limité à
TextFieldBuffer: les propriétés d'extensionTrackedRangen'existent que dans le champ d'applicationTextFieldBufferoù le handle a été créé ou interrogé. - Élagage automatique lors de la réduction : si les modifications de l'utilisateur suppriment tous les caractères couverts par un
TrackedRange, sa longueur est réduite à0,isValidrenvoiefalseet le style est supprimé du tampon.
Interroger et supprimer des styles
Compose propose deux façons d'inspecter les styles actifs, selon que vous modifiez le tampon ou que vous lisez l'état de votre UI :
- Dans un bloc
TextFieldBuffer: appelezgetSpanStylesetgetParagraphStylesdirectement sur le récepteurTextFieldBufferpour obtenir des poignéesTrackedRangemodifiables. - En dehors d'un bloc de modification : lisez
TextFieldState.textStyles, qui renvoie des listesAnnotatedString.Rangeimmuables reflétant l'état validé.
Interroger les styles d'intersection dans TextFieldBuffer
TextFieldBuffer.getSpanStyles et TextFieldBuffer.getParagraphStyles renvoient toutes les instances TrackedRange qui croisent l'élément TextRange donné, classées par ordre d'ajout au tampon.
Par exemple, vous pouvez interroger getSpanStyles(selection) dans state.edit pour inspecter ou mettre à jour les styles couvrant la sélection actuelle de l'utilisateur :
state.edit { if (selection.collapsed) return@edit // Query all bold spans that overlap with the current selection val boldStyles: List<TrackedRange<SpanStyle>> = getSpanStyles(selection).filter { it.spanStyle.fontWeight == FontWeight.Bold } // ... }
Les intervalles de style utilisent une plage mathématique semi-ouverte [start, end). Le renvoi d'un style adjacent dépend de l'interrogation d'une sélection ou d'un seul point de curseur :
- Pour une sélection non vide
[start, end):- Renvoyé : tout style qui chevauche, contient ou se trouve dans la plage de sélection. Par exemple, la requête
[5, 15)correspond à[0, 20),[8, 12)ou[0, 10). - Non renvoyés : styles qui ne touchent que les bords extérieurs sans chevaucher les caractères. Par exemple, la requête
[5, 15)exclut[0, 5)et[15, 25).
- Renvoyé : tout style qui chevauche, contient ou se trouve dans la plage de sélection. Par exemple, la requête
- Pour un curseur réduit
[index, index]:- Résultat : tout style contenant le curseur ou commençant à la position du curseur. Par exemple, un curseur à
10correspond à[0, 20)et[10, 20). - Non renvoyé (règle de fin de contact) : tout style se terminant à la position du curseur. Par exemple, un curseur à
10exclut[0, 10).
- Résultat : tout style contenant le curseur ou commençant à la position du curseur. Par exemple, un curseur à
Supprimer ou activer/désactiver des styles avec removeStyle
Transmettez un handle TrackedRange à removeStyle pour supprimer cet intervalle de style spécifique de TextFieldBuffer.
removeStyle renvoie true si l'élément TrackedRange a été trouvé et supprimé, ou false s'il a déjà été supprimé ou s'il appartenait à un autre tampon. Il ne supprime que le handle TrackedRange ciblé, laissant intacts les autres styles qui se chevauchent.
fun toggleBold(state: TextFieldState) { state.edit { if (selection.collapsed) return@edit // Query all bold spans that overlap with the current selection val boldStyles: List<TrackedRange<SpanStyle>> = getSpanStyles(selection).filter { it.spanStyle.fontWeight == FontWeight.Bold } if (boldStyles.isNotEmpty()) { // Remove or trim existing bold spans covering the selection for (style in boldStyles) { val range = style.textRange if (range.start >= selection.min && range.end <= selection.max) { removeStyle(style) } else if (range.start < selection.min && range.end > selection.max) { val oldEnd = range.end style.textRange = TextRange(range.start, selection.min) addStyle( spanStyle = SpanStyle(fontWeight = FontWeight.Bold), range = TextRange(selection.max, oldEnd), expandPolicy = ExpandPolicy.AtEnd ) } else if (range.start < selection.min) { style.textRange = TextRange(range.start, selection.min) } else { style.textRange = TextRange(selection.max, range.end) } } } else { // No bold spans intersect the selection - apply bold addStyle( spanStyle = SpanStyle(fontWeight = FontWeight.Bold), range = selection, expandPolicy = ExpandPolicy.AtEnd ) } } }
Extraire ou sérialiser du texte enrichi
Pour afficher une entrée mise en forme dans un composable Text en lecture seule ou la sérialiser dans un service de backend, lisez TextFieldState.textStyles en dehors d'un bloc de modification.
state.textStyles.getSpanStyles et state.textStyles.getParagraphStyles renvoient List<AnnotatedString.Range<SpanStyle>> et List<AnnotatedString.Range<ParagraphStyle>>, que vous pouvez transmettre directement au constructeur AnnotatedString :
fun TextFieldState.toAnnotatedString(): AnnotatedString { val fullRange = TextRange(0, text.length) return AnnotatedString( text = text.toString(), spanStyles = textStyles.getSpanStyles(fullRange), paragraphStyles = textStyles.getParagraphStyles(fullRange) ) }
Exemple complet dans Jetchat
Pour voir une implémentation complète qui combine la mise en forme des caractères intégrés (gras, italique, souligné, barré et couleur), consultez l'exemple Jetchat sur GitHub :
RichTextState.kt: encapsule les requêtes de styleTextFieldState, l'activation/la désactivation des segments, le rognage ou la division des limites de sélection, etc.UserInput.kt: connecteRichTextStateà la barre d'outils de mise en forme du texte enrichi et au champ de saisie du message.



