Span

Span là các đối tượng đánh dấu hữu ích mà bạn có thể sử dụng để tạo kiểu cho văn bản ở cấp độ ký tự hoặc đoạn văn bả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 khác nhau, bao gồm thêm màu, làm cho văn bản có thể nhấp vào, chuyển tỷ lệ kích thước văn bản và vẽ văn bản tuỳ thích. Span cũng có thể thay đổi thuộc tính TextPaint, vẽ trên Canvas và thậm chí 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 bên dưới. Mỗi lớp sẽ khác nhau tuỳ thuộc vào việc tự thân văn bản có thể thay đổi được 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 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)

Sau đây là cách quyết định nên sử dụng loại 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 không cần đính kèm nhiều span vào một đối tượng văn bản và văn bản đó chỉ có thể đọ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 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 đó chỉ có thể đọc hay không, hãy sử dụng SpannableStringBuilder.

Tất cả các lớp này mở rộng giao diện Spanned. SpannableStringSpannableStringBuilder cũng mở rộng giao diện Spannable.

Để áp dụng span, gọi setSpan(Object _what_, int _start_, int _end_, int _flags_) trên đối tượng Spannable. Tham số what ám chỉ span áp dụng cho văn bản, trong khi tham số startend cho biết phần văn bản để áp dụng span.

Sau khi áp dụng span, nếu bạn chèn văn bản vào bên trong span, span đó sẽ tự động mở rộng để bao gồm văn bản đã chèn. Khi chèn văn bản ở các ranh giới span, tức là tại chỉ mục start hoặc end, tham số flags sẽ xác định span có cần mở rộng để gồm cả văn bản được chèn hay không. Sử dụng cờ Spannable.SPAN_EXCLUSIVE_INCLUSIVE để bao gồm cả 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ụ dưới đây hướng dẫn 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 1. Văn bản được tạo kiểu bằng ForegroundColorSpan.

Vì span được thiết lập 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ư trong ví dụ dưới đây:

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 2. Span sẽ mở rộng để bao gồm 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ụ bên dưới cho thấy cách tạo văn bản có cả chữ in đậm và 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 = 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 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:

  • Cách 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 từng ký tự, trong khi các span khác phải được áp dụng cho toàn bộ đoạn văn.

Hình 4. Danh mục span: ký tự so với đoạn văn bản, giao diện so với chỉ số

Các phần dưới đây 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

Các khoảng thời gian có thể ả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 gạch ngang chữ. Tất cả các span này mở rộng lớp CharacterStyle.

Mã ví dụ bên dưới cho thấy cách áp dụng UnderlineSpan để gạch dưới 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 5. Gạch dưới văn bản bằng 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 định nghĩa 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

Span cũng có thể ả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. Tất cả các span này mở rộng lớp MetricAffectingSpan.

Ví dụ về mã bên dưới sẽ tạo RelativeSizeSpan, cho phép tăng 50% kích thước văn bản:

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 6. Đặt kích thước văn bản bằng RelativeSizeSpan

Việc áp dụng span ảnh hưởng đến chỉ số văn bản sẽ làm cho đối tượng quan sát đo lường lại văn bản để tạo bố cục và kết xuất 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ừ bị nhảy dòng. Việc áp dụng span ở trên 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 này thường mở rộng lớp MetricAffectingSpan. Lớp này là một lớp trừu tượng cho phép các lớp con định nghĩa cách span ảnh hưởng 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 lớp CharacterSpan, các lớp con sẽ ảnh hưởng đến giao diện văn bản ở cấp ký tự.

Các span ảnh hưởng đến từng ký tự

Span có thể ảnh hưởng đến văn bản ở cấp ký tự. Ví dụ: bạn có thể cập nhật các phần tử ký tự như màu nền, kiểu hoặc kích thước. Các span ảnh hưởng đến các ký tự riêng lẻ sẽ mở rộng lớp CharacterStyle.

Mã ví dụ bên dưới đính kèm BackgroundColorSpan vào một tập hợp con ký tự trong văn bản:

Kotlin

val string = SpannableString("Text with a background color span")
string.setSpan(BackgroundColorSpan(color), 12, 28, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

Java

SpannableString string = new SpannableString("Text with a background color span");
string.setSpan(new BackgroundColorSpan(color), 12, 28, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

Hình 7. Áp dụng BackgroundColorSpan vào văn bản

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 toàn bộ 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. Khi sử dụng các span này, bạn phải đính kèm các span đó 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ố áp dụng một span cho một nội dung khác ngoài một đ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 8. Trong Android, đoạn văn bản kết thúc bằng ký tự dòng mới ('\n').

Ví dụ về đoạn mã sau đây áp dụng QuoteSpan cho toàn bộ đoạn văn. 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 và cuối đoạn văn bản, thì Android sẽ không tạo bất kỳ kiểu nào cả:

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 9. Áp dụng QuoteSpan cho đoạn văn bả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 riêng, bạn cần quyết định xem span có ảnh hưởng đến văn bản ở cấp ký tự hoặc đoạn văn bản cũng như có ảnh hưởng đến bố cục hoặc giao diện văn bản hay không. Điều này giúp bạn xác định những lớp cơ sở cũng như những giao diện có thể mở rộng. Hãy tham khảo bảng dưới đây:

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 văn bản ở cấp đoạn văn bản. ParagraphStyle
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

Ví dụ: nếu cần triển khai một span tuỳ chỉnh cho phép chỉnh sửa kích thước và màu sắc văn bản, bạn có thể mở rộng lớp RelativeSizeSpan. Vì lớp này đã cung cấp lệnh gọi lại cho updateDrawState và updateMeasureState, 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ã dưới đây sẽ tạo một span tuỳ chỉnh mở rộng lớp 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);
    }
}

Lưu ý rằng ví dụ này chỉ minh hoạ cách tạo span tuỳ chỉnh. Bạn có thể đạt được hiệu quả tương tự 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 thiết lập và truy xuất span từ văn bản. Khi kiểm thử, bạn nên triển khai kiểm thử Android JUnit để xác minh rằng span thích hợp đã được thêm vào đúng vị trí. Mẫu tạo kiểu văn bản chứa một span áp dụng đánh dấu cho dấu đầu dòng bằng cách đính kèm BulletPointSpan vào văn bản. Ví dụ về mã dưới đây hướng dẫn cách kiểm tra xem các dấu đầu dòng có xuất hiện đúng như dự kiến hay không:

Kotlin

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

   // check that the markup tags were 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 that the correct number of spans were created
   assertEquals(2, spans.size.toLong())

   // check that the spans are instances of BulletPointSpan
   val bulletSpan1 = spans[0] as BulletPointSpan
   val bulletSpan2 = spans[1] as BulletPointSpan

   // check that 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 that the markup tags were 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 that the correct number of spans were created
    assertEquals(2, spans.length);

    // check that the spans are instances of BulletPointSpan
    BulletPointSpan bulletSpan1 = (BulletPointSpan) spans[0];
    BulletPointSpan bulletSpan2 = (BulletPointSpan) spans[1];

    // check that 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.

Kiểm thử triển khai span tuỳ chỉnh

Khi kiểm thử span, bạn nên 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 nhất đị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 áp dụng đúng span, một dấu đầu dòng có kích thước và màu sắc nhất đị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

Xem cách triển khai kiểm thử trong mẫu TextStylingKotlin.

Bạn có thể kiểm tra hoạt động tương tác với Canvas bằng cách mô phỏng canvas, truyền đối tượng mô phỏng đến phương thức drawLeadingMargin(), đồng thời xác minh các phương thức đúng được gọi bằng các tham số đúng như ví dụ bên dưới đây:

Kotlin

val GAP_WIDTH = 5
val canvas = mock(Canvas::class.java)
val paint = mock(Paint::class.java)
val text = SpannableString("text")

@Test fun drawLeadingMargin() {
    val x = 10
    val dir = 15
    val top = 5
    val bottom = 7
    val color = Color.RED

    // Given a span that is set on a text
    val span = BulletPointSpan(GAP_WIDTH, color)
    text.setSpan(span, 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

    // When the leading margin is drawn
    span.drawLeadingMargin(canvas, paint, x, dir, top, 0, bottom,
            text, 0, 0, true, mock(Layout::class.java))

    // Check that the correct canvas and paint methods are called,
    // in the correct order
    val inOrder = inOrder(canvas, paint)
    // bullet point paint color is the one we set
    inOrder.verify(paint).color = color
    inOrder.verify(paint).style = eq<Paint.Style>(Paint.Style.FILL)
    // a circle with the correct size is drawn
    // at the correct location
    val xCoordinate = GAP_WIDTH.toFloat() + x.toFloat()
        + dir * BulletPointSpan.DEFAULT_BULLET_RADIUS
    val yCoordinate = (top + bottom) / 2f
    inOrder.verify(canvas)
           .drawCircle(
                eq(xCoordinate),
                eq(yCoordinate),
                eq(BulletPointSpan.DEFAULT_BULLET_RADIUS),
                eq(paint)
            )
    verify(canvas, never()).save()
    verify(canvas, never()).translate(
               eq(xCoordinate),
               eq(yCoordinate)
    )
}

Java

private int GAP_WIDTH = 5;
private Canvas canvas = mock(Canvas.class);
private Paint paint = mock(Paint.class);
private SpannableString text = new SpannableString("text");

@Test
public void drawLeadingMargin() {
    int x = 10;
    int dir = 15;
    int top = 5;
    int bottom = 7;
    int color = Color.RED;

    // Given a span that is set on a text
    BulletPointSpan span = new BulletPointSpan(GAP_WIDTH, color);
    text.setSpan(span, 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

    // When the leading margin is drawn
    span.drawLeadingMargin(canvas, paint, x, dir, top, 0, bottom, text, 0, 0, true, mock
            (Layout.class));

    // Check that the correct canvas and paint methods are called, in the correct order
    InOrder inOrder = inOrder(canvas, paint);
    inOrder.verify(paint).setColor(color);
    inOrder.verify(paint).setStyle(eq(Paint.Style.FILL));
    // a circle with the correct size is drawn
    // at the correct location
    int xCoordinate = (float)GAP_WIDTH + (float)x
        + dir * BulletPointSpan.BULLET_RADIUS;
    int yCoordinate = (top + bottom) / 2f;
    inOrder.verify(canvas)
           .drawCircle(
                eq(xCoordinate),
                eq(yCoordinate),
                eq(BulletPointSpan.BULLET_RADIUS),
                eq(paint));
    verify(canvas, never()).save();
    verify(canvas, never()).translate(
            eq(xCoordinate),
            eq(yCoordinate);
}

Bạn có thể tìm thêm các bài 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ớ hiệu quả để đặ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ý nhiều span. 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. Điều này có nghĩa là văn bản và các span không thể bị thay đổi, vì vậy, khi cần cập nhật văn bản hoặc span, bạn cần tạo lại đối tượng Spannable mới và gọi lại setText(). Thao tác này cũng sẽ kích hoạt việc đo lường và vẽ lại bố cục.

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

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, do tham số BufferType.SPANNABLE, TextView tạo ra một SpannableString và đối tượng CharSequence được TextView giữ lại hiện sẽ có mã đánh dấu có thể thay đổi và văn bản không thể thay đổi. Để cập nhật span, ta có thể 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. Tuy nhiên, lưu ý rằng nếu thay đổi thuộc tính nội bộ của span hiện có, bạn sẽ cần gọi invalidate() nếu thực hiện các thay đổi liên quan đến giao diện hoặc requestLayout() nếu 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ó thiết lập BufferType hay không, TextView đều tạo một bản sao của đối tượng CharSequence và lưu đối tượng này vào trong bộ nhớ. Việc này đảm bảo rằng tất cả các cập nhật TextView đều là có chủ ý — bạn không thể chỉ cập nhật đối tượng CharSequence gốc để cập nhật văn bản. Tức là mỗi khi thiết lập mộ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 quá trình này và tránh tạo thêm đối tượng, bạn có thể mở rộng giao diện Spannable.Factory của riêng mình và ghi đè newSpannable(). Thay vì tạo một đối tượng văn bản mới, bạn chỉ cần truyền và trả về CharSequence hiện tại dưới dạng Spannable như ví dụ minh hoạ dưới đây:

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;
    }
};

Lưu ý rằng bạn phải sử dụng textView.setText(spannableObject, BufferType.SPANNABLE) khi thiết lập văn bản. Nếu không, nguồn CharSequence sẽ được tạo dưới dạng thực thể Spanned và không thể truyền tới Spannable. Trong trường hợp đó, newSpannable() sẽ báo ngoại lệ ClassCastException.

Sau khi ghi đè newSpannable(), bạn cần phải chỉ định TextView sử dụng Factory mới:

Kotlin

textView.setSpannableFactory(spannableFactory)

Java

textView.setSpannableFactory(spannableFactory);

Hãy nhớ thiết lập đối tượng Spannable.Factory một lần ngay sau khi được tham chiếu đến TextView. Nếu đang sử dụng RecyclerView, thiết lập đối tượng Factory khi tăng cường khung nhìn lần đầu. Đ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 hạng mục mới với ViewHolder.

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

Nếu bạn 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, bạn có thể tránh tình trạng dư thừa nguồn lực từ việc gọi nhiều lần setText() bằng cách giữ lại thông tin tham chiếu đến span đó 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 mà bạn đã thay đổi.

Trong đoạn mã ví dụ bên dưới, dấu đầu dòng tuỳ chỉnh có màu đỏ mặc định sẽ chuyển sang màu xám khi người dùng nhấp vào 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 our mutable span
            bulletSpan.color = Color.GRAY
            // color won’t be changed 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 our mutable span
                bulletSpan.setColor(Color.GRAY);
                // color won’t be changed 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 việc sử dụng 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.