SpannableString: khái niệm, các lớp kiểu và cách hoạt động

Tác giả: IT Sectr Đã đăng: 2026-07-23 Thời gian đọc: 10 phút

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ớp Android để tạo kiểu văn bản qua đối tượng Span, không thay đổi sau khi tạo
  • Span được chia thành CharacterStyle (ký tự riêng lẻ) và ParagraphStyle (toàn bộ đoạn văn)
  • ForegroundColorSpan thay đổi màu văn bản, StyleSpan chịu trách nhiệm in đậm và in nghiêng
  • SpannableStringBuilder — phiên bản có thể thay đổi để xây dựng văn bản động với hỗ trợ Editable
  • Cờ Span xác định hành vi của span khi chèn hoặc xóa văn bản tại ranh giới phạm vi

SpannableString là gì?

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.

Khác biệt với CharSequence

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.

Cấu trúc bên trong

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.

Các loại Span: CharacterStyle và ParagraphStyle

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

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áiChèn bên phải
SPAN_EXCLUSIVE_EXCLUSIVEkhông bao gồmkhông bao gồm
SPAN_INCLUSIVE_INCLUSIVEbao gồmbao gồm
SPAN_EXCLUSIVE_INCLUSIVEkhông bao gồmbao gồm
SPAN_INCLUSIVE_EXCLUSIVEbao gồmkhô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.

Các lớp Span chính trong Android SDK

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.

Span màu sắc và nền

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.

Span phông chữ

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.

Span tương tác

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.

kotlin
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.

Cách sử dụng SpannableString trong mã

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().

Màu sắc và kích thước văn bản

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.

kotlin
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ở.

Kết hợp với thẻ HTML

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().

kotlin
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.

Kết hợp nhiều Span

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ó.

Chồng lấn span của các loại khác nhau

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.

Lấy danh sách span

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 để xây dựng văn bản

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 SpannableEditable, 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.

Chuỗi phương thức

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.

kotlin
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.

Hiệu suất và tối ưu hóa Span

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

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ái sử dụng đối tượng

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.

Đo lường và hồ sơ

Để 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

SpannableString khác với String thông thường như thế nào?

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.

Làm thế nào để xóa tất cả span khỏi SpannableString?

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 có hoạt động trong EditText không?

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.

Có thể sử dụng SpannableString trong Compose không?

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.

Làm thế nào để tạo Span tùy chỉnh?

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

  • SpannableString — lớp Android để tạo kiểu văn bản qua đối tượng Span với liên kết phạm vi ký tự
  • CharacterStyle ảnh hưởng đến từng ký tự riêng lẻ, ParagraphStyle ảnh hưởng đến toàn bộ đoạn văn
  • Các span chính: ForegroundColorSpan, StyleSpan, RelativeSizeSpan, URLSpan, ClickableSpan
  • SpannableStringBuilder — phiên bản có thể thay đổi để xây dựng văn bản động với Editable
  • Kết hợp span an toàn — mỗi span thay đổi thuộc tính riêng của nó một cách độc lập
  • Cờ Span kiểm soát hành vi span khi chèn văn bản tại ranh giới
  • Tối ưu hóa: sử dụng TextAppearanceSpan cho nhóm kiểu và giới hạn span ở 100

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.

Thảo luận dự án

Đọc thêm