SpannableString là một lớp Android SDK trong gói android.text cho phép áp dụng nhiều kiểu cho các phần khác nhau của một chuỗi văn bản trong TextView. Không giống như đánh dấu HTML, SpannableString hoạt động ở cấp độ đối tượng Span, kiểm soát hiển thị trực quan của văn bản: màu sắc, kích thước, phông chữ, gạch chân và các yếu tố tương tác. Theo Google Developers, SpannableString được sử dụng trong các thành phần hệ thống Android để định dạng liên kết. Đây là cách chính để tạo kiểu văn bản mà không cần thư viện bên thứ ba.
Những điểm chính
SpannableString là một lớp Android triển khai giao diện Spannable, lưu trữ văn bản cùng với một tập hợp các đối tượng Span kiểm soát hiển thị trực quan. Không giống như String thông thường, SpannableString cho phép gắn thuộc tính kiểu vào các phạm vi ký tự cụ thể: làm cho một phần văn bản có màu đỏ, tăng kích thước phông chữ trong tiêu đề hoặc thêm liên kết có thể nhấp bên trong một đoạn văn.
Lớp này nằm trong gói android.text và có sẵn từ API Level 1. SpannableString là bất biến — một khi được tạo, cấu trúc của nó cố định và việc thay thế văn bản đòi hỏi phải tạo một đối tượng mới. Để chỉnh sửa động, SpannableStringBuilder được sử dụng, hỗ trợ chèn và xóa ký tự mà không mất kiểu.
CharSequence là giao diện cơ bản cho dữ liệu văn bản, được triển khai bởi String, StringBuilder và SpannableString. Sự khác biệt chính giữa SpannableString và String là hỗ trợ gắn các đối tượng tùy ý vào một chuỗi con. TextView nhận ra giao diện Spannable và áp dụng các đối tượng span vào các đoạn văn bản tương ứng trong quá trình kết xuất. Nếu bạn truyền một String thông thường cho TextView, sẽ không có kiểu nào được áp dụng.
SpannableString lưu trữ văn bản dưới dạng mảng char[] và một mảng riêng các đối tượng span với siêu dữ liệu về vị trí bắt đầu và kết thúc. Khi setSpan(what, start, end, flags) được gọi, đối tượng what được lưu trong danh sách cùng với thông tin phạm vi. Trong quá trình kết xuất, TextView tuần tự áp dụng tất cả các span nằm trong phạm vi hiển thị, gọi các phương thức updateDrawState và updateMeasureState.
CharacterStyle là lớp cơ sở cho các span ảnh hưởng đến từng ký tự riêng lẻ bất kể vị trí của chúng trong các dòng. Điều này bao gồm ForegroundColorSpan (màu văn bản), RelativeSizeSpan (kích thước tương đối), StyleSpan (in đậm và in nghiêng), UnderlineSpan (gạch chân) và các loại khác. Span ký tự được áp dụng cho từng ký tự trong phạm vi được chỉ định một cách riêng lẻ.
ParagraphStyle là giao diện cho các span ảnh hưởng đến toàn bộ đoạn văn. Đại diện nổi tiếng nhất là AlignmentSpan, căn chỉnh toàn bộ đoạn văn sang trái, giữa hoặc phải. Span đoạn văn phải bao phủ toàn bộ đoạn văn; nếu không, Android bỏ qua việc áp dụng chúng. Giới hạn này là vì căn chỉnh hoặc thụt lề chỉ có ý nghĩa đối với toàn bộ khối văn bản.
Cờ Span là bốn hằng số xác định hành vi của span khi văn bản được chèn hoặc xóa tại ranh giới phạm vi của nó. SPAN_EXCLUSIVE_EXCLUSIVE giữ span hoạt động chỉ trong ranh giới ban đầu, SPAN_INCLUSIVE_INCLUSIVE mở rộng nó khi văn bản được thêm vào ranh giới. SPAN_EXCLUSIVE_INCLUSIVE và SPAN_INCLUSIVE_EXCLUSIVE cung cấp hành vi hỗn hợp cho đầu và cuối phạm vi.
| Cờ | Chèn bên trái | Chèn bên phải |
|---|---|---|
| SPAN_EXCLUSIVE_EXCLUSIVE | không bao gồm | không bao gồm |
| SPAN_INCLUSIVE_INCLUSIVE | bao gồm | bao gồm |
| SPAN_EXCLUSIVE_INCLUSIVE | không bao gồm | bao gồm |
| SPAN_INCLUSIVE_EXCLUSIVE | bao gồm | không bao gồm |
Việc chọn cờ chính xác rất quan trọng đối với văn bản Editable trong EditText, nơi người dùng có thể chèn và xóa ký tự. Đối với TextView chỉ đọc, thường sử dụng SPAN_EXCLUSIVE_EXCLUSIVE — kiểu chỉ áp dụng cho phạm vi ban đầu và không mở rộng với các thay đổi theo chương trình.
Android SDK cung cấp hơn 25 lớp span tích hợp bao phủ hầu hết các tác vụ tạo kiểu văn bản. Mỗi lớp triển khai giao diện CharacterStyle hoặc ParagraphStyle và chấp nhận tham số thông qua hàm tạo của nó. Tất cả các lớp đều nằm trong gói android.text.style và có sẵn mà không cần thêm phụ thuộc.
ForegroundColorSpan đặt màu văn bản cho một phạm vi được chỉ định, chấp nhận màu ở định dạng int. BackgroundColorSpan tô màu nền phía sau văn bản, hữu ích để làm nổi bật kết quả tìm kiếm. AbsoluteSizeSpan đặt kích thước phông chữ chính xác bằng pixel, RelativeSizeSpan áp dụng hệ số nhân so với kích thước văn bản cơ sở trong TextView.
StyleSpan chấp nhận các hằng số Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC hoặc BOLD_ITALIC và thay đổi phông chữ của các ký tự. UnderlineSpan thêm gạch chân, StrikethroughSpan thêm gạch ngang. SuperscriptSpan và SubscriptSpan tạo chỉ số trên và chỉ số dưới. TypefaceSpan cho phép đặt phông chữ tùy chỉnh thông qua đối tượng Typeface cho một phạm vi văn bản.
ClickableSpan là một lớp trừu tượng để tạo các đoạn văn bản có thể nhấp. Khi chạm, phương thức onClick() được gọi. Để các nhấp chuột hoạt động, TextView phải có setMovementMethod(LinkMovementMethod.getInstance()). URLSpan là một lớp con của ClickableSpan cho siêu liên kết với tự động mở trình duyệt. ClickableSpan thường được kết hợp với ForegroundColorSpan để làm nổi bật trực quan liên kết bằng màu xanh lam.
val spannable = SpannableString("Open developer.android.com")
spannable.setSpan(
URLSpan("https://developer.android.com"),
9, 31, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannable.setSpan(
ForegroundColorSpan(Color.BLUE),
9, 31, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
textView.movementMethod = LinkMovementMethod.getInstance()
Nếu không có LinkMovementMethod, các nhấp chuột trên URLSpan sẽ không được xử lý. MovementMethod chịu trách nhiệm chặn các sự kiện chạm và tìm ClickableSpan tại vị trí chạm. Span màu làm cho liên kết hiển thị với người dùng.
Làm việc với SpannableString bắt đầu bằng việc tạo một thể hiện từ một chuỗi văn bản và tuần tự áp dụng các span thông qua phương thức setSpan(). Phương thức này chấp nhận bốn tham số: đối tượng span, vị trí bắt đầu, vị trí kết thúc và các cờ. Sau khi đặt tất cả các span, đối tượng được truyền cho TextView thông qua setText().
Hãy tạo một chuỗi nơi từ đầu tiên có màu đỏ và được phóng to. Để làm điều này, chúng ta sử dụng ForegroundColorSpan cho màu sắc và RelativeSizeSpan cho tỷ lệ. Cả hai span được áp dụng cho cùng một phạm vi một cách độc lập — thứ tự gọi setSpan không quan trọng.
val text = "Header: remaining text"
val spannable = SpannableString(text)
val colorSpan = ForegroundColorSpan(Color.RED)
val sizeSpan = RelativeSizeSpan(1.5f)
spannable.setSpan(colorSpan, 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(sizeSpan, 0, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannable
Các ký tự từ vị trí 0 đến 9 nhận cả hai kiểu cùng lúc. TextView tự động áp dụng tất cả các span trong quá trình kết xuất — không cần gọi thêm. RelativeSizeSpan với hệ số nhân 1.5f tăng kích thước phông chữ lên 50% so với cơ sở.
Html.fromHtml() tạo một đối tượng Spanned từ một chuỗi HTML, nhưng tập hợp các thẻ được hỗ trợ bị giới hạn. SpannableString cho phép kiểm soát hoàn toàn mọi thuộc tính mà không bị giới hạn HTML. Nếu bạn cần chuyển đổi HTML thành span và sau đó thêm kiểu tùy chỉnh, bạn có thể sử dụng Html.fromHtml() làm cơ sở và sau đó bổ sung bằng span thông qua setSpan().
val htmlText = Html.fromHtml(
"<b>Important:</b> check the data",
Html.FROM_HTML_MODE_LEGACY
)
val spannable = SpannableString(htmlText)
spannable.setSpan(
ForegroundColorSpan(Color.RED),
0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannable
Kết quả — văn bản “Quan trọng:” sẽ được in đậm (từ HTML) và màu đỏ (từ span). Cách tiếp cận này thuận tiện khi làm việc với nội dung máy chủ nơi một phần định dạng được chỉ định trong HTML và một phần được thêm vào phía máy khách theo chương trình.
SpannableString cho phép áp dụng số lượng không giới hạn các span vào cùng một phạm vi hoặc phạm vi chồng lấn. Kết hợp các span là một lợi thế chính so với đánh dấu HTML, nơi các thẻ lồng nhau có thể xung đột. Các span độc lập và được áp dụng tuần tự trong quá trình kết xuất.
Ví dụ, bạn có thể làm cho một đoạn văn bản đồng thời in đậm, màu đỏ và có thể nhấp. Để làm điều này, hãy tạo ba span — StyleSpan, ForegroundColorSpan và ClickableSpan — và áp dụng mỗi span vào cùng một phạm vi. Thứ tự áp dụng không ảnh hưởng đến kết quả, vì mỗi span chịu trách nhiệm cho thuộc tính văn bản riêng của nó.
Nếu các span của các loại khác nhau chồng lấn một phần, mỗi span hoạt động độc lập trong ranh giới riêng của nó. ForegroundColorSpan trên phạm vi 0–10 và StyleSpan(BOLD) trên phạm vi 5–15 sẽ cho văn bản đỏ in đậm trên đoạn 5–10 và chỉ in đậm trên 10–15. Không có xung đột nào phát sinh vì mỗi span sửa đổi thuộc tính riêng của nó trong quá trình kết xuất.
Phương thức getSpans(int start, int end, Class type) trả về một mảng các span trong phạm vi được chỉ định. Điều này hữu ích để kiểm tra kiểu nào đã được áp dụng hoặc để xóa các span cụ thể. Với nextSpanTransition(), bạn có thể lặp qua các ranh giới thay đổi span — đây là nền tảng cho các triển khai TextView tùy chỉnh cần biết nơi kiểu thay đổi.
SpannableStringBuilder là một lớp để xây dựng từng bước văn bản có kiểu với khả năng chèn, thay thế và xóa các đoạn. Không giống như SpannableString được tạo từ một chuỗi có sẵn và bất biến, Builder cho phép thêm các phần văn bản tuần tự và gán kiểu ngay lập tức. Đây là lựa chọn lý tưởng cho các tin nhắn tổng hợp: nhật ký, trò chuyện, tiêu đề tin tức với nhãn động.
Builder triển khai các giao diện Spannable và Editable, làm cho nó tương thích với EditText. Người dùng có thể chỉnh sửa văn bản và các kiểu được bảo toàn và dịch chuyển chính xác khi chèn ký tự mới. SpannableString, vì bất biến, không phù hợp cho các trường có thể chỉnh sửa.
Phương thức append() trả về chính builder, cho phép chuỗi phương thức. Sau khi thêm văn bản, các span được áp dụng thông qua setSpan(). Các vị trí được chỉ định tương đối so với độ dài hiện tại của builder. Insert() và replace() cũng có sẵn để kiểm soát nội dung chính xác hơn.
val builder = SpannableStringBuilder()
.append("New ")
.append("comment")
val blue = ForegroundColorSpan(Color.BLUE)
val gray = ForegroundColorSpan(Color.GRAY)
builder.setSpan(blue, 0, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
builder.setSpan(gray, 6, 17, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = builder
Văn bản “Mới ” được tô màu xanh lam và “bình luận” được tô màu xám. Khi chèn thêm ký tự giữa chúng, span sẽ không ảnh hưởng đến văn bản mới nhờ các cờ EXCLUSIVE. Builder tự động điều chỉnh các chỉ mục nội bộ khi sửa đổi.
Sử dụng số lượng lớn span ảnh hưởng đến hiệu suất kết xuất của TextView. Mỗi span gọi phương thức updateDrawState() hoặc updateMeasureState() trên mỗi lần vẽ lại. Khuyến nghị giới hạn số lượng span trên mỗi TextView ở mức 50–100 để có hiệu suất thoải mái trên các thiết bị tầm trung. Các span thay đổi kích thước văn bản (RelativeSizeSpan, AbsoluteSizeSpan) yêu cầu tính toán lại bố cục mỗi khi thay đổi, tốn kém hơn đáng kể so với các span chỉ màu sắc hoặc gạch chân.
TextAppearanceSpan cho phép áp dụng toàn bộ tập hợp kiểu từ tài nguyên XML android:textAppearance chỉ với một lần gọi setSpan(). Thay vì ba span riêng biệt (màu sắc, kích thước, phông chữ), một TextAppearanceSpan duy nhất với tham chiếu kiểu được sử dụng. Điều này giảm số lượng đối tượng và đơn giản hóa việc bảo trì — thay đổi kiểu trong tài nguyên tự động áp dụng cho tất cả văn bản sử dụng span này.
Tạo một thể hiện span mới cho mỗi setSpan() gây thêm tải cho bộ thu gom rác. Tối ưu nhất là tạo các đối tượng span hằng số nếu chúng được sử dụng nhiều lần. Ví dụ, ForegroundColorSpan(Color.RED) có thể được lưu trữ trong một đối tượng companion và tái sử dụng. Tuy nhiên, các span có trạng thái (ClickableSpan với các trình xử lý khác nhau) phải được tạo riêng cho từng trường hợp.
Để chẩn đoán vấn đề hiệu suất span, hãy sử dụng Layout Inspector trong Android Studio và trình hồ sơ GPU. Nếu TextView có nhiều span bị chậm đáng kể khi cuộn, hãy cân nhắc thay thế một số span bằng kiểu tĩnh thông qua TextAppearanceSpan hoặc giảm số lượng span bằng cách kết hợp các thuộc tính trong các triển khai UpdateAppearance tùy chỉnh.
Câu hỏi thường gặp
String là một chuỗi ký tự bất biến không hỗ trợ kiểu. SpannableString lưu trữ các ký tự giống nhau nhưng bổ sung chứa một mảng các đối tượng Span với thông tin định dạng. TextView xác định loại CharSequence được truyền và áp dụng các span vào các phạm vi tương ứng trong quá trình kết xuất. String bỏ qua mọi thuộc tính kiểu và hiển thị dưới dạng văn bản thuần.
Phương thức removeSpan(Object span) xóa một span cụ thể. Để làm sạch hoàn toàn, hãy gọi getSpans(0, length, Object::class.java), trả về một mảng tất cả các span, sau đó xóa từng span qua removeSpan. Thay vào đó, hãy tạo một SpannableString(text.toString()) mới không có span. SpannableStringBuilder có phương thức clear() xóa cả văn bản và span.
SpannableString hoạt động trong EditText, nhưng đối với văn bản có thể chỉnh sửa, SpannableStringBuilder triển khai Editable được ưu tiên hơn. EditText yêu cầu giao diện Editable để theo dõi thay đổi. Nếu bạn truyền SpannableString cho EditText, văn bản sẽ hiển thị với kiểu, nhưng trong quá trình chỉnh sửa, Android sẽ chuyển đổi nó thành Editable, điều này có thể đặt lại một số span.
Trong Jetpack Compose, các span của Android SDK không được sử dụng trực tiếp. Thay vào đó, Compose cung cấp AnnotatedString, tương đương riêng của SpannableString với các khả năng tương tự: SpanStyle cho kiểu ký tự riêng lẻ và ParagraphStyle cho đoạn văn. Việc chuyển đổi SpannableString thành AnnotatedString có thể thực hiện qua buildAnnotatedString với vòng lặp qua các span.
Tạo một lớp mở rộng CharacterStyle và ghi đè phương thức updateDrawState(TextPaint tp). Bên trong phương thức, sửa đổi các thuộc tính TextPaint: màu sắc, độ rộng nét, hiệu ứng. Đối với các thay đổi metric, sử dụng UpdateLayout hoặc MetricAffectingSpan. Các span tùy chỉnh được áp dụng qua setSpan() giống như các span tích hợp.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm