ספרד

כדאי לנסות את התכונה 'כתיבה מהירה'
Jetpack Compose היא ערכת הכלים המומלצת לבניית ממשק משתמש ב-Android. איך משתמשים בטקסט ב'כתיבה מהירה'

קטעי טקסט הם אובייקטים חזקים של סימון שאפשר להשתמש בהם כדי לעצב טקסט ברמת התו או הפסקה. כשמחברים קטעי טקסט לאובייקטים של טקסט, אפשר לשנות את הטקסט בדרכים שונות, כולל הוספת צבע, הפיכת הטקסט לקליקביל, שינוי גודל הטקסט ורישום טקסט באופן מותאם אישית. אפשר גם לשנות את המאפיינים של TextPaint, לצייר על Canvas ולשנות את פריסת הטקסט באמצעות קטעי טקסט.

ב-Android יש כמה סוגים של span שמכסים מגוון דפוסים נפוצים של עיצוב טקסט. אפשר גם ליצור קטעי טקסט משלכם כדי להחיל עיצוב בהתאמה אישית.

יצירת span והחלה שלו

כדי ליצור span, אפשר להשתמש באחת מהכיתות שמפורטות בטבלה הבאה. ההבדלים בין הכיתות נובעים מהעובדה שהטקסט עצמו יכול להשתנות, מהעובדה שסימון הטקסט יכול להשתנות ומהמבנה הבסיסי של הנתונים שמכיל את נתוני ה-span.

דרגה טקסט שניתן לשינוי תגי עיצוב שניתן לשינוי מבנה נתונים
SpannedString לא לא מערך לינארי
SpannableString לא כן מערך לינארי
SpannableStringBuilder כן כן עץ Interval

כל שלוש המחלקות מרחיבות את הממשק Spanned. גם SpannableString וגם SpannableStringBuilder מרחיבים את הממשק Spannable.

כך מחליטים באיזה פורמט להשתמש:

  • אם לא משנים את הטקסט או את ה-Markup אחרי היצירה, משתמשים ב-SpannedString.
  • אם צריך לצרף מספר קטן של קטעי טקסט לאובייקט טקסט יחיד והטקסט עצמו הוא לקריאה בלבד, צריך להשתמש ב-SpannableString.
  • אם צריך לשנות טקסט אחרי היצירה ולהצמיד אליו קטעי טקסט, משתמשים ב-SpannableStringBuilder.
  • אם אתם צריכים לצרף מספר גדול של קטעי טקסט לאובייקט טקסט, ללא קשר לכך שהטקסט עצמו הוא לקריאה בלבד, השתמשו ב-SpannableStringBuilder.

כדי להחיל span, קוראים ל-setSpan(Object _what_, int _start_, int _end_, int _flags_) על אובייקט Spannable. הפרמטר what מתייחס למקף שטחי הטקסט שמוחל על הטקסט, והפרמטרים start ו-end מציינים את החלק בטקסט שבו חל המקף.

אם מוסיפים טקסט בתוך גבולות של קטע טקסט, הקטע מתרחב באופן אוטומטי כך שיכלול את הטקסט שהוספתם. כשמוסיפים טקסט ב גבולות הטווח – כלומר, במדדים start או end – הפרמטר flags קובע אם הטווח יתרחב כך שיכלול את הטקסט שהוספתם. משתמשים בדגל Spannable.SPAN_EXCLUSIVE_INCLUSIVE כדי לכלול טקסט שהוכנס, ובדגל Spannable.SPAN_EXCLUSIVE_EXCLUSIVE כדי להחריג את הטקסט שהוכנס.

בדוגמה הבאה מוסבר איך לצרף ForegroundColorSpan למחרוזת:

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
);
תמונה שמציגה טקסט אפור, חלקו אדום.
איור 1. טקסט בסגנון ForegroundColorSpan.

מכיוון שההיקף מוגדר באמצעות Spannable.SPAN_EXCLUSIVE_INCLUSIVE, הוא מתרחב כך שיכלול טקסט שהוכנס בגבולות ההיקף, כפי שמוצג בדוגמה הבאה:

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)");
תמונה שמראה איך ה-span כולל יותר טקסט כשמשתמשים ב-SPAN_EXCLUSIVE_INCLUSIVE.
איור 2. כשמשתמשים ב-Spannable.SPAN_EXCLUSIVE_INCLUSIVE, ה-span מתרחב כך שיכלול טקסט נוסף.

אפשר לצרף כמה קטעי טקסט לאותו טקסט. בדוגמה הבאה מוסבר איך ליצור טקסט מודגש ואדום:

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
);
תמונה שמציגה טקסט עם כמה קטעי טקסט: 'ForegroundColorSpan(Color.RED)' ו-'StyleSpan(BOLD)'
איור 3. טקסט עם כמה קטעים: ForegroundColorSpan(Color.RED) וגם StyleSpan(BOLD).

סוגי span ב-Android

ב-Android יש יותר מ-20 סוגי span בחבילה android.text.style. מערכת Android מסווגת את ה-spans בשתי דרכים עיקריות:

  • איך ה-span משפיע על הטקסט: ה-span יכול להשפיע על המראה של הטקסט או על מדדי הטקסט.
  • היקף ה-span: אפשר להחיל span מסוימים על תווים בודדים, ואילו אחרים צריך להחיל על פסקה שלמה.
תמונה שמציגה קטגוריות שונות של מרחבים
איור 4. קטגוריות של מרחבים ב-Android.

בקטעים הבאים מוסבר בהרחבה על הקטגוריות האלה.

קטעי טקסט שמשפיעים על המראה של הטקסט

חלק מהקטעים שחלים ברמת התו השפיעים על מראה הטקסט, כמו שינוי צבע הטקסט או הרקע והוספת קו תחתון או קו מחיקה. ה-spans האלה מרחיבים את הכיתה CharacterStyle.

בדוגמת הקוד הבאה מוסבר איך להשתמש ב-UnderlineSpan כדי להדגיש את הטקסט:

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);
תמונה שמראה איך להוסיף קו תחתון לטקסט באמצעות 'UnderlineSpan'
איור 5. טקסט עם קו תחתון באמצעות UnderlineSpan.

קטעי טקסט שמשפיעים רק על המראה של הטקסט גורמים לציור מחדש של הטקסט בלי לגרום לחישוב מחדש של הפריסה. הקטעים האלה מיישמים את UpdateAppearance ומרחיבים את CharacterStyle. כדי להגדיר איך לצייר טקסט, תת-הסוגים של CharacterStyle מספקים גישה לעדכון של TextPaint.

קטעי טקסט שמשפיעים על מדדי הטקסט

קטעי טקסט אחרים שחלים ברמת התו השפיעים על מדדי הטקסט, כמו גובה השורה וגודל הטקסט. ה-spans האלה מרחיבים את הכיתה MetricAffectingSpan.

בדוגמת הקוד הבאה נוצר RelativeSizeSpan שמגדיל את גודל הטקסט ב-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);
תמונה שמראה את השימוש ב-RelativeSizeSpan
איור 6. טקסט שהוגדל באמצעות RelativeSizeSpan.

החלת span שמשפיע על מדדי הטקסט גורמת לאובייקט שמתבונן למדוד מחדש את הטקסט כדי לבדוק אם הפריסה והעיבוד שלו נכונים. לדוגמה, שינוי גודל הטקסט עשוי לגרום למילים להופיע בשורות שונות. החלת ה-span הקודם מפעילה מדידה מחדש, חישוב מחדש של פריסת הטקסט ורישום מחדש של הטקסט.

קטעי טקסט שמשפיעים על מדדי הטקסט מרחיבים את הכיתה MetricAffectingSpan, שהיא כיתה מופשטת שמאפשרת לתת-כיתות להגדיר איך הקטע משפיע על מדידת הטקסט על ידי מתן גישה ל-TextPaint. מאחר ש-MetricAffectingSpan הוא תת-סוג של CharacterStyle, תת-הסוגים משפיעים על המראה של הטקסט ברמת התו.

קטעי טקסט שמשפיעים על פסקאות

span יכול גם להשפיע על טקסט ברמת הפסקה, למשל שינוי היישור או השוליים של מקטע טקסט. קטעי טקסט שמשפיעים על פסקאות שלמות מיישמים את ParagraphStyle. כדי להשתמש בקטעי הקוד האלה, צריך לצרף אותם לכל הפסקה, לא כולל תו השורה החדשה בסוף. אם מנסים להחיל קטע טקסט של פסקה על משהו שאינו פסקה שלמה, מערכת Android לא מחילה את הקטע בכלל.

באיור 8 מוצגת אופן ההפרדה של Android בין פסקאות בטקסט.

איור 7. ב-Android, פסקאות מסתיימות בתווית של שורה חדשה (\n).

בדוגמת הקוד הבאה מוחלים את הערך QuoteSpan על פסקה. חשוב לזכור שאם מחברים את ה-span למיקום כלשהו שאינו תחילת או סוף הפסקה, מערכת Android לא מחילה את הסגנון בכלל.

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);
תמונה שמציגה דוגמה ל-QuoteSpan
איור 8. QuoteSpan הוחלה על פסקה.

יצירת מקטעים מותאמים אישית

אם אתם צריכים פונקציונליות נוספת מעבר לזו שסופקת על ידי ה-spans הקיימים ל-Android, תוכלו להטמיע span מותאם אישית. כשמטמיעים span משלכם, צריך להחליט אם ה-span ישפיע על הטקסט ברמת התווים או ברמת הפסקה, וגם אם הוא ישפיע על הפריסה או על המראה של הטקסט. כך תוכלו לקבוע אילו מחלקות בסיס אפשר להרחיב ואילו ממשקים ייתכן שתצטרכו להטמיע. בטבלה הבאה מפורט מידע נוסף:

תרחיש כיתה או ממשק
ה-span משפיע על הטקסט ברמת התו. CharacterStyle
הרוחב של ה-span משפיע על המראה של הטקסט. UpdateAppearance
טווח הזמן משפיע על מדדי הטקסט. UpdateLayout
ה-span משפיע על הטקסט ברמת הפסקה. ParagraphStyle

לדוגמה, אם אתם צריכים להטמיע span בהתאמה אישית שמשנה את גודל הטקסט ואת הצבע שלו, תוכלו להרחיב את RelativeSizeSpan. באמצעות ירושה, RelativeSizeSpan מרחיב את CharacterStyle ומטמיע את שני הממשקים Update. מכיוון שהכיתה הזו כבר מספקת קריאות חזרה ל-updateDrawState ול-updateMeasureState, אפשר לשנות את ברירת המחדל של הקריאות האלה כדי להטמיע את ההתנהגות בהתאמה אישית. הקוד הבא יוצר span בהתאמה אישית שמרחיב את RelativeSizeSpan ומחליף את הפונקציה הלא חוזרת updateDrawState כדי להגדיר את הצבע של ה-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);
    }
}

הדוגמה הזו ממחישה איך יוצרים קטע טקסט בהתאמה אישית. אפשר להשיג את אותו אפקט על ידי החלת RelativeSizeSpan ו-ForegroundColorSpan על הטקסט.

בדיקת השימוש ב-span

הממשק Spanned מאפשר להגדיר קטעי טקסט (spans) וגם לאחזר קטעי טקסט מתוך טקסט. בזמן הבדיקה, מטמיעים בדיקת JUnit ל-Android כדי לוודא שה-spans הנכונים נוספו במיקומים הנכונים. אפליקציית הדוגמה של עיצוב טקסט מכילה span שמחבר את BulletPointSpan לטקסט כדי להחיל סימון על תבליטים. בדוגמה הבאה מוסבר איך לבדוק אם הנקודות מופיעות כצפוי:

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

דוגמאות נוספות לבדיקות זמינות ב-MarkdownBuilderTest ב-GitHub.

בדיקת קטעים מותאמים אישית

כשבודקים את ה-spans, מוודאים שהשינויים הצפויים מופיעים ב-TextPaint ושהרכיבים הנכונים מופיעים ב-Canvas. לדוגמה, אפשר להטמיע span בהתאמה אישית שמוסיף סימן נקודה-פסיק לתחילת טקסט מסוים. לנקודת הסימון יש גודל וצבע מוגדרים, ויש פער בין השוליים הימניים של האזור שאפשר לצייר בו לבין נקודת הסימון.

אפשר לבדוק את ההתנהגות של הכיתה הזו על ידי הטמעת בדיקת AndroidJUnit, ולבדוק את הדברים הבאים:

  • אם מחילים את ה-span בצורה נכונה, נקודה עם תווית תיצור בבד הציור בגודל ובצבע שצוינו, והרווח המתאים יהיה בין השוליים הימניים לבין הנקודה עם התווית.
  • אם לא מחילים את ה-span, אף אחד מההתנהגויות בהתאמה אישית לא יופיע.

אפשר לראות את ההטמעה של הבדיקות האלה בדוגמה של TextStyling ב-GitHub.

כדי לבדוק את האינטראקציות של Canvas, אפשר ליצור רמאות ל-Canvas, להעביר את האובייקט המזויף לשיטה drawLeadingMargin() ולאמת שהשיטות הנכונות נקראות עם הפרמטרים הנכונים.

דוגמאות נוספות לבדיקות של span מפורטות ב-BulletPointSpanTest.

שיטות מומלצות לשימוש ב-spans

יש כמה דרכים לחסוך בזיכרון כשמגדירים טקסט ב-TextView, בהתאם לצרכים שלכם.

איך מחברים או מנתקים קטע טקסט בלי לשנות את הטקסט שמתחתיו

TextView.setText() מכיל כמה עומסי יתר שמטפלים ב-spans בצורה שונה. לדוגמה, אפשר להגדיר אובייקט טקסט מסוג Spannable באמצעות הקוד הבא:

Kotlin

textView.setText(spannableObject)

Java

textView.setText(spannableObject);

כשקוראים לעומס יתר הזה של setText(), ה-TextView יוצר עותק של Spannable בתור SpannedString ושומר אותו בזיכרון בתור CharSequence. פירוש הדבר הוא שהטקסט והקטעים לא ניתנים לשינוי, ולכן כשצריך לעדכן את הטקסט או את הקטעים, צריך ליצור אובייקט Spannable חדש ולקרוא שוב לפונקציה setText(). הפעולה הזו תגרום גם למדידת מחדש ולציור מחדש של הפריסה.

כדי לציין שהמרווחים חייבים להיות ניתנים לשינוי, אפשר להשתמש במקום זאת ב-setText(CharSequence text, TextView.BufferType type), כפי שמתואר בדוגמה הבאה:

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

בדוגמה הזו, הפרמטר BufferType.SPANNABLE גורם ל-TextView ליצור SpannableString, ואובייקט ה-CharSequence שנשמר על ידי ה-TextView מכיל עכשיו סימון שניתן לשינוי וטקסט שלא ניתן לשינוי. כדי לעדכן את הטווח, מאחזרים את הטקסט כ-Spannable ואז מעדכנים את הקטעים לפי הצורך.

כשמחברים, מנתקים או משנים את המיקום של קטעי הטקסט, הערך של TextView מתעדכן באופן אוטומטי בהתאם לשינוי בטקסט. אם משנים מאפיין פנימי של span קיים, צריך להפעיל את invalidate() כדי לבצע שינויים שקשורים למראה או את requestLayout() כדי לבצע שינויים שקשורים למדדים.

הגדרת טקסט ב-TextView כמה פעמים

במקרים מסוימים, למשל כשמשתמשים ב-RecyclerView.ViewHolder, כדאי להשתמש שוב ב-TextView ולהגדיר את הטקסט כמה פעמים. כברירת מחדל, ללא קשר להגדרה של BufferType, ה-TextView יוצר עותק של האובייקט CharSequence ושומר אותו בזיכרון. כך כל העדכונים של TextView יהיו מכוונים – לא תוכלו לעדכן את האובייקט המקורי CharSequence כדי לעדכן את הטקסט. כלומר, בכל פעם שמגדירים טקסט חדש, ה-TextView יוצר אובייקט חדש.

אם רוצים לשלוט יותר בתהליך הזה ולהימנע מהיצירה של האובייקט הנוסף, אפשר להטמיע Spannable.Factory משלכם ולשנות את newSpannable(). במקום ליצור אובייקט טקסט חדש, אפשר להמיר את ה-CharSequence הקיים ולחזור אליו כ-Spannable, כפי שמתואר בדוגמה הבאה:

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

חובה להשתמש ב-textView.setText(spannableObject, BufferType.SPANNABLE) כשמגדירים את הטקסט. אחרת, המקור CharSequence נוצר כמכונה של Spanned ולא ניתן להמיר אותו ל-Spannable, וכתוצאה מכך newSpannable() יזרוק ClassCastException.

אחרי שמבטלים את השינויים של newSpannable(), צריך להורות ל-TextView להשתמש ב-Factory החדש:

Kotlin

textView.setSpannableFactory(spannableFactory)

Java

textView.setSpannableFactory(spannableFactory);

מגדירים את האובייקט Spannable.Factory פעם אחת, מיד אחרי שמקבלים הפניה ל-TextView. אם אתם משתמשים ב-RecyclerView, מגדירים את האובייקט Factory בפעם הראשונה שמנפחים את מספר הצפיות. כך אפשר למנוע יצירת אובייקטים נוספים כשה-RecyclerView מקשר פריט חדש ל-ViewHolder.

שינוי מאפייני span פנימיים

אם אתם צריכים לשנות רק מאפיין פנימי של span שניתן לשינוי, כמו צבע הנקודה ב-span של נקודה בהתאמה אישית, תוכלו להימנע מהעלות הנוספת של קריאה ל-setText() כמה פעמים על ידי שמירת הפניה ל-span בזמן היצירה שלו. כשצריך לשנות את ה-span, אפשר לשנות את ההפניה ואז לבצע קריאה ל-invalidate() או ל-requestLayout() ב-TextView, בהתאם לסוג המאפיין ששיניתם.

בדוגמת הקוד הבאה, להטמעה של נקודה שחורה בהתאמה אישית יש צבע ברירת מחדל אדום שמשתנה לאפור כשמקישים על הלחצן:

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

שימוש בפונקציות של תוסף KTX ל-Android

Android KTX מכיל גם פונקציות תוסף שמקלות על העבודה עם span. מידע נוסף זמין במסמכי העזרה של החבילה androidx.core.text.