Mit der Rich-Text-Eingabe können Nutzer den eingegebenen Text formatieren, z. B. ein Wort fett formatieren, eine Überschrift hinzufügen, einen Absatz ausrichten oder einen hervorgehobenen @mention einfügen.
In Compose erstellen Sie diese auf zustandsbasierten Textfeldern (BasicTextField und TextField). Stile befinden sich in TextFieldState neben dem Text, sodass sie Änderungen, Auswahländerungen und Neuzusammensetzungen überstehen und synchron bleiben, wenn der Nutzer Text eingibt, löscht und einfügt.
Auf dieser Seite wird beschrieben, wie Sie mit TextFieldBuffer Stile in einem TextFieldState anwenden, aktualisieren, abfragen, extrahieren und entfernen und wie Sie eine Formatierungssymbolleiste aus TextFieldState.textStyles steuern.
Inline- und Absatzstile anwenden
Sie können Formatierungen auf Zeichen- und Absatzebene innerhalb eines beliebigen TextFieldBuffer-Empfängerbereichs anwenden, einschließlich TextFieldState.edit, InputTransformation.transformInput und OutputTransformation.transformOutput.
Grenzenerweiterung mit ExpandPolicy steuern
Wenn Sie addStyle aufrufen, können Sie ein ExpandPolicy übergeben, um festzulegen, wie sich der formatierte Bereich verhält, wenn der Nutzer neue Zeichen direkt an der Grenze start oder end einfügt:
- Einfügen innerhalb von
(start, end): Der formatierte Bereich wird immer erweitert. - Einfügen vor
start: Verschiebt sowohl denstart- als auch denend-Offset immer nach rechts. - Direktes Einfügen bei
startoderend: Wird durch die von Ihnen angegebeneExpandPolicygesteuert.
In der folgenden Tabelle werden die verfügbaren ExpandPolicy-Optionen und ihre typischen Anwendungsfälle beschrieben:
| Richtlinie | Grenzverhalten | Empfohlene Anwendungsfälle | Beispiel |
|---|---|---|---|
ExpandPolicy.InsideOnly |
Text, der bei start oder end eingegeben wird, bleibt außerhalb des formatierten Bereichs. |
Diskrete Tokens wie @mentions, #hashtags oder Hintergrund-Pills, bei denen die Eingabe neben dem Tag nicht die Hintergrundfarbe des Tags übernehmen soll. |
![]() |
ExpandPolicy.AtStart |
Text, der bei start eingegeben wird, übernimmt die Formatierung, während Text, der bei end eingegeben wird, unformatiert bleibt. |
Text, der direkt vor dem formatierten Text eingegeben wird, sollte ebenfalls dieselbe Formatierung übernehmen. | ![]() |
ExpandPolicy.AtEnd |
Text, der bei end eingegeben wird, übernimmt den Stil, und Text, der bei start eingegeben wird, bleibt unformatiert. |
Standardeingabe im Rich-Text-Editor, bei der die Formatierung fortgesetzt wird, wenn Sie nach einem fett oder kursiv formatierten Wort weitertippen. | ![]() |
ExpandPolicy.AtBoth |
Text, der entweder bei start oder end eingegeben wird, erweitert den Bereich und übernimmt den Stil. |
Aktive Hervorhebungen oder kontinuierlich formatierte Blöcke, die von beiden Seiten aus wachsen sollen. | ![]() |
Zeichenformatierung mit SpanStyle anwenden
Verwenden Sie addStyle mit einem SpanStyle, um eine Formatierung auf Zeichenebene wie Textfarbe, Hintergrundhervorhebung, FontWeight, FontStyle oder Unterstreichung auf einen bestimmten TextRange im Zwischenspeicher anzuwenden.
Wichtige Verhaltensweisen für SpanStyle-Bereiche
- Reihenfolge der Ebenen: Stile werden in der Reihenfolge angewendet, in der Sie sie dem Zwischenspeicher hinzufügen. Wenn mehrere sich überschneidende
SpanStyle-Bereiche dieselbe Property mit unterschiedlichen Werten festlegen, wird der später hinzugefügte Stil darüber gerendert. - Anforderung für nicht leeren Bereich: Sie müssen einen nicht minimierten Bereich übergeben, in dem
start<endist. Der Aufruf vonaddStylefür eine minimierte Auswahl der Länge0hat keine Auswirkungen. Wenn umgekehrte oder außerhalb des zulässigen Bereichs liegende Indexe übergeben werden, wird einIllegalArgumentExceptionausgelöst.
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 ) } } } )
Blockformatierung mit ParagraphStyle anwenden
Verwenden Sie addStyle mit einem ParagraphStyle, um eine strukturelle Formatierung auf Blockebene wie textAlign, lineHeight, textDirection oder textIndent auf einen Absatzbereich anzuwenden.
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 umschalten.Zu vermeidende Fehler bei ParagraphStyle
Da ParagraphStyle auf ganze Textblöcke angewendet wird, sollten Sie es in den folgenden Szenarien nicht verwenden:
- Erweiterung auf benachbarte Absätze: Legen Sie
ExpandPolicysorgfältig fest, damit sich benachbarteParagraphStyle-Bereiche nach Nutzeränderungen nicht überschneiden. Wenn beispielsweise[0, 5)ExpandPolicy.AtEndund[5, 10)ExpandPolicy.AtStartverwendet, wird durch das Einfügen von Text bei Index5eine Überschneidung der beiden Stile verursacht und einIllegalArgumentExceptionausgegeben. ParagraphStyle-Bereiche verschachteln: Vollständig verschachtelte Bereiche (z. B.[0, 10)und[2, 8)) sind inAnnotatedStringzwar gültig, Sie sollten jedoch das Verschachteln vonParagraphStyle-Bereichen in einem Textfeld vermeiden. Das Layout teilt verschachtelte Bereiche in separate Absätze auf. Durch nachfolgende Änderungen können verschachtelte Bereiche versehentlich zu einer teilweisen Überschneidung werden.
Stil mit TrackedRange aktualisieren
Wenn Sie addStyle mit einem TextRange und einem ExpandPolicy aufrufen, wird ein TrackedRange-Handle zurückgegeben, das TrackedRange<SpanStyle> oder TrackedRange<ParagraphStyle> sein kann. Ein TrackedRange ist ein dynamischer Verweis auf ein Stilintervall im TextFieldBuffer, dessen Zeichenversätze start und end automatisch aktualisiert werden, wenn Text um ihn herum eingefügt, gelöscht oder ersetzt wird.
Direkte Veränderlichkeit
Sie müssen removeStyle und addStyle nicht aufrufen, um einen vorhandenen Stil zu aktualisieren oder seine Grenzen anzupassen. Innerhalb eines TextFieldBuffer-Bereichs bietet TrackedRange Erweiterungseigenschaften, mit denen das Intervall direkt aktualisiert wird, während die ursprüngliche Ebenenreihenfolge im Verhältnis zu anderen Stilen beibehalten wird:
TrackedRange<SpanStyle>.spanStyle: Liest oder ersetzt dieSpanStyle-Nutzlast an Ort und Stelle in einemTrackedRange<SpanStyle>.TrackedRange<ParagraphStyle>.paragraphStyle: Liest oder ersetzt dieParagraphStyle-Nutzlast an Ort und Stelle in einemTrackedRange<ParagraphStyle>.TrackedRange<*>.textRange: Liest oder aktualisiert die aktuellenTextRange-Grenzen direkt. Wenn Sie einen minimierten, umgekehrten oder außerhalb des zulässigen Bereichs liegenden Bereich festlegen, wird einIllegalArgumentException-Fehler ausgegeben.TrackedRange<*>.expandPolicy: Liest oder aktualisiert dieExpandPolicydirekt.TrackedRange<*>.isValid: Gibttruezurück, wenn das Stilintervall im Zwischenspeicher noch aktiv ist, oderfalse, wenn die Länge durch Änderungen auf0reduziert wurde oder wenn es entfernt wurde.
// 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) }
Bereichseinschränkung und Lebenszyklusregeln
Beachten Sie bei der Arbeit mit TrackedRange-Instanzen die folgenden Bereichs- und Lebenszyklusregeln:
- Auf
TextFieldBufferbeschränkt:TrackedRange-Erweiterungseigenschaften existieren nur innerhalb desTextFieldBuffer-Bereichs, in dem das Handle erstellt oder abgefragt wurde. - Automatisches Kürzen beim Minimieren: Wenn durch Nutzeränderungen alle Zeichen gelöscht werden, die von einem
TrackedRangeabgedeckt werden, wird die Länge auf0reduziert,isValidgibtfalsezurück und der Stil wird aus dem Zwischenspeicher entfernt.
Stile abfragen und entfernen
Compose bietet zwei Möglichkeiten, aktive Stile zu prüfen, je nachdem, ob Sie den Zwischenspeicher ändern oder den Status für Ihre UI lesen:
- In einem
TextFieldBuffer-Block: RufegetSpanStylesundgetParagraphStylesdirekt auf demTextFieldBuffer-Empfänger auf, um änderbareTrackedRange-Handles zu erhalten. - Außerhalb eines Bearbeitungsblocks: Lesen Sie
TextFieldState.textStyles, wodurch unveränderlicheAnnotatedString.Range-Listen zurückgegeben werden, die den übernommenen Status widerspiegeln.
Sich überschneidende Stile in TextFieldBuffer abfragen
TextFieldBuffer.getSpanStyles und TextFieldBuffer.getParagraphStyles geben alle TrackedRange-Instanzen zurück, die sich mit dem angegebenen TextRange überschneiden, sortiert nach dem Zeitpunkt, zu dem sie dem Puffer hinzugefügt wurden.
Sie können beispielsweise getSpanStyles(selection) in state.edit abfragen, um Stile zu prüfen oder zu aktualisieren, die die aktuelle Auswahl des Nutzers abdecken:
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 } // ... }
Stilintervalle verwenden einen halboffenen mathematischen Bereich [start, end). Ob ein Berührungsstil zurückgegeben wird, hängt davon ab, ob Sie eine Auswahl oder einen einzelnen Cursorpunkt abfragen:
- Für eine nicht leere Auswahl
[start, end):- Zurückgegeben: Alle Stile, die sich mit dem Auswahlbereich überschneiden, ihn enthalten oder sich darin befinden. Beispielsweise stimmt die Abfrage
[5, 15)mit[0, 20),[8, 12)oder[0, 10)überein. - Nicht zurückgegeben: Stile, die nur die äußeren Begrenzungskanten berühren, ohne Zeichen zu überlappen. Beispiel: Die Abfrage
[5, 15)schließt[0, 5)und[15, 25)aus.
- Zurückgegeben: Alle Stile, die sich mit dem Auswahlbereich überschneiden, ihn enthalten oder sich darin befinden. Beispielsweise stimmt die Abfrage
- Für einen minimierten Cursor
[index, index]:- Zurückgegeben: Alle Stile, die den Cursor enthalten oder an der Cursorposition beginnen. Beispiel: Ein Cursor bei
10entspricht[0, 20)und[10, 20). - Nicht zurückgegeben (Regel für Berührung des Endes): Alle Stile, die an der Cursorposition enden. Beispiel: Ein Cursor bei
10schließt[0, 10)aus.
- Zurückgegeben: Alle Stile, die den Cursor enthalten oder an der Cursorposition beginnen. Beispiel: Ein Cursor bei
Stile mit removeStyle entfernen oder umschalten
Übergeben Sie ein TrackedRange-Handle an removeStyle, um das entsprechende Stilintervall aus dem TextFieldBuffer zu löschen.
removeStyle gibt true zurück, wenn der TrackedRange gefunden und entfernt wurde, oder false, wenn er bereits entfernt wurde oder zu einem anderen Puffer gehörte. Es wird nur der Ziel-TrackedRange-Handle entfernt, andere überlappende Stile bleiben intakt.
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 ) } } }
RTF extrahieren oder serialisieren
Wenn Sie formatierte Eingaben in einer schreibgeschützten Text komponierbaren Funktion anzeigen oder in einen Backend-Dienst serialisieren möchten, lesen Sie TextFieldState.textStyles außerhalb eines Bearbeitungsblocks.
state.textStyles.getSpanStyles und state.textStyles.getParagraphStyles geben List<AnnotatedString.Range<SpanStyle>> und List<AnnotatedString.Range<ParagraphStyle>> zurück, die Sie direkt an den AnnotatedString-Konstruktor übergeben können:
fun TextFieldState.toAnnotatedString(): AnnotatedString { val fullRange = TextRange(0, text.length) return AnnotatedString( text = text.toString(), spanStyles = textStyles.getSpanStyles(fullRange), paragraphStyles = textStyles.getParagraphStyles(fullRange) ) }
Vollständiges Beispiel in Jetchat
Eine vollständige Implementierung, die Inline-Zeichenstile (fett, kursiv, unterstrichen, durchgestrichen und farbig) kombiniert, finden Sie im Jetchat-Beispiel auf GitHub:
RichTextState.kt: KapseltTextFieldState-Stilabfragen, das Ein- und Ausschalten von Spans, das Kürzen oder Aufteilen über Auswahlgrenzen hinweg und mehr.UserInput.kt: VerbindetRichTextStatemit der Symbolleiste für die Rich-Text-Formatierung und dem Nachrichteneingabefeld.



