Tekst sformatowany umożliwia użytkownikom formatowanie wpisywanego tekstu, w tym pogrubianie słów, dodawanie nagłówków, wyrównywanie akapitów czy wstawianie wyróżnionego elementu @mention.
W Compose tworzysz to na podstawie pól tekstowych opartych na stanie (BasicTextField i TextField). Style są przechowywane w TextFieldState razem z tekstem, więc nie są tracone podczas edycji, zmian zaznaczenia i rekompozycji. Są też synchronizowane, gdy użytkownik wpisuje, usuwa i wkleja tekst.
Na tej stronie dowiesz się, jak stosować, aktualizować, wyodrębniać i usuwać style w TextFieldState za pomocą TextFieldBuffer oraz jak sterować paskiem narzędzi formatowania z TextFieldState.textStyles.
Stosowanie stylów wbudowanych i stylów akapitów
Formatowanie na poziomie znaków i akapitów możesz stosować w dowolnym zakresie odbiornika TextFieldBuffer, w tym TextFieldState.edit, InputTransformation.transformInput i OutputTransformation.transformOutput.
Kontrolowanie rozszerzania granic za pomocą ExpandPolicy
Gdy wywołujesz addStyle, możesz przekazać ExpandPolicy, aby określić, jak zachowuje się zakres stylizowany, gdy użytkownik wstawia nowe znaki bezpośrednio na granicy start lub end:
- Wstawianie wewnątrz
(start, end): zawsze rozszerza zakres stylizowany. - Wstawienie przed
start: zawsze przesuwa przesunięciastartiendw prawo. - Wstawianie bezpośrednio w
startlubend: kontrolowane przez określony przez Ciebie parametrExpandPolicy.
W tabeli poniżej opisujemy dostępne opcje ExpandPolicy i ich typowe przypadki użycia:
| Zasady | Działanie na granicy | Zalecane przypadki użycia | Przykład |
|---|---|---|---|
ExpandPolicy.InsideOnly |
Tekst wpisany w miejscu start lub end pozostaje poza zakresem stylizowanym. |
Odrębne tokeny, takie jak @mentions, #hashtags lub pigułki tła, w przypadku których wpisywanie obok tagu nie powinno powodować dziedziczenia koloru tła tagu. |
![]() |
ExpandPolicy.AtStart |
Tekst wpisany w miejscu start dziedziczy styl, a tekst wpisany w miejscu end pozostaje bez stylu. |
Tekst wpisany bezpośrednio przed tekstem ze stylem powinien również przyjąć to samo formatowanie. | ![]() |
ExpandPolicy.AtEnd |
Tekst wpisany w miejscu end dziedziczy styl, a tekst wpisany w miejscu start pozostaje bez stylu. |
Standardowe pisanie w edytorze tekstu sformatowanego, gdzie kontynuowanie pisania po słowie pogrubionym lub zapisanym kursywą powoduje kontynuowanie tego formatowania. | ![]() |
ExpandPolicy.AtBoth |
Tekst wpisany w miejscu start lub end rozszerza zakres i dziedziczy styl. |
Aktywne wyróżnienia lub ciągłe sformatowane bloki, które powinny rozszerzać się od dowolnej krawędzi. | ![]() |
Stosowanie formatowania znaków za pomocą SpanStyle
Użyj addStyle z SpanStyle, aby zastosować formatowanie na poziomie znaków, takie jak kolor tekstu, wyróżnienie tła, FontWeight, FontStyle lub podkreślenie, do określonego TextRange w buforze.
Kluczowe zachowania w zakresach SpanStyle
- Kolejność warstw: style są stosowane w kolejności, w jakiej zostały dodane do bufora. Jeśli kilka nakładających się zakresów
SpanStyleustawia tę samą właściwość z różnymi wartościami, styl dodany później jest renderowany na górze. - Wymaganie dotyczące niepustego zakresu: musisz przekazać zakres, który nie jest zwinięty, gdzie
start<end. Wywołanie metodyaddStylena zwiniętym zaznaczeniu o długości0nie ma żadnego efektu, a przekazanie odwróconych lub wykraczających poza zakres indeksów powoduje błądIllegalArgumentException.
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 ) } } } )
Stosowanie formatowania blokowego za pomocą ParagraphStyle
Użyj addStyle z ParagraphStyle, aby zastosować formatowanie strukturalne na poziomie bloku, takie jak textAlign, lineHeight, textDirection lub textIndent, do zakresu akapitu.
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.Czego unikać w przypadku ParagraphStyle
Ponieważ tag ParagraphStyle dotyczy całych bloków tekstu, unikaj używania go w tych sytuacjach:
- Rozszerzanie na sąsiednie akapity: ustaw
ExpandPolicyostrożnie, aby sąsiednie zakresyParagraphStylenie nakładały się na siebie po zmianach wprowadzonych przez użytkownika. Jeśli na przykład[0, 5)używaExpandPolicy.AtEnd, a[5, 10)używaExpandPolicy.AtStart, wstawienie tekstu w indeksie5powoduje rozszerzenie obu stylów, co prowadzi do ich nakładania się i wywołania błęduIllegalArgumentException. - Zagnieżdżanie zakresów
ParagraphStyle: chociaż całkowicie zagnieżdżone zakresy (takie jak[0, 10)i[2, 8)) są prawidłowe wAnnotatedString, unikaj zagnieżdżania zakresówParagraphStylew polu tekstowym. Układ dzieli zagnieżdżone zakresy na osobne akapity, a kolejne zmiany mogą nieumyślnie przekształcić zagnieżdżone zakresy w częściowe nakładanie się.
Zaktualizuj styl na miejscu za pomocą TrackedRange
Wywołanie addStyle z TextRange i ExpandPolicy zwraca uchwyt TrackedRange, który może być TrackedRange<SpanStyle> lub TrackedRange<ParagraphStyle>. TrackedRange to dynamiczne odwołanie do interwału stylu w TextFieldBuffer, które automatycznie aktualizuje przesunięcia znaków start i end, gdy tekst jest wstawiany, usuwany lub zastępowany wokół niego.
Zmienność na miejscu
Nie musisz wywoływać funkcji removeStyle i addStyle, aby zaktualizować istniejący styl lub dostosować jego granice. W zakresie TextFieldBuffer TrackedRange udostępnia właściwości rozszerzenia, które aktualizują interwał na miejscu, zachowując pierwotną kolejność warstw względem innych stylów:
TrackedRange<SpanStyle>.spanStyle: odczytuje lub zastępuje ładunekSpanStylena miejscu wTrackedRange<SpanStyle>.TrackedRange<ParagraphStyle>.paragraphStyle: odczytuje lub zastępuje ładunekParagraphStylena urządzeniuTrackedRange<ParagraphStyle>.TrackedRange<*>.textRange: odczytuje lub aktualizuje bieżące graniceTextRangena miejscu. Ustawienie zwiniętego, odwróconego lub wykraczającego poza zakres zakresu powoduje błądIllegalArgumentException.TrackedRange<*>.expandPolicy: odczytuje lub aktualizujeExpandPolicyna miejscu.TrackedRange<*>.isValid: zwracatrue, jeśli interwał stylu jest nadal aktywny w buforze, lubfalse, jeśli zmiany spowodowały skrócenie jego długości do0lub jeśli został usunięty.
// 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) }
Ograniczenie zakresu i reguły cyklu życia
Podczas pracy z instancjami TrackedRange pamiętaj o tych regułach dotyczących zakresu i cyklu życia:
- Ograniczone do
TextFieldBuffer: właściwości rozszerzeniaTrackedRangeistnieją tylko w zakresieTextFieldBuffer, w którym uchwyt został utworzony lub odpytany. - Automatyczne usuwanie po zwinięciu: jeśli zmiany wprowadzone przez użytkownika spowodują usunięcie wszystkich znaków objętych przez
TrackedRange, jego długość zmniejszy się do0,isValidzwrócifalse, a styl zostanie usunięty z bufora.
Wykonywanie zapytań i usuwanie stylów
Compose udostępnia 2 sposoby sprawdzania aktywnych stylów w zależności od tego, czy mutujesz bufor, czy odczytujesz stan interfejsu:
- Wewnątrz bloku
TextFieldBuffer: wywołajgetSpanStylesigetParagraphStylesbezpośrednio na odbiornikuTextFieldBuffer, aby uzyskać zmienne uchwytyTrackedRange. - Poza blokiem edycji: odczytaj
TextFieldState.textStyles, który zwraca niezmienne listyAnnotatedString.Rangeodzwierciedlające zatwierdzony stan.
Style przecinające zapytanie w: TextFieldBuffer
TextFieldBuffer.getSpanStyles i TextFieldBuffer.getParagraphStyles zwracają wszystkie instancje TrackedRange, które przecinają się z danym TextRange, uporządkowane według czasu dodania do bufora.
Możesz na przykład wysłać zapytanie getSpanStyles(selection) wewnątrz state.edit, aby sprawdzić lub zaktualizować style obejmujące bieżące zaznaczenie użytkownika:
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 } // ... }
Przedziały stylów używają półotwartego zakresu matematycznego [start, end). To, czy zwracany jest styl dotykowy, zależy od tego, czy zapytanie dotyczy zaznaczenia czy pojedynczego punktu kursora:
- W przypadku niepustego zaznaczenia
[start, end):- Zwracane: dowolny styl, który pokrywa się z zakresem zaznaczenia, zawiera go lub znajduje się w nim. Na przykład zapytanie
[5, 15)pasuje do[0, 20),[8, 12)lub[0, 10). - Nie zwrócono: style, które tylko dotykają zewnętrznych krawędzi obramowania, ale nie nakładają się na znaki. Na przykład zapytanie
[5, 15)wyklucza[0, 5)i[15, 25).
- Zwracane: dowolny styl, który pokrywa się z zakresem zaznaczenia, zawiera go lub znajduje się w nim. Na przykład zapytanie
- W przypadku zwiniętego kursora
[index, index]:- Zwracane: dowolny styl zawierający kursor lub zaczynający się w pozycji kursora. Na przykład kursor w pozycji
10pasuje do[0, 20)i[10, 20). - Not returned (touching end rule): Any style ending at the cursor
position. Na przykład kursor w pozycji
10wyklucza[0, 10).
- Zwracane: dowolny styl zawierający kursor lub zaczynający się w pozycji kursora. Na przykład kursor w pozycji
Usuwanie i przełączanie stylów za pomocą removeStyle
Przekaż uchwyt TrackedRange do removeStyle, aby usunąć ten konkretny interwał stylu z obiektu TextFieldBuffer.
removeStyle zwraca true, jeśli element TrackedRange został znaleziony i usunięty, lub false, jeśli został już usunięty lub należał do innego bufora. Usuwa tylko docelowy uchwyt TrackedRange, pozostawiając inne nakładające się style bez zmian.
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 ) } } }
Wyodrębnianie i serializacja tekstu sformatowanego
Aby wyświetlić sformatowane dane wejściowe w funkcji kompozycyjnej Text tylko do odczytu lub zserializować je do usługi backendu, odczytaj TextFieldState.textStyles poza blokiem edycji.
state.textStyles.getSpanStyles i state.textStyles.getParagraphStyles zwracają List<AnnotatedString.Range<SpanStyle>> i List<AnnotatedString.Range<ParagraphStyle>>, które możesz przekazać bezpośrednio do konstruktora AnnotatedString:
fun TextFieldState.toAnnotatedString(): AnnotatedString { val fullRange = TextRange(0, text.length) return AnnotatedString( text = text.toString(), spanStyles = textStyles.getSpanStyles(fullRange), paragraphStyles = textStyles.getParagraphStyles(fullRange) ) }
Kompletny przykład w Jetchat
Aby zobaczyć pełną implementację, która łączy style znaków w tekście (pogrubienie, kursywę, podkreślenie, przekreślenie i kolor), zapoznaj się z przykładem Jetchat na GitHubie:
RichTextState.kt: obejmuje zapytania o stylTextFieldState, przełączanie zakresu, przycinanie lub dzielenie w granicach zaznaczenia i inne funkcje.UserInput.kt: łączyRichTextStatez paskiem narzędzi formatowania tekstu sformatowanego i polem do wprowadzania danych wiadomości.



