ספרד

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

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

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

יצירה והחלה של טווח

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

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

כל שלוש המחלקות מרחיבות את הממשק Spanned. בנוסף, SpannableString ו-SpannableStringBuilder מרחיבים את הממשק של Spannable.

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

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

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

אם מוסיפים טקסט בתוך הגבולות של span, ה-span מתרחב באופן אוטומטי כדי לכלול את הטקסט שנוסף. כשמוסיפים טקסט בגבולות של טווח, כלומר במדדי ההתחלה או הסוף, הפרמטר 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_EXCLUSIVE_INCLUSIVE.
איור 2. הטווח מתרחב וכולל עוד טקסט כשמשתמשים ב-Spannable.SPAN_EXCLUSIVE_INCLUSIVE.

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

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

סוגי טווחים ב-Android

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

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

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

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

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

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

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

טווחים אחרים שחלים ברמת התו משפיעים על מדדי טקסט, כמו גובה השורה וגודל הטקסט. הטווחים האלה מרחיבים את המחלקה 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 שלמעלה מפעילה מדידה מחדש, חישוב מחדש של פריסת הטקסט וציור מחדש של הטקסט.

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

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

תג 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 חל על פסקה.

יצירת טווחים מותאמים אישית

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

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

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

בדיקת השימוש בטווח

ממשק Spanned מאפשר להגדיר טווחים וגם לאחזר טווחים מטקסט. כשבודקים, מטמיעים בדיקת Android JUnitכדי לוודא שהטווחים הנכונים נוספים במיקומים הנכונים. אפליקציית הדוגמה Text Styling כוללת תג 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.

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

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

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

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

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

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

דוגמאות נוספות לבדיקת טווח זמינות ב-BulletPointSpanTest.

שיטות מומלצות לשימוש בטווחים

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

צירוף או ניתוק של טווח בלי לשנות את הטקסט הבסיסי

TextView.setText() כולל כמה עומסים שונים שמטפלים בטווחים בצורה שונה. לדוגמה, אפשר להגדיר אובייקט טקסט 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 ומחזיקה אותו בזיכרון. כך כל העדכונים יהיו מכוונים – אי אפשר לעדכן את האובייקט המקורי כדי לעדכן את הטקסט.TextViewCharSequence כלומר, בכל פעם שמגדירים טקסט חדש, 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 של תבליט מותאם אישית, אתם יכולים להימנע מהתקורה של קריאה ל-setText() כמה פעמים על ידי שמירת הפניה ל-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();
            }
        });
    }
}

שימוש בפונקציות הרחבה של Android KTX

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