Vùng

Thử cách sử dụng Compose
Jetpack Compose là bộ công cụ giao diện người dùng được đề xuất cho Android. Tìm hiểu cách sử dụng văn bản trong Compose.

Span là các đối tượng đánh dấu mạnh mẽ mà bạn có thể dùng để tạo kiểu cho văn bản ở cấp ký tự hoặc đoạn. Bằng cách đính kèm span vào đối tượng văn bản, bạn có thể thay đổi văn bản theo nhiều cách, bao gồm cả việc thêm màu, giúp văn bản có thể nhấp vào, điều chỉnh kích thước văn bản và vẽ văn bản theo cách tuỳ chỉnh. Span cũng có thể thay đổi thuộc tính TextPaint, vẽ trên Canvas và thay đổi bố cục văn bản.

Android cung cấp nhiều loại span cho phép tạo nhiều kiểu văn bản phổ biến. Bạn cũng có thể tạo các span của riêng mình để tạo kiểu theo ý thích.

Tạo và áp dụng span

Để tạo span, bạn có thể sử dụng một trong các lớp được liệt kê trong bảng sau. Các lớp khác nhau dựa trên việc văn bản đó có thể thay đổi hay không, mã đánh dấu văn bản có thể thay đổi hay không và cấu trúc dữ liệu cơ bản nào chứa dữ liệu span.

Lớp Văn bản có thể thay đổi Mã đánh dấu có thể thay đổi Cấu trúc dữ liệu
SpannedString Không Không Mảng tuyến tính
SpannableString Không Mảng tuyến tính
SpannableStringBuilder Cây khoảng (interval tree)

Cả ba lớp đều mở rộng giao diện Spanned. SpannableStringSpannableStringBuilder cũng mở rộng giao diện Spannable.

Sau đây là cách quyết định nên sử dụng định dạng nào:

  • Nếu không cần sửa đổi văn bản hoặc mã đánh dấu sau khi tạo, hãy sử dụng SpannedString.
  • Nếu bạn cần đính kèm một số ít span vào một đối tượng văn bản và văn bản đó ở chế độ chỉ đọc, hãy sử dụng SpannableString.
  • Nếu cần sửa đổi văn bản sau khi tạo và cần đính kèm các span vào văn bản, hãy sử dụng SpannableStringBuilder.
  • Nếu bạn cần đính kèm nhiều span vào một đối tượng văn bản, bất kể văn bản đó có ở chế độ chỉ đọc hay không, hãy sử dụng SpannableStringBuilder.

Để áp dụng span, gọi setSpan(Object _what_, int _start_, int _end_, int _flags_) trên đối tượng Spannable. Tham số what (cái gì) đề cập đến span (khoảng) bạn đang áp dụng cho văn bản, còn các tham số start (bắt đầu) và end (kết thúc) cho biết phần văn bản mà bạn đang áp dụng span.

Nếu bạn chèn văn bản vào bên trong ranh giới của một span, span đó sẽ tự động mở rộng để bao gồm văn bản đã chèn. Khi chèn văn bản tại phạm vi span (tức là ở chỉ mục start (bắt đầu) hoặc end (kết thúc), tham số flags sẽ xác định xem span có mở rộng để bao gồm văn bản được chèn hay không. Sử dụng cờ Spannable.SPAN_EXCLUSIVE_INCLUSIVE để bao gồm văn bản được chèn và sử dụng Spannable.SPAN_EXCLUSIVE_EXCLUSIVE để loại trừ văn bản được chèn.

Ví dụ sau đây cho biết cách đính kèm ForegroundColorSpan vào một chuỗi:

Kotlin

val spannable = SpannableStringBuilder("Text is spantastic!")
spannable.setSpan(
    ForegroundColorSpan(Color.RED),
    8, // start
    12, // end
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)

Java

SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!");
spannable.setSpan(
    new ForegroundColorSpan(Color.RED),
    8, // start
    12, // end
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
);
Hình ảnh cho thấy một phần văn bản màu xám, một phần có màu đỏ.
Hình 1. Văn bản được tạo kiểu bằng ForegroundColorSpan.

Vì span được đặt bằng Spannable.SPAN_EXCLUSIVE_INCLUSIVE, nên span sẽ mở rộng để bao gồm văn bản được chèn tại ranh giới span, như minh hoạ trong ví dụ sau:

Kotlin

val spannable = SpannableStringBuilder("Text is spantastic!")
spannable.setSpan(
    ForegroundColorSpan(Color.RED),
    8, // start
    12, // end
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
spannable.insert(12, "(& fon)")

Java

SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!");
spannable.setSpan(
    new ForegroundColorSpan(Color.RED),
    8, // start
    12, // end
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
);
spannable.insert(12, "(& fon)");
Hình ảnh cho thấy cách span chứa nhiều văn bản hơn khi sử dụng SPAN_EXCLUSIVE_INCLUSIVE.
Hình 2. span sẽ mở rộng để chứa văn bản bổ sung khi sử dụng Spannable.SPAN_EXCLUSIVE_INCLUSIVE.

Bạn có thể đính kèm nhiều span cho cùng một văn bản. Ví dụ sau cho biết cách tạo văn bản in đậm và có màu đỏ:

Kotlin

val spannable = SpannableString("Text is spantastic!")
spannable.setSpan(ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(
    StyleSpan(Typeface.BOLD),
    8,
    spannable.length,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)

Java

SpannableString spannable = new SpannableString("Text is spantastic!");
spannable.setSpan(
    new ForegroundColorSpan(Color.RED),
    8, 12,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
);
spannable.setSpan(
    new StyleSpan(Typeface.BOLD),
    8, spannable.length(),
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
);
Hình ảnh cho thấy một văn bản có nhiều span: "ForegroundColorSpan(Color.RED)" và "StyleSpan(BOLD)"
Hình 3. Văn bản có nhiều span: ForegroundColorSpan(Color.RED)StyleSpan(BOLD).

Loại span trên Android

Android cung cấp hơn 20 loại span trong gói android.text.style. Android phân loại span theo hai cách chính:

  • Ảnh hưởng của span ảnh hưởng đến văn bản: span có thể ảnh hưởng đến giao diện văn bản hoặc chỉ số văn bản.
  • Phạm vi span: một số span có thể được áp dụng cho các ký tự riêng lẻ, trong khi một số span khác phải được áp dụng cho toàn bộ đoạn văn.
Hình ảnh thể hiện các danh mục span khác nhau
Hình 4. Danh mục các span trên Android.

Phần sau đây sẽ mô tả chi tiết hơn về những danh mục này.

Các span ảnh hưởng đến giao diện văn bản

Một số khoảng áp dụng ở cấp ký tự sẽ ảnh hưởng đến giao diện văn bản, chẳng hạn như thay đổi văn bản hoặc màu nền và thêm dấu gạch dưới hoặc dấu gạch ngang. Các span này mở rộng lớp CharacterStyle.

Ví dụ về mã sau đây cho biết cách áp dụng UnderlineSpan để gạch chân văn bản:

Kotlin

val string = SpannableString("Text with underline span")
string.setSpan(UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

Java

SpannableString string = new SpannableString("Text with underline span");
string.setSpan(new UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hình ảnh minh hoạ cách gạch chân văn bản bằng `underlineSpan`
Hình 5. Văn bản được gạch chân bằng một UnderlineSpan.

Các span chỉ ảnh hưởng đến giao diện văn bản sẽ kích hoạt thao tác vẽ lại văn bản mà không kích hoạt việc tính toán lại bố cục. Các span này sẽ triển khai UpdateAppearance và mở rộng CharacterStyle. Các lớp con CharacterStyle xác định cách vẽ văn bản bằng cách cấp quyền truy cập để cập nhật TextPaint.

Các span ảnh hưởng đến chỉ số văn bản

Các span khác áp dụng ở cấp ký tự sẽ ảnh hưởng đến chỉ số văn bản, chẳng hạn như chiều cao dòng và kích thước văn bản. Các span này mở rộng lớp MetricAffectingSpan.

Ví dụ về mã sau đây sẽ tạo một RelativeSizeSpan giúp tăng kích thước văn bản thêm 50%:

Kotlin

val string = SpannableString("Text with relative size span")
string.setSpan(RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

Java

SpannableString string = new SpannableString("Text with relative size span");
string.setSpan(new RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hình ảnh cho thấy cách sử dụng InteractionsSizeSpan
Hình 6. Văn bản được phóng to bằng RelativeSizeSpan.

Việc áp dụng span ảnh hưởng đến chỉ số văn bản sẽ khiến đối tượng quan sát đo lường lại văn bản để có bố cục và hiển thị chính xác (ví dụ: việc thay đổi kích thước văn bản có thể khiến các từ xuất hiện trên nhiều dòng). Việc áp dụng span trước đó sẽ kích hoạt việc đo lường, tính toán lại bố cục văn bản và vẽ lại văn bản.

Các span ảnh hưởng đến chỉ số văn bản mở rộng lớp MetricAffectingSpan, một lớp trừu tượng cho phép các lớp con xác định cách span ảnh hưởng đến việc đo lường văn bản bằng cách cấp quyền truy cập vào TextPaint. Vì MetricAffectingSpan mở rộng CharacterSpan, nên các lớp con sẽ ảnh hưởng đến giao diện của văn bản ở cấp ký tự.

Các span ảnh hưởng đến đoạn văn bản

span cũng có thể ảnh hưởng đến văn bản ở cấp đoạn, chẳng hạn như thay đổi cách căn chỉnh hoặc lề của một khối văn bản. Các span ảnh hưởng đến toàn bộ đoạn văn sẽ mở rộng giao diện ParagraphStyle. Để sử dụng các span này, bạn đính kèm chúng vào toàn bộ đoạn văn bản, ngoại trừ ký tự kết thúc dòng mới. Nếu bạn cố gắng áp dụng một span cho một nội dung không phải toàn bộ đoạn văn bản, thì Android sẽ không áp dụng span đó.

Hình 8 thể hiện cách Android phân tách các đoạn văn bản.

Hình 7. Trong Android, đoạn văn bản kết thúc bằng một ký tự dòng mới (\n).

Ví dụ về mã sau đây áp dụng QuoteSpan cho một đoạn. Xin lưu ý rằng nếu bạn đính kèm span vào bất kỳ vị trí nào khác ngoài vị trí đầu hoặc cuối của một đoạn, thì Android sẽ hoàn toàn không áp dụng kiểu này.

Kotlin

spannable.setSpan(QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

Java

spannable.setSpan(new QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Hình ảnh cho thấy ví dụ về QuoteSpan
Hình 8. Đã áp dụng QuoteSpan cho một đoạn.

Tạo span tuỳ chỉnh

Nếu cần thêm chức năng khác với chức năng được cung cấp trong các span Android hiện tại, bạn có thể triển khai một span tuỳ chỉnh. Khi triển khai span của riêng bạn, hãy quyết định xem span ảnh hưởng đến văn bản ở cấp ký tự hay cấp đoạn văn bản, đồng thời ảnh hưởng đến bố cục hoặc giao diện của văn bản. Điều này giúp bạn xác định những lớp cơ sở bạn có thể mở rộng và những giao diện bạn có thể cần triển khai. Hãy tham khảo bảng sau:

Trường hợp Lớp hoặc giao diện
Span ảnh hưởng đến văn bản ở cấp ký tự. CharacterStyle
Span ảnh hưởng đến giao diện văn bản. UpdateAppearance
Span ảnh hưởng đến các chỉ số văn bản. UpdateLayout
Span ảnh hưởng đến văn bản ở cấp đoạn văn bản. ParagraphStyle

Ví dụ: nếu bạn cần triển khai một span tuỳ chỉnh để sửa đổi kích thước và màu sắc văn bản, hãy mở rộng RelativeSizeSpan. Thông qua tính kế thừa, RelativeSizeSpan mở rộng CharacterStyle và triển khai 2 giao diện Update. Vì lớp này đã cung cấp các lệnh gọi lại cho updateDrawStateupdateMeasureState, nên bạn có thể ghi đè các lệnh gọi lại này để triển khai hành vi tuỳ chỉnh. Mã sau đây tạo một span tuỳ chỉnh mở rộng RelativeSizeSpan và ghi đè lệnh gọi lại updateDrawState để đặt màu của TextPaint:

Kotlin

class RelativeSizeColorSpan(
    size: Float,
    @ColorInt private val color: Int
) : RelativeSizeSpan(size) {
    override fun updateDrawState(textPaint: TextPaint) {
        super.updateDrawState(textPaint)
        textPaint.color = color
    }
}

Java

public class RelativeSizeColorSpan extends RelativeSizeSpan {
    private int color;
    public RelativeSizeColorSpan(float spanSize, int spanColor) {
        super(spanSize);
        color = spanColor;
    }
    @Override
    public void updateDrawState(TextPaint textPaint) {
        super.updateDrawState(textPaint);
        textPaint.setColor(color);
    }
}

Ví dụ này minh hoạ cách tạo span tuỳ chỉnh. Bạn có thể đạt được cùng một hiệu quả bằng cách áp dụng RelativeSizeSpanForegroundColorSpan cho văn bản.

Kiểm thử sử dụng span

Giao diện Spanned cho phép bạn đặt cả span cũng như truy xuất span từ văn bản. Khi kiểm thử, hãy triển khai kiểm thử Android JUnit để xác minh rằng span chính xác được thêm vào đúng vị trí. Ứng dụng mẫu Tạo kiểu văn bản chứa một span áp dụng mã đánh dấu cho dấu đầu dòng bằng cách đính kèm BulletPointSpan vào văn bản. Mã ví dụ sau đây cho thấy cách kiểm tra xem các dấu đầu dòng có xuất hiện như dự kiến hay không:

Kotlin

@Test fun textWithBulletPoints() {
   val result = builder.markdownToSpans("Points\n* one\n+ two")

   // Check whether the markup tags are removed.
   assertEquals("Points\none\ntwo", result.toString())

   // Get all the spans attached to the SpannedString.
   val spans = result.getSpans<Any>(0, result.length, Any::class.java)

   // Check whether the correct number of spans are created.
   assertEquals(2, spans.size.toLong())

   // Check whether the spans are instances of BulletPointSpan.
   val bulletSpan1 = spans[0] as BulletPointSpan
   val bulletSpan2 = spans[1] as BulletPointSpan

   // Check whether the start and end indices are the expected ones.
   assertEquals(7, result.getSpanStart(bulletSpan1).toLong())
   assertEquals(11, result.getSpanEnd(bulletSpan1).toLong())
   assertEquals(11, result.getSpanStart(bulletSpan2).toLong())
   assertEquals(14, result.getSpanEnd(bulletSpan2).toLong())
}

Java

@Test
public void textWithBulletPoints() {
    SpannedString result = builder.markdownToSpans("Points\n* one\n+ two");

    // Check whether the markup tags are removed.
    assertEquals("Points\none\ntwo", result.toString());

    // Get all the spans attached to the SpannedString.
    Object[] spans = result.getSpans(0, result.length(), Object.class);

    // Check whether the correct number of spans are created.
    assertEquals(2, spans.length);

    // Check whether the spans are instances of BulletPointSpan.
    BulletPointSpan bulletSpan1 = (BulletPointSpan) spans[0];
    BulletPointSpan bulletSpan2 = (BulletPointSpan) spans[1];

    // Check whether the start and end indices are the expected ones.
    assertEquals(7, result.getSpanStart(bulletSpan1));
    assertEquals(11, result.getSpanEnd(bulletSpan1));
    assertEquals(11, result.getSpanStart(bulletSpan2));
    assertEquals(14, result.getSpanEnd(bulletSpan2));
}

Để xem thêm các ví dụ kiểm thử, hãy xem MarkdownBuilderTest trên GitHub.

Kiểm thử span tuỳ chỉnh

Khi kiểm thử span, hãy xác minh rằng TextPaint chứa các bản sửa đổi dự kiến và các phần tử chính xác xuất hiện trên Canvas. Ví dụ: hãy cân nhắc việc triển khai một span tuỳ chỉnh để thêm dấu đầu dòng vào một số văn bản. Dấu đầu dòng có kích thước và màu sắc được chỉ định, đồng thời có một khoảng trống giữa lề bên trái của vùng có thể vẽ và dấu đầu dòng.

Bạn có thể kiểm thử hành vi của lớp này bằng cách triển khai kiểm thử AndroidJUnit để kiểm tra những việc sau:

  • Nếu bạn áp dụng đúng span, một dấu đầu dòng có kích thước và màu sắc đã chỉ định sẽ xuất hiện trên canvas và khoảng trống thích hợp sẽ nằm giữa lề bên trái và dấu đầu dòng.
  • Nếu không áp dụng span thì sẽ không có hành vi tuỳ chỉnh nào xuất hiện.

Bạn có thể xem cách triển khai các thử nghiệm này trong mẫu TextStyle trên GitHub.

Bạn có thể kiểm thử hoạt động tương tác với Canvas bằng cách mô phỏng canvas, chuyển đối tượng mô phỏng đến phương thức drawLeadingMargin() và xác minh rằng các phương thức đúng được gọi với đúng tham số.

Bạn có thể tìm thêm các mẫu kiểm thử span trong BulletPointSpanTest.

Các phương pháp hay nhất để sử dụng span

Có một số cách tiết kiệm bộ nhớ để đặt văn bản trong TextView, tuỳ thuộc vào nhu cầu của bạn.

Đính kèm hoặc tách span mà không thay đổi văn bản cơ bản

TextView.setText() chứa nhiều phương thức nạp chồng có khả năng xử lý các span khác nhau. Ví dụ: bạn có thể đặt đối tượng văn bản Spannable bằng mã sau:

Kotlin

textView.setText(spannableObject)

Java

textView.setText(spannableObject);

Khi gọi phương thức nạp chồng setText() này, TextView sẽ tạo một bản sao của Spannable dưới dạng SpannedString và lưu bản sao đó trong bộ nhớ dưới dạng CharSequence. Tức là văn bản và các span không thể thay đổi được. Vì vậy, khi cần cập nhật văn bản hoặc các span, hãy tạo một đối tượng Spannable mới và gọi lại setText(). Thao tác này cũng sẽ kích hoạt hoạt động đo lường và vẽ lại bố cục.

Để cho biết rằng span phải có thể thay đổi, bạn có thể sử dụng setText(CharSequence text, TextView.BufferType type), như trong ví dụ sau:

Kotlin

textView.setText(spannable, BufferType.SPANNABLE)
val spannableText = textView.text as Spannable
spannableText.setSpan(
     ForegroundColorSpan(color),
     8, spannableText.length,
     SPAN_INCLUSIVE_INCLUSIVE
)

Java

textView.setText(spannable, BufferType.SPANNABLE);
Spannable spannableText = (Spannable) textView.getText();
spannableText.setSpan(
     new ForegroundColorSpan(color),
     8, spannableText.getLength(),
     SPAN_INCLUSIVE_INCLUSIVE);

Trong ví dụ này, tham số BufferType.SPANNABLE khiến TextView tạo ra một SpannableString và đối tượng CharSequence do TextView giữ lại hiện có mã đánh dấu có thể thay đổi và văn bản không thể thay đổi. Để cập nhật span, hãy truy xuất văn bản dưới dạng Spannable rồi cập nhật span nếu cần.

Khi bạn đính kèm, tách hoặc đặt lại vị trí span, TextView sẽ tự động cập nhật để phản ánh thay đổi trong văn bản. Nếu bạn thay đổi thuộc tính nội bộ của một span hiện có, hãy gọi invalidate() để thực hiện các thay đổi liên quan đến giao diện hoặc requestLayout() để thực hiện các thay đổi liên quan đến chỉ số.

Thiết lập văn bản nhiều lần trong TextView

Trong một số trường hợp, chẳng hạn như khi sử dụng RecyclerView.ViewHolder, bạn có thể muốn sử dụng lại TextView và thiết lập văn bản nhiều lần. Theo mặc định, bất kể bạn có đặt BufferType hay không, TextView sẽ tạo một bản sao của đối tượng CharSequence và lưu giữ đối tượng đó trong bộ nhớ. Việc này khiến tất cả nội dung cập nhật TextView đều có chủ ý – bạn không thể cập nhật đối tượng CharSequence gốc để cập nhật văn bản. Tức là mỗi lần bạn đặt văn bản mới, TextView sẽ tạo một đối tượng mới.

Nếu muốn kiểm soát nhiều hơn quá trình này và tránh việc tạo thêm đối tượng, bạn có thể triển khai Spannable.Factory của riêng mình và ghi đè newSpannable(). Thay vì tạo đối tượng văn bản mới, bạn có thể truyền và trả về CharSequence hiện có dưới dạng Spannable, như minh hoạ trong ví dụ sau:

Kotlin

val spannableFactory = object : Spannable.Factory() {
    override fun newSpannable(source: CharSequence?): Spannable {
        return source as Spannable
    }
}

Java

Spannable.Factory spannableFactory = new Spannable.Factory(){
    @Override
    public Spannable newSpannable(CharSequence source) {
        return (Spannable) source;
    }
};

Bạn phải sử dụng textView.setText(spannableObject, BufferType.SPANNABLE) khi đặt văn bản. Nếu không, nguồn CharSequence sẽ được tạo dưới dạng một thực thể Spanned và không thể truyền tới Spannable, khiến newSpannable() gửi ClassCastException.

Sau khi ghi đè newSpannable(), hãy yêu cầu TextView sử dụng Factory mới:

Kotlin

textView.setSpannableFactory(spannableFactory)

Java

textView.setSpannableFactory(spannableFactory);

Đặt đối tượng Spannable.Factory một lần, ngay sau khi bạn tham chiếu đến TextView. Nếu bạn đang sử dụng RecyclerView, hãy đặt đối tượng Factory khi bạn tăng cường khung hiển thị lần đầu tiên. Điều này giúp tránh việc tạo thêm đối tượng khi RecyclerView liên kết một mục mới với ViewHolder.

Thay đổi các thuộc tính của span nội bộ

Nếu chỉ cần thay đổi một thuộc tính nội bộ của một span có thể thay đổi, chẳng hạn như màu dấu đầu dòng trong một span dấu đầu dòng tuỳ chỉnh, thì bạn có thể tránh hao tổn tài nguyên khi gọi setText() nhiều lần bằng cách giữ lại thông tin tham chiếu đến span đó ngay khi được tạo. Khi cần sửa đổi span, bạn có thể sửa đổi tham chiếu, sau đó gọi invalidate() hoặc requestLayout() trên TextView, tuỳ thuộc vào loại thuộc tính bạn đã thay đổi.

Trong mã ví dụ sau đây, phương thức triển khai dấu đầu dòng tuỳ chỉnh có màu mặc định là màu đỏ và chuyển sang màu xám khi nhấn vào một nút:

Kotlin

class MainActivity : AppCompatActivity() {

    // Keeping the span as a field.
    val bulletSpan = BulletPointSpan(color = Color.RED)

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val spannable = SpannableString("Text is spantastic")
        // Setting the span to the bulletSpan field.
        spannable.setSpan(
            bulletSpan,
            0, 4,
            Spanned.SPAN_INCLUSIVE_INCLUSIVE
        )
        styledText.setText(spannable)
        button.setOnClickListener {
            // Change the color of the mutable span.
            bulletSpan.color = Color.GRAY
            // Color doesn't change until invalidate is called.
            styledText.invalidate()
        }
    }
}

Java

public class MainActivity extends AppCompatActivity {

    private BulletPointSpan bulletSpan = new BulletPointSpan(Color.RED);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        SpannableString spannable = new SpannableString("Text is spantastic");
        // Setting the span to the bulletSpan field.
        spannable.setSpan(bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        styledText.setText(spannable);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // Change the color of the mutable span.
                bulletSpan.setColor(Color.GRAY);
                // Color doesn't change until invalidate is called.
                styledText.invalidate();
            }
        });
    }
}

Sử dụng các hàm tiện ích Android KTX

Android KTX cũng chứa các hàm tiện ích giúp thao tác với span dễ dàng hơn. Để tìm hiểu thêm, hãy xem tài liệu về gói androidx.core.text.