Input rich text memungkinkan pengguna memformat apa yang mereka ketik, termasuk menebalkan kata,
menambahkan judul, meratakan paragraf, atau menyisipkan @mention yang ditandai.
Di Compose, Anda membangunnya di atas kolom teks berbasis status
(BasicTextField dan TextField). Gaya berada di
TextFieldState bersama teks, sehingga tetap ada setelah pengeditan, perubahan
pilihan, dan rekomposisi, serta tetap sinkron saat pengguna mengetik, menghapus, dan
menempelkan.
Halaman ini menunjukkan cara menerapkan, memperbarui, membuat kueri, mengekstrak, dan menghapus gaya di
TextFieldState menggunakan TextFieldBuffer, dan cara mengarahkan toolbar
pemformatan dari TextFieldState.textStyles.
Menerapkan gaya inline dan paragraf
Anda dapat menerapkan pemformatan tingkat karakter dan tingkat paragraf di dalam
cakupan penerima TextFieldBuffer, termasuk TextFieldState.edit,
InputTransformation.transformInput, dan
OutputTransformation.transformOutput.
Mengontrol perluasan batas dengan ExpandPolicy
Saat memanggil addStyle, Anda dapat meneruskan ExpandPolicy untuk menentukan perilaku
rentang bergaya saat pengguna menyisipkan karakter baru langsung di batas
start atau end:
- Penyisipan di dalam
(start, end): Selalu memperluas rentang bergaya. - Penyisipan sebelum
start: Selalu menggeser offsetstartdanendke kanan. - Penyisipan langsung di
startatauend: Dikontrol olehExpandPolicyyang Anda tentukan.
Tabel berikut menjelaskan opsi ExpandPolicy yang tersedia dan kasus penggunaan umumnya:
| Kebijakan | Perilaku batas | Kasus penggunaan yang direkomendasikan | Contoh |
|---|---|---|---|
ExpandPolicy.InsideOnly |
Teks yang diketik di start atau end tetap berada di luar rentang yang diberi gaya. |
Token diskret seperti @mentions, #hashtags, atau pil latar belakang yang pengetikan di samping tag-nya tidak boleh mewarisi warna latar belakang tag. |
![]() |
ExpandPolicy.AtStart |
Teks yang diketik di start akan mewarisi gaya, dan teks yang diketik di end akan tetap tanpa gaya. |
Teks yang diketik langsung di depan teks bergaya juga harus menggunakan format yang sama. | ![]() |
ExpandPolicy.AtEnd |
Teks yang diketik di end akan mewarisi gaya, dan teks yang diketik di start akan tetap tanpa gaya. |
Pengetikan editor rich text standar, yang melanjutkan pemformatan tersebut jika pengetikan dilanjutkan setelah kata yang dicetak tebal atau miring. | ![]() |
ExpandPolicy.AtBoth |
Teks yang diketik di start atau end akan memperluas rentang dan mewarisi gaya. |
Sorotan aktif atau blok berformat berkelanjutan yang harus bertambah dari salah satu tepi. | ![]() |
Menerapkan pemformatan karakter dengan SpanStyle
Gunakan addStyle dengan SpanStyle untuk menerapkan pemformatan tingkat karakter,
seperti warna teks, sorotan latar belakang, FontWeight, FontStyle,
atau garis bawah ke TextRange tertentu dalam buffer.
Perilaku utama untuk rentang SpanStyle
- Urutan lapisan: Gaya diterapkan sesuai urutan penambahannya ke buffer. Jika
beberapa rentang
SpanStyleyang tumpang-tindih menetapkan properti yang sama dengan nilai yang berbeda, gaya yang ditambahkan kemudian akan dirender di atas. - Persyaratan rentang tidak kosong: Anda harus meneruskan rentang yang tidak diciutkan dengan
start<end. MemanggiladdStylepada pilihan yang diciutkan dengan panjang0tidak akan berpengaruh, dan meneruskan indeks terbalik atau di luar batas akan menampilkanIllegalArgumentException.
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 ) } } } )
Menerapkan pemformatan blok dengan ParagraphStyle
Gunakan addStyle dengan ParagraphStyle untuk menerapkan pemformatan
struktural tingkat blok seperti textAlign, lineHeight,
textDirection, atau textIndent ke rentang paragraf.
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.Kesalahan yang harus dihindari dengan ParagraphStyle
Karena ParagraphStyle berlaku untuk seluruh blok teks, hindari penggunaannya dalam
skenario berikut:
- Memperluas ke paragraf yang berdekatan: Tetapkan
ExpandPolicydengan hati-hati agar rentangParagraphStyleyang berdekatan tidak tumpang-tindih setelah pengeditan pengguna. Misalnya, jika[0, 5)menggunakanExpandPolicy.AtEnddan[5, 10)menggunakanExpandPolicy.AtStart, menyisipkan teks pada indeks5akan memperluas kedua gaya menjadi tumpang-tindih dan menampilkanIllegalArgumentException. - Rentang
ParagraphStylebertingkat: Meskipun rentang bertingkat sepenuhnya (seperti[0, 10)dan[2, 8)) valid diAnnotatedString, hindari rentangParagraphStylebertingkat di kolom teks. Tata letak membagi rentang bertingkat menjadi paragraf terpisah dan pengeditan berikutnya dapat secara tidak sengaja mengubah rentang bertingkat menjadi tumpang-tindih sebagian.
Memperbarui gaya di tempat dengan TrackedRange
Memanggil addStyle dengan TextRange dan ExpandPolicy akan menampilkan handle TrackedRange, yang dapat berupa TrackedRange<SpanStyle> atau TrackedRange<ParagraphStyle>. TrackedRange adalah referensi dinamis ke
interval gaya di TextFieldBuffer yang otomatis memperbarui offset karakter start
dan end saat teks disisipkan, dihapus, atau diganti di sekitarnya.
Mutabilitas langsung
Anda tidak perlu memanggil removeStyle dan addStyle untuk memperbarui gaya yang ada atau menyesuaikan batasnya. Di dalam cakupan TextFieldBuffer, TrackedRange
menyediakan properti ekstensi yang memperbarui interval di tempatnya sekaligus mempertahankan
urutan lapisan aslinya relatif terhadap gaya lain:
TrackedRange<SpanStyle>.spanStyle: Membaca atau mengganti payloadSpanStyledi tempat padaTrackedRange<SpanStyle>.TrackedRange<ParagraphStyle>.paragraphStyle: Membaca atau mengganti payloadParagraphStyledi tempat padaTrackedRange<ParagraphStyle>.TrackedRange<*>.textRange: Membaca atau memperbarui batasTextRangesaat ini di tempat. Menetapkan rentang yang diciutkan, dibalik, atau di luar batas akan menampilkanIllegalArgumentException.TrackedRange<*>.expandPolicy: Membaca atau memperbaruiExpandPolicydi tempat.TrackedRange<*>.isValid: Menampilkantruejika interval gaya masih aktif di buffer, ataufalsejika pengeditan menyebabkan panjangnya diciutkan menjadi0atau jika dihapus.
// 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) }
Pembatasan cakupan dan aturan siklus proses
Perhatikan aturan cakupan dan siklus proses berikut saat menggunakan
instance TrackedRange:
- Terbatas pada
TextFieldBuffer: Properti ekstensiTrackedRangehanya ada dalam cakupanTextFieldBuffertempat handle dibuat atau dikueri. - Pemangkasan otomatis saat diciutkan: Jika hasil edit pengguna menghapus semua karakter yang dicakup
oleh
TrackedRange, panjangnya akan menyusut menjadi0,isValidakan menampilkanfalse, dan gaya akan dihapus dari buffer.
Membuat kueri dan menghapus gaya
Compose menyediakan dua cara untuk memeriksa gaya aktif, bergantung pada apakah Anda mengubah buffer atau membaca status untuk UI:
- Di dalam blok
TextFieldBuffer: PanggilgetSpanStylesdangetParagraphStyleslangsung pada penerimaTextFieldBufferuntuk mendapatkan handleTrackedRangeyang dapat diubah. - Di luar blok edit: Baca
TextFieldState.textStyles, yang menampilkan daftarAnnotatedString.Rangeyang tidak dapat diubah yang mencerminkan status yang di-commit.
Membuat kueri gaya yang berpotongan di TextFieldBuffer
TextFieldBuffer.getSpanStyles dan
TextFieldBuffer.getParagraphStyles menampilkan semua instance TrackedRange
yang berpotongan dengan TextRange yang diberikan, diurutkan berdasarkan waktu penambahannya ke
buffer.
Misalnya, Anda dapat membuat kueri getSpanStyles(selection) di dalam state.edit untuk
memeriksa atau memperbarui gaya yang mencakup pilihan pengguna saat ini:
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 } // ... }
Interval gaya menggunakan rentang matematika semi-terbuka [start, end). Apakah gaya sentuh ditampilkan atau tidak, bergantung pada apakah Anda mengkueri pilihan atau satu titik kursor:
- Untuk pilihan yang tidak kosong
[start, end):- Ditampilkan: Gaya apa pun yang tumpang-tindih, berisi, atau berada di dalam
rentang pilihan. Misalnya, kueri
[5, 15)cocok dengan[0, 20),[8, 12), atau[0, 10). - Tidak ditampilkan: Gaya yang hanya menyentuh tepi batas luar tanpa
karakter yang tumpang-tindih. Misalnya, kueri
[5, 15)mengecualikan[0, 5)dan[15, 25).
- Ditampilkan: Gaya apa pun yang tumpang-tindih, berisi, atau berada di dalam
rentang pilihan. Misalnya, kueri
- Untuk kursor yang diciutkan
[index, index]:- Tampilan: Gaya apa pun yang berisi kursor atau dimulai pada posisi kursor. Misalnya, kursor di
10cocok dengan[0, 20)dan[10, 20). - Tidak ditampilkan (aturan akhir sentuhan): Gaya apa pun yang berakhir di posisi kursor. Misalnya, kursor di
10mengecualikan[0, 10).
- Tampilan: Gaya apa pun yang berisi kursor atau dimulai pada posisi kursor. Misalnya, kursor di
Menghapus atau mengalihkan gaya dengan removeStyle
Teruskan pengendali TrackedRange ke removeStyle untuk menghapus interval gaya tertentu tersebut dari TextFieldBuffer.
removeStyle menampilkan true jika TrackedRange ditemukan dan dihapus, atau
false jika sudah dihapus atau termasuk dalam buffer yang berbeda. Tindakan ini hanya menghapus
handle TrackedRange yang ditargetkan, sehingga gaya tumpang-tindih lainnya
tetap utuh.
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 ) } } }
Mengekstrak atau melakukan serialisasi rich text
Untuk menampilkan input yang diformat dalam composable Text hanya baca, atau menyerialisasikannya ke
layanan backend, baca TextFieldState.textStyles di luar blok edit.
state.textStyles.getSpanStyles dan
state.textStyles.getParagraphStyles menampilkan
List<AnnotatedString.Range<SpanStyle>> dan
List<AnnotatedString.Range<ParagraphStyle>>, yang dapat Anda teruskan langsung ke
konstruktor AnnotatedString:
fun TextFieldState.toAnnotatedString(): AnnotatedString { val fullRange = TextRange(0, text.length) return AnnotatedString( text = text.toString(), spanStyles = textStyles.getSpanStyles(fullRange), paragraphStyles = textStyles.getParagraphStyles(fullRange) ) }
Contoh lengkap di Jetchat
Untuk melihat implementasi lengkap yang menggabungkan gaya karakter inline (tebal, miring, garis bawah, coret, dan warna), lihat contoh Jetchat di GitHub:
RichTextState.kt: Mengenkapsulasi kueri gayaTextFieldState, pengalihan span, pemangkasan atau pemisahan di seluruh batas pilihan, dan lainnya.UserInput.kt: MenghubungkanRichTextStateke toolbar pemformatan teks kaya dan kolom input pesan.



