Tính năng nhập văn bản đa dạng thức cho phép người dùng định dạng nội dung họ nhập, bao gồm cả việc in đậm một từ, thêm tiêu đề, căn chỉnh đoạn văn hoặc chèn một @mention được làm nổi bật.
Trong Compose, bạn xây dựng tính năng này dựa trên các trường văn bản dựa trên trạng thái (BasicTextField và TextField). Kiểu nằm trong TextFieldState cùng với văn bản, vì vậy, kiểu sẽ tồn tại sau khi chỉnh sửa, thay đổi lựa chọn và kết hợp lại, đồng thời luôn đồng bộ hoá khi người dùng nhập, xoá và dán.
Trang này trình bày cách áp dụng, cập nhật, truy vấn, trích xuất và xoá kiểu trong TextFieldState bằng TextFieldBuffer, cũng như cách điều khiển thanh công cụ định dạng từ TextFieldState.textStyles.
Áp dụng kiểu cùng dòng và kiểu đoạn văn
Bạn có thể áp dụng định dạng ở cấp ký tự và cấp đoạn văn trong bất kỳ phạm vi trình thu nhận TextFieldBuffer nào, bao gồm cả TextFieldState.edit, InputTransformation.transformInput và OutputTransformation.transformOutput.
Kiểm soát việc mở rộng ranh giới bằng ExpandPolicy
Khi gọi addStyle, bạn có thể truyền một ExpandPolicy để xác định cách phạm vi được tạo kiểu hoạt động khi người dùng chèn các ký tự mới ngay tại ranh giới start hoặc end:
- Chèn vào bên trong
(start, end): Luôn mở rộng phạm vi được tạo kiểu. - Chèn trước
start: Luôn dịch chuyển cả độ lệchstartvàendsang bên phải. - Chèn trực tiếp tại
starthoặcend: DoExpandPolicymà bạn chỉ định kiểm soát.
Bảng sau đây mô tả các tuỳ chọn ExpandPolicy hiện có và các trường hợp sử dụng thông thường:
| Chính sách | Hành vi ranh giới | Trường hợp nên sử dụng | Ví dụ |
|---|---|---|---|
ExpandPolicy.InsideOnly |
Văn bản được nhập tại start hoặc end nằm ngoài phạm vi được tạo kiểu. |
Các mã thông báo riêng biệt như @mentions, #hashtags hoặc thẻ nền mà khi nhập bên cạnh thẻ thì không được kế thừa màu nền của thẻ. |
![]() |
ExpandPolicy.AtStart |
Văn bản được nhập tại start sẽ kế thừa kiểu, còn văn bản được nhập tại end sẽ không được định kiểu. |
Văn bản được nhập ngay trước văn bản được định kiểu cũng phải áp dụng cùng một định dạng. | ![]() |
ExpandPolicy.AtEnd |
Văn bản được nhập tại end sẽ kế thừa kiểu, còn văn bản được nhập tại start sẽ không được định kiểu. |
Nhập văn bản đa dạng thức theo cách thông thường, trong đó việc tiếp tục nhập sau một từ in đậm hoặc in nghiêng sẽ tiếp tục định dạng đó. | ![]() |
ExpandPolicy.AtBoth |
Văn bản được nhập tại start hoặc end sẽ mở rộng phạm vi và kế thừa kiểu. |
Các khối được định dạng liên tục hoặc được làm nổi bật đang hoạt động sẽ mở rộng từ một trong hai cạnh. | ![]() |
Áp dụng định dạng ký tự bằng SpanStyle
Sử dụng addStyle với SpanStyle để áp dụng định dạng ở cấp ký tự, chẳng hạn như màu văn bản, màu nền, FontWeight, FontStyle hoặc gạch dưới cho một TextRange cụ thể trong vùng đệm.
Hành vi chính trong phạm vi SpanStyle
- Thứ tự phân lớp: Kiểu được áp dụng theo thứ tự bạn thêm vào vùng đệm. Nếu nhiều dải ô
SpanStylechồng chéo đặt cùng một thuộc tính với các giá trị khác nhau, thì kiểu được thêm sau sẽ hiển thị ở trên cùng. - Yêu cầu về dải ô không trống: Bạn phải truyền một dải ô không thu gọn, trong đó
start<end. Việc gọiaddStyletrên một vùng chọn thu gọn có độ dài0sẽ không có hiệu lực và việc truyền các chỉ mục đảo ngược hoặc nằm ngoài giới hạn sẽ gửi mộtIllegalArgumentException.
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 ) } } } )
Áp dụng định dạng khối bằng ParagraphStyle
Sử dụng addStyle với ParagraphStyle để áp dụng định dạng cấu trúc cấp khối như textAlign, lineHeight, textDirection hoặc textIndent cho một dải ô đoạn văn.
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.Những sai lầm cần tránh khi sử dụng ParagraphStyle
Vì ParagraphStyle áp dụng cho toàn bộ khối văn bản, nên hãy tránh sử dụng thuộc tính này trong các trường hợp sau:
- Mở rộng sang các đoạn văn liền kề: Đặt
ExpandPolicycẩn thận để các dải ôParagraphStyleliền kề không chồng chéo sau khi người dùng chỉnh sửa. Ví dụ: nếu[0, 5)sử dụngExpandPolicy.AtEndvà[5, 10)sử dụngExpandPolicy.AtStart, thì việc chèn văn bản tại chỉ mục5sẽ mở rộng cả hai kiểu thành một kiểu chồng chéo và gửi mộtIllegalArgumentException. - Lồng các dải ô
ParagraphStyle: Mặc dù các dải ô được lồng hoàn toàn (chẳng hạn như[0, 10)và[2, 8)) là hợp lệ trongAnnotatedString, nhưng hãy tránh lồng các dải ôParagraphStyletrong một trường văn bản. Bố cục chia các dải ô lồng nhau thành các đoạn riêng biệt và các nội dung chỉnh sửa tiếp theo có thể vô tình biến các dải ô lồng nhau thành một phần chồng chéo.
Cập nhật kiểu tại chỗ bằng TrackedRange
Việc gọi addStyle bằng TextRange và ExpandPolicy sẽ trả về một tay điều khiển TrackedRange, có thể là TrackedRange<SpanStyle> hoặc TrackedRange<ParagraphStyle>. TrackedRange là một tham chiếu động đến một khoảng kiểu trong TextFieldBuffer, tự động cập nhật các độ lệch ký tự start và end khi văn bản được chèn, xoá hoặc thay thế xung quanh nó.
Khả năng biến đổi tại chỗ
Bạn không cần gọi removeStyle và addStyle để cập nhật kiểu hiện có hoặc điều chỉnh ranh giới của kiểu đó. Trong phạm vi TextFieldBuffer, TrackedRange cung cấp các thuộc tính tiện ích giúp cập nhật khoảng thời gian tại chỗ trong khi vẫn giữ nguyên thứ tự phân lớp ban đầu so với các kiểu khác:
TrackedRange<SpanStyle>.spanStyle: Đọc hoặc thay thế tải trọngSpanStyletại chỗ trênTrackedRange<SpanStyle>.TrackedRange<ParagraphStyle>.paragraphStyle: Đọc hoặc thay thế tải trọngParagraphStyletại chỗ trênTrackedRange<ParagraphStyle>.TrackedRange<*>.textRange: Đọc hoặc cập nhật các giới hạnTextRangehiện tại. Việc đặt một dải ô thu gọn, đảo ngược hoặc nằm ngoài giới hạn sẽ gửi mộtIllegalArgumentException.TrackedRange<*>.expandPolicy: Đọc hoặc cập nhậtExpandPolicytại chỗ.TrackedRange<*>.isValid: Trả vềtruenếu khoảng thời gian của kiểu vẫn đang hoạt động trong vùng đệm, hoặcfalsenếu các nội dung chỉnh sửa khiến độ dài của khoảng thời gian đó giảm xuống0hoặc nếu khoảng thời gian đó đã bị xoá.
// 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) }
Quy tắc về giới hạn phạm vi và vòng đời
Hãy lưu ý các quy tắc về phạm vi và vòng đời sau đây khi làm việc với các thực thể TrackedRange:
- Giới hạn trong
TextFieldBuffer: Các thuộc tính tiện íchTrackedRangechỉ tồn tại trong phạm viTextFieldBuffernơi tay điều khiển được tạo hoặc truy vấn. - Tự động cắt bớt khi thu gọn: Nếu người dùng chỉnh sửa xoá tất cả ký tự thuộc phạm vi của một
TrackedRange, thì độ dài của ký tự đó sẽ thu nhỏ thành0,isValidtrả vềfalsevà kiểu sẽ bị xoá khỏi vùng đệm.
Truy vấn và xoá kiểu
Compose cung cấp hai cách để kiểm tra kiểu đang hoạt động, tuỳ thuộc vào việc bạn đang thay đổi vùng đệm hay đọc trạng thái cho giao diện người dùng:
- Bên trong khối
TextFieldBuffer: GọigetSpanStylesvàgetParagraphStylestrực tiếp trên trình nhậnTextFieldBufferđể lấy các handleTrackedRangecó thể thay đổi. - Bên ngoài khối chỉnh sửa: Đọc
TextFieldState.textStyles, thao tác này sẽ trả về danh sáchAnnotatedString.Rangekhông thể thay đổi, phản ánh trạng thái đã cam kết.
Kiểu giao nhau của truy vấn trong TextFieldBuffer
TextFieldBuffer.getSpanStyles và TextFieldBuffer.getParagraphStyles trả về tất cả các thực thể TrackedRange giao nhau với TextRange đã cho, được sắp xếp theo thời điểm chúng được thêm vào vùng đệm.
Ví dụ: bạn có thể truy vấn getSpanStyles(selection) bên trong state.edit để kiểm tra hoặc cập nhật các kiểu áp dụng cho lựa chọn hiện tại của người dùng:
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 } // ... }
Khoảng kiểu sử dụng phạm vi toán học nửa mở [start, end). Việc kiểu chạm có được trả về hay không phụ thuộc vào việc bạn truy vấn một vùng chọn hay một điểm con trỏ:
- Đối với lựa chọn không trống
[start, end):- Được trả về: Mọi kiểu trùng lặp, chứa hoặc nằm trong dải ô lựa chọn. Ví dụ: cụm từ tìm kiếm
[5, 15)khớp với[0, 20),[8, 12)hoặc[0, 10). - Không được trả về: Kiểu chỉ chạm vào các cạnh ranh giới bên ngoài mà không chồng chéo các ký tự. Ví dụ: truy vấn
[5, 15)loại trừ[0, 5)và[15, 25).
- Được trả về: Mọi kiểu trùng lặp, chứa hoặc nằm trong dải ô lựa chọn. Ví dụ: cụm từ tìm kiếm
- Đối với con trỏ thu gọn
[index, index]:- Trả về: Mọi kiểu chứa con trỏ hoặc bắt đầu tại vị trí con trỏ. Ví dụ: một con trỏ tại
10khớp với[0, 20)và[10, 20). - Không được trả về (quy tắc chạm vào điểm cuối): Mọi kiểu kết thúc tại vị trí con trỏ. Ví dụ: con trỏ tại
10sẽ loại trừ[0, 10).
- Trả về: Mọi kiểu chứa con trỏ hoặc bắt đầu tại vị trí con trỏ. Ví dụ: một con trỏ tại
Xoá hoặc bật/tắt kiểu bằng removeStyle
Truyền một tay điều khiển TrackedRange đến removeStyle để xoá khoảng kiểu cụ thể đó khỏi TextFieldBuffer.
removeStyle trả về true nếu tìm thấy và xoá TrackedRange, hoặc false nếu đã xoá hoặc thuộc về một vùng đệm khác. Thao tác này chỉ xoá
tay điều khiển TrackedRange được nhắm mục tiêu, giữ nguyên các kiểu chồng chéo khác.
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 ) } } }
Trích xuất hoặc chuyển đổi tuần tự văn bản đa dạng thức
Để hiển thị thông tin đầu vào được định dạng trong một composable Text chỉ có thể đọc hoặc chuyển đổi tuần tự dữ liệu đó thành một dịch vụ phụ trợ, hãy đọc TextFieldState.textStyles bên ngoài một khối chỉnh sửa.
state.textStyles.getSpanStyles và state.textStyles.getParagraphStyles trả về List<AnnotatedString.Range<SpanStyle>> và List<AnnotatedString.Range<ParagraphStyle>>, bạn có thể chuyển trực tiếp đến hàm khởi tạo AnnotatedString:
fun TextFieldState.toAnnotatedString(): AnnotatedString { val fullRange = TextRange(0, text.length) return AnnotatedString( text = text.toString(), spanStyles = textStyles.getSpanStyles(fullRange), paragraphStyles = textStyles.getParagraphStyles(fullRange) ) }
Ví dụ hoàn chỉnh trong Jetchat
Để xem cách triển khai hoàn chỉnh kết hợp kiểu ký tự cùng dòng (đậm, nghiêng, gạch chân, gạch ngang chữ và màu), hãy xem mẫu Jetchat trên GitHub:
RichTextState.kt: Đóng gói các truy vấn kiểuTextFieldState, bật/tắt span, cắt bớt hoặc phân tách trên các ranh giới lựa chọn, v.v.UserInput.kt: Kết nốiRichTextStatevới thanh công cụ định dạng văn bản đa dạng thức và trường nhập dữ liệu tin nhắn.



