L'input RTF consente agli utenti di formattare ciò che digitano, ad esempio mettendo in grassetto una parola, aggiungendo un'intestazione, allineando un paragrafo o inserendo un @mention evidenziato.
In Compose, puoi creare questa funzionalità sopra i campi di testo basati sullo stato (BasicTextField e TextField). Gli stili si trovano in TextFieldState insieme al testo, quindi sopravvivono alle modifiche, alle variazioni di selezione e alle ricomposizioni e rimangono sincronizzati mentre l'utente digita, elimina e incolla.
Questa pagina mostra come applicare, aggiornare, eseguire query, estrarre e rimuovere gli stili in un TextFieldState utilizzando TextFieldBuffer e come gestire una barra degli strumenti di formattazione da TextFieldState.textStyles.
Applica stili in linea e di paragrafo
Puoi applicare la formattazione a livello di carattere e di paragrafo all'interno di qualsiasi ambito del ricevitore TextFieldBuffer, inclusi TextFieldState.edit, InputTransformation.transformInput e OutputTransformation.transformOutput.
Controlla l'espansione dei confini con ExpandPolicy
Quando chiami addStyle, puoi passare un ExpandPolicy per definire il comportamento dell'intervallo con stile quando l'utente inserisce nuovi caratteri direttamente al suo confine start o end:
- Inserimento all'interno di
(start, end): espande sempre l'intervallo con stile. - Inserimento prima di
start: sposta sempre gli offsetstarteendverso destra. - Inserimento direttamente in
startoend: controllato dalExpandPolicyspecificato.
La tabella seguente descrive le opzioni ExpandPolicy disponibili e i relativi casi d'uso tipici:
| Norme | Comportamento dei confini | Casi d'uso consigliati | Esempio |
|---|---|---|---|
ExpandPolicy.InsideOnly |
Il testo digitato in start o end rimane al di fuori dell'intervallo con stile. |
Token discreti come @mentions, #hashtags o pillole di sfondo in cui la digitazione accanto al tag non deve ereditare il colore di sfondo del tag. |
![]() |
ExpandPolicy.AtStart |
Il testo digitato in start eredita lo stile, mentre il testo digitato in end rimane senza stile. |
Anche il testo digitato direttamente davanti al testo con stile deve adottare la stessa formattazione. | ![]() |
ExpandPolicy.AtEnd |
Il testo digitato in end eredita lo stile, mentre il testo digitato in start rimane senza stile. |
Digitazione standard nell'editor di testo RTF, in cui la formattazione in grassetto o corsivo viene mantenuta se continui a digitare dopo una parola in grassetto o corsivo. | ![]() |
ExpandPolicy.AtBoth |
Il testo digitato in start o end espande l'intervallo ed eredita lo stile. |
Evidenziazioni attive o blocchi formattati continui che dovrebbero espandersi da entrambi i lati. | ![]() |
Applica la formattazione dei caratteri con SpanStyle
Utilizza addStyle con un SpanStyle per applicare la formattazione a livello di carattere,
come il colore del testo, l'evidenziazione dello sfondo, FontWeight, FontStyle
o la sottolineatura a un TextRange specifico nel buffer.
Comportamenti chiave per gli intervalli SpanStyle
- Ordine di sovrapposizione: gli stili vengono applicati nell'ordine in cui li aggiungi al buffer. Se più intervalli
SpanStylesovrapposti impostano la stessa proprietà con valori diversi, lo stile aggiunto successivamente viene visualizzato sopra. - Requisito di intervallo non vuoto: devi passare un intervallo non compresso in cui
start<end. La chiamata diaddStylesu una selezione compressa di lunghezza0non ha alcun effetto e il passaggio di indici invertiti o fuori intervallo genera unIllegalArgumentException.
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 ) } } } )
Applica la formattazione dei blocchi con ParagraphStyle
Utilizza addStyle con un ParagraphStyle per applicare la formattazione strutturale a livello di blocco, ad esempio textAlign, lineHeight, textDirection o textIndent, a un intervallo di paragrafi.
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.Insidie da evitare con ParagraphStyle
Poiché ParagraphStyle si applica a interi blocchi di testo, evita di utilizzarlo nei seguenti scenari:
- Espansione nei paragrafi adiacenti: imposta
ExpandPolicycon attenzione in modo che gli intervalliParagraphStyleadiacenti non si sovrappongano dopo le modifiche dell'utente. Ad esempio, se[0, 5)utilizzaExpandPolicy.AtEnde[5, 10)utilizzaExpandPolicy.AtStart, l'inserimento di testo nell'indice5espande entrambi gli stili in una sovrapposizione e genera unIllegalArgumentException. - Intervalli
ParagraphStylenidificati: sebbene gli intervalli completamente nidificati (come[0, 10)e[2, 8)) siano validi inAnnotatedString, evita di nidificare gli intervalliParagraphStylein un campo di testo. Layout suddivide gli intervalli nidificati in paragrafi separati e le modifiche successive possono inavvertitamente trasformare gli intervalli nidificati in una sovrapposizione parziale.
Aggiorna uno stile esistente con TrackedRange
La chiamata di addStyle con TextRange e ExpandPolicy restituisce un handle TrackedRange, che può essere TrackedRange<SpanStyle> o TrackedRange<ParagraphStyle>. Un TrackedRange è un riferimento dinamico a un intervallo di stile in TextFieldBuffer che aggiorna automaticamente gli offset dei caratteri start e end quando il testo viene inserito, eliminato o sostituito intorno.
Mutabilità in loco
Non è necessario chiamare removeStyle e addStyle per aggiornare uno stile esistente o modificarne i confini. All'interno di un ambito TextFieldBuffer, TrackedRange fornisce proprietà di estensione che aggiornano l'intervallo in loco, preservando l'ordine di stratificazione originale rispetto ad altri stili:
TrackedRange<SpanStyle>.spanStyle: legge o sostituisce il payloadSpanStylein posizione su unTrackedRange<SpanStyle>.TrackedRange<ParagraphStyle>.paragraphStyle: legge o sostituisce il payloadParagraphStylein posizione su unTrackedRange<ParagraphStyle>.TrackedRange<*>.textRange: legge o aggiorna i limitiTextRangeattuali in loco. L'impostazione di un intervallo compresso, invertito o fuori dai limiti genera un erroreIllegalArgumentException.TrackedRange<*>.expandPolicy: legge o aggiornaExpandPolicyin loco.TrackedRange<*>.isValid: restituiscetruese l'intervallo di stile è ancora attivo nel buffer ofalsese le modifiche hanno ridotto la sua lunghezza a0o se è stato rimosso.
// 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) }
Limitazione dell'ambito e regole del ciclo di vita
Tieni presente le seguenti regole relative all'ambito e al ciclo di vita quando utilizzi le istanze TrackedRange:
- Limitato a
TextFieldBuffer: le proprietà di estensioneTrackedRangeesistono solo nell'ambitoTextFieldBufferin cui l'handle è stato creato o sottoposto a query. - Eliminazione automatica in caso di compressione: se le modifiche dell'utente eliminano tutti i caratteri coperti da un
TrackedRange, la sua lunghezza si riduce a0,isValidrestituiscefalsee lo stile viene rimosso dal buffer.
Esegui query e rimuovi gli stili
Compose offre due modi per ispezionare gli stili attivi, a seconda che tu stia modificando il buffer o leggendo lo stato della UI:
- All'interno di un blocco
TextFieldBuffer: chiamagetSpanStylesegetParagraphStylesdirettamente sul ricevitoreTextFieldBufferper ottenere handleTrackedRangemodificabili. - Al di fuori di un blocco di modifica: leggi
TextFieldState.textStyles, che restituisce elenchiAnnotatedString.Rangeimmutabili che riflettono lo stato di commit.
Esegui query sugli stili di intersezione in TextFieldBuffer
TextFieldBuffer.getSpanStyles e TextFieldBuffer.getParagraphStyles restituiscono tutte le istanze TrackedRange che si intersecano con l'elemento TextRange specificato, ordinate in base al momento in cui sono state aggiunte al buffer.
Ad esempio, puoi eseguire una query su getSpanStyles(selection) all'interno di state.edit per
esaminare o aggiornare gli stili che coprono la selezione corrente dell'utente:
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 } // ... }
Gli intervalli di stile utilizzano un intervallo matematico semiaperto [start, end). La restituzione di uno stile di contatto dipende dal fatto che tu esegua una query su una selezione o su un singolo punto del cursore:
- Per una selezione non vuota
[start, end):- Restituiti: qualsiasi stile che si sovrappone, contiene o si trova all'interno dell'intervallo di selezione. Ad esempio, la query
[5, 15)corrisponde a[0, 20),[8, 12)o[0, 10). - Non restituito: stili che toccano solo i bordi esterni senza sovrapporsi ai caratteri. Ad esempio, la query
[5, 15)esclude[0, 5)e[15, 25).
- Restituiti: qualsiasi stile che si sovrappone, contiene o si trova all'interno dell'intervallo di selezione. Ad esempio, la query
- Per un cursore compresso
[index, index]:- Restituito: qualsiasi stile contenente il cursore o che inizia nella posizione del cursore. Ad esempio, un cursore in
10corrisponde a[0, 20)e[10, 20). - Non restituito (regola di fine contatto): qualsiasi stile che termina nella posizione del cursore. Ad esempio, un cursore in
10esclude[0, 10).
- Restituito: qualsiasi stile contenente il cursore o che inizia nella posizione del cursore. Ad esempio, un cursore in
Rimuovi o attiva/disattiva gli stili con removeStyle
Passa un handle TrackedRange a removeStyle per eliminare l'intervallo di stile specifico da TextFieldBuffer.
removeStyle restituisce true se TrackedRange è stato trovato e rimosso, oppure false se era già stato rimosso o apparteneva a un buffer diverso. Rimuove solo l'handle TrackedRange scelto come target, lasciando intatti gli altri stili sovrapposti.
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 ) } } }
Estrai o serializza RTF
Per visualizzare l'input formattato in un composable Text di sola lettura o serializzarlo in un servizio di backend, leggi TextFieldState.textStyles al di fuori di un blocco di modifica.
state.textStyles.getSpanStyles e
state.textStyles.getParagraphStyles restituiscono
List<AnnotatedString.Range<SpanStyle>> e
List<AnnotatedString.Range<ParagraphStyle>>, che puoi passare direttamente al
costruttore AnnotatedString:
fun TextFieldState.toAnnotatedString(): AnnotatedString { val fullRange = TextRange(0, text.length) return AnnotatedString( text = text.toString(), spanStyles = textStyles.getSpanStyles(fullRange), paragraphStyles = textStyles.getParagraphStyles(fullRange) ) }
Esempio completo in Jetchat
Per un'implementazione completa che combina lo stile dei caratteri in linea (grassetto, corsivo, sottolineato, barrato e colore), consulta l'esempio Jetchat su GitHub:
RichTextState.kt: incapsula le query di stileTextFieldState, l'attivazione/disattivazione degli intervalli, il ritaglio o la suddivisione dei limiti di selezione e altro ancora.UserInput.kt: collegaRichTextStatealla barra degli strumenti di formattazione del testo RTF e al campo di immissione del messaggio.



