Spans

Funktion „Schreiben“ ausprobieren
Jetpack Compose ist das empfohlene UI-Toolkit für Android. Hier erfahren Sie, wie Sie Text in Compose verwenden.

Spans sind leistungsstarke Markup-Objekte, mit denen Sie Text auf Zeichen- oder Absatzebene formatieren können. Wenn Sie Spans an Textobjekte anhängen, können Sie Text auf verschiedene Weise ändern. Sie können Text z. B. durch Hinzufügen von Farbe, Anklickbarer Text, Skalieren der Textgröße oder beim Zeichnen von Text individuell anpassen. Mit Spans können auch TextPaint-Eigenschaften geändert, auf einem Canvas gezeichnet und das Textlayout geändert werden.

Android bietet verschiedene Arten von Spans, die eine Vielzahl gängiger Textstilmuster abdecken. Sie können auch eigene Spans erstellen, um benutzerdefinierte Stile anzuwenden.

Span erstellen und anwenden

Zum Erstellen eines Spans können Sie eine der in der folgenden Tabelle aufgeführten Klassen verwenden. Die Klassen unterscheiden sich in Abhängigkeit davon, ob der Text selbst änderbar ist, ob das Text-Markup veränderbar ist und in welcher zugrunde liegenden Datenstruktur die Span-Daten enthalten sind.

Klasse Änderbarer Text Änderbares Markup Datenstruktur
SpannedString Nein Nein Lineares Array
SpannableString Nein Ja Lineares Array
SpannableStringBuilder Ja Ja Intervallbaum

Alle drei Klassen erweitern die Schnittstelle Spanned. SpannableString und SpannableStringBuilder erweitern außerdem die Spannable-Schnittstelle.

So entscheiden Sie sich für ein Tool:

  • Wenn du den Text oder das Markup nach dem Erstellen nicht änderst, verwende SpannedString.
  • Wenn Sie eine kleine Anzahl von Spans an ein einzelnes Textobjekt anhängen müssen und der Text selbst schreibgeschützt ist, verwenden Sie SpannableString.
  • Wenn Sie Text nach dem Erstellen ändern und Spans an den Text anhängen müssen, verwenden Sie SpannableStringBuilder.
  • Wenn Sie eine große Anzahl von Spans an ein Textobjekt anhängen müssen, auch wenn der Text selbst schreibgeschützt ist, verwenden Sie SpannableStringBuilder.

Wenn Sie einen Span anwenden möchten, rufen Sie setSpan(Object _what_, int _start_, int _end_, int _flags_) für ein Spannable-Objekt auf. Der what-Parameter bezieht sich auf den Span, den Sie auf den Text anwenden, und die start- und end-Parameter geben den Teil des Textes an, auf den Sie den Span anwenden.

Wenn Sie Text innerhalb der Begrenzungen eines Spans einfügen, wird der Span automatisch um den eingefügten Text erweitert. Beim Einfügen von Text an den Span-Grenzen, also am Index start oder end, bestimmt der Parameter flags, ob der Span um den eingefügten Text erweitert wird. Verwenden Sie das Flag Spannable.SPAN_EXCLUSIVE_INCLUSIVE, um eingefügten Text einzuschließen, und Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, um den eingefügten Text auszuschließen.

Das folgende Beispiel zeigt, wie ein ForegroundColorSpan an einen String angehängt wird:

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
);
Ein Bild mit grauem Text, teilweise rot.
Abbildung 1: Text mit ForegroundColorSpan formatiert.

Da der Span mit Spannable.SPAN_EXCLUSIVE_INCLUSIVE festgelegt wird, wird er erweitert und enthält dann eingefügten Text an den Span-Grenzen, wie im folgenden Beispiel gezeigt:

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)");
Ein Bild, das zeigt, dass der Span bei Verwendung von SPAN_EXCLUSIVE_INCLUSIVE mehr Text enthält.
Abbildung 2. Der Span wird bei Verwendung von Spannable.SPAN_EXCLUSIVE_INCLUSIVE um zusätzlichen Text erweitert.

Sie können an einen Text mehrere Spans anhängen. Das folgende Beispiel zeigt, wie Sie fett- und roten Text erstellen:

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
);
Ein Bild, das einen Text mit mehreren Spannen zeigt: „ForegroundColorSpan(Color.RED)“ und „StyleSpan(BOLD)“
Abbildung 3: Text mit mehreren Spans: ForegroundColorSpan(Color.RED) und StyleSpan(BOLD).

Android-Span-Typen

Bei Android sind im Paket android.text.style über 20 Span-Typen enthalten. Android kategorisiert Spans hauptsächlich auf zwei Arten:

  • Auswirkungen des Spans auf Text: Ein Span kann sich auf die Textdarstellung oder die Textmesswerte auswirken.
  • Span-Bereich: Einige Spans können auf einzelne Zeichen angewendet werden, andere müssen auf einen ganzen Absatz angewendet werden.
Ein Bild mit verschiedenen Span-Kategorien
Abbildung 4. Die Android-Kategorien erstrecken sich über

In den folgenden Abschnitten werden diese Kategorien ausführlicher beschrieben.

Spans, die die Textdarstellung beeinflussen

Einige Spans auf Zeichenebene wirken sich auf die Textdarstellung aus, z. B. das Ändern der Text- oder Hintergrundfarbe und das Hinzufügen von Unterstreichungen oder Durchstreichen. Diese Spans erweitern die Klasse CharacterStyle.

Das folgende Codebeispiel zeigt, wie ein UnderlineSpan angewendet wird, um den Text zu unterstreichen:

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);
Ein Bild, auf dem zu sehen ist, wie Text mit „UnterstrichSpan“ unterstrichen wird
Abbildung 5: Text unterstrichen mit einem UnderlineSpan.

Bei Spans, die sich nur auf die Textdarstellung auswirken, wird der Text neu gezeichnet, ohne eine Neuberechnung des Layouts zu veranlassen. Diese Spans implementieren UpdateAppearance und erweitern CharacterStyle. Mit abgeleiteten CharacterStyle-Klassen wird festgelegt, wie Text gezeichnet wird, indem sie Zugriff zum Aktualisieren des TextPaint-Objekts gewähren.

Spans, die sich auf Textmesswerte auswirken

Andere Spans auf Zeichenebene wirken sich auf Textmesswerte wie Zeilenhöhe und Textgröße aus. Diese Spans erweitern die Klasse MetricAffectingSpan.

Im folgenden Codebeispiel wird ein RelativeSizeSpan erstellt, mit dem die Textgröße um 50 % erhöht wird:

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);
Bild, das die Verwendung von RelativeSizeSpan zeigt
Abbildung 6. Text wurde mit RelativeSizeSpan vergrößert.

Wenn Sie einen Bereich anwenden, der sich auf Textmesswerte auswirkt, führt ein beobachtendes Objekt dazu, dass der Text neu für das korrekte Layout und Rendering gemessen wird. Wenn Sie beispielsweise die Textgröße ändern, können Wörter in anderen Zeilen angezeigt werden. Durch Anwenden des vorherigen Spans werden eine Neumessung, eine Neuberechnung des Textlayouts und die Neuzeichnung des Texts ausgelöst.

Spans, die sich auf Textmesswerte auswirken, erweitern die MetricAffectingSpan-Klasse, eine abstrakte Klasse, mit der Unterklassen definieren können, wie sich der Span auf die Textmessung auswirkt, indem sie Zugriff auf den TextPaint bietet. Da MetricAffectingSpan CharacterSpan erweitert, beeinflussen Unterklassen die Darstellung des Texts auf Zeichenebene.

Spans, die sich auf Absätze auswirken

Ein Bereich kann sich auch auf Text auf Absatzebene auswirken, z. B. durch Ändern der Ausrichtung oder des Rands eines Textblocks. Für Spans, die ganze Absätze betreffen, wird ParagraphStyle implementiert. Um diese Abschnitte zu verwenden, müssen Sie sie mit Ausnahme des abschließenden Zeilenumbruchzeichens mit dem gesamten Absatz verknüpfen. Wenn Sie versuchen, eine Absatzspanne auf etwas anderes als einen ganzen Absatz anzuwenden, wird sie von Android überhaupt nicht angewendet.

Abbildung 8 zeigt, wie Android Absätze in Text trennt.

Abbildung 7. In Android enden Absätze mit einem Zeilenumbruchzeichen (\n).

Im folgenden Codebeispiel wird ein QuoteSpan auf einen Absatz angewendet. Wenn Sie den Span an einer anderen Position als am Anfang oder Ende eines Absatzes anhängen, wird der Stil in Android überhaupt nicht angewendet.

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);
Bild, das ein Beispiel für QuoteSpan zeigt
Abbildung 8. Ein QuoteSpan, der auf einen Absatz angewendet wird.

Benutzerdefinierte Spans erstellen

Wenn Sie mehr Funktionen benötigen, als in den vorhandenen Android-Spans verfügbar sind, können Sie einen benutzerdefinierten Span implementieren. Wenn Sie einen eigenen Span implementieren, sollten Sie entscheiden, ob sich dieser auf Text auf Zeichen- oder Absatzebene auswirkt und ob sich dies auf das Layout oder die Darstellung des Texts auswirkt. So können Sie leichter ermitteln, welche Basisklassen Sie erweitern können und welche Schnittstellen Sie unter Umständen implementieren müssen. Verwenden Sie die folgende Tabelle als Referenz:

Szenario Klasse oder Benutzeroberfläche
Ihr Span wirkt sich auf den Text auf Zeichenebene aus. CharacterStyle
Ihr Span wirkt sich auf die Textdarstellung aus. UpdateAppearance
Ihr Span wirkt sich auf Textmesswerte aus. UpdateLayout
Ihr Span wirkt sich auf den Text auf Absatzebene aus. ParagraphStyle

Wenn Sie beispielsweise einen benutzerdefinierten Span implementieren müssen, der die Textgröße und -farbe ändert, erweitern Sie RelativeSizeSpan. Durch Übernahme erweitert RelativeSizeSpan CharacterStyle und implementiert die beiden Update-Schnittstellen. Da diese Klasse bereits Callbacks für updateDrawState und updateMeasureState bereitstellt, können Sie diese Callbacks überschreiben, um Ihr benutzerdefiniertes Verhalten zu implementieren. Mit dem folgenden Code wird ein benutzerdefinierter Span erstellt, der RelativeSizeSpan erweitert und den updateDrawState-Callback überschreibt, um die Farbe von TextPaint festzulegen:

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

In diesem Beispiel wird gezeigt, wie Sie einen benutzerdefinierten Span erstellen. Den gleichen Effekt erzielen Sie, wenn Sie RelativeSizeSpan und ForegroundColorSpan auf den Text anwenden.

Span-Nutzung testen

Mit der Schnittstelle Spanned können Sie Spans festlegen und auch Spans aus Text abrufen. Implementieren Sie für Tests einen Android JUnit-Test, um zu prüfen, ob die richtigen Spans an den richtigen Stellen hinzugefügt wurden. Die Beispiel-App für Textstile enthält einen Bereich, der Markup auf Aufzählungspunkte anwendet, indem BulletPointSpan an den Text angehängt wird. Das folgende Codebeispiel zeigt, wie Sie testen können, ob die Aufzählungspunkte wie erwartet angezeigt werden:

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

Weitere Testbeispiele finden Sie auf GitHub unter MarkdownBuilderTest.

Benutzerdefinierte Spans testen

Prüfen Sie beim Testen von Spans, ob TextPaint die erwarteten Änderungen enthält und ob die richtigen Elemente in der Canvas angezeigt werden. Angenommen, Sie haben die Implementierung eines benutzerdefinierten Spans, bei der einem Text ein Aufzählungspunkt vorangestellt wird. Der Aufzählungspunkt hat eine bestimmte Größe und Farbe und es gibt eine Lücke zwischen dem linken Rand des Drawable-Bereichs und dem Aufzählungspunkt.

Sie können das Verhalten dieser Klasse testen, indem Sie einen AndroidJUnit-Test implementieren und Folgendes prüfen:

  • Wenn Sie die Spanne richtig anwenden, wird ein Aufzählungspunkt der angegebenen Größe und Farbe auf dem Canvas angezeigt und der richtige Abstand zwischen dem linken Rand und dem Aufzählungspunkt ist vorhanden.
  • Wenn Sie den Span nicht anwenden, wird keines der benutzerdefinierten Verhalten angezeigt.

Die Implementierung dieser Tests finden Sie im TextStyling-Beispiel auf GitHub.

Sie können Canvas-Interaktionen testen, indem Sie den Canvas simulieren, das simulierte Objekt an die Methode drawLeadingMargin() übergeben und prüfen, ob die richtigen Methoden mit den richtigen Parametern aufgerufen werden.

Weitere Span-Testbeispiele finden Sie unter BulletPointSpanTest.

Best Practices für die Verwendung von Spans

Je nach Ihren Anforderungen gibt es mehrere speichereffiziente Möglichkeiten, Text in einem TextView festzulegen.

Spanne anhängen oder entfernen, ohne den zugrunde liegenden Text zu ändern

TextView.setText() enthält mehrere Überlasten, die Spans unterschiedlich verarbeiten. Sie können beispielsweise ein Spannable-Textobjekt mit dem folgenden Code festlegen:

Kotlin

textView.setText(spannableObject)

Java

textView.setText(spannableObject);

Wenn Sie diese setText()-Überlastung aufrufen, erstellt TextView eine Kopie von Spannable als SpannedString und speichert sie im Arbeitsspeicher als CharSequence. Das bedeutet, dass Ihr Text und die Spans unveränderlich sind. Wenn Sie also den Text oder die Spans aktualisieren müssen, müssen Sie ein neues Spannable-Objekt erstellen und setText() noch einmal aufrufen. Dadurch wird ebenfalls das Layout neu gemessen und gezeichnet.

Wenn Sie angeben möchten, dass die Spans änderbar sein müssen, können Sie stattdessen setText(CharSequence text, TextView.BufferType type) verwenden, wie im folgenden Beispiel gezeigt:

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

In diesem Beispiel bewirkt der Parameter BufferType.SPANNABLE, dass TextView ein SpannableString-Objekt erstellt und das von TextView beibehaltene CharSequence-Objekt jetzt änderbares Markup und unveränderlichen Text enthält. Rufen Sie zum Aktualisieren des Spans den Text als Spannable ab und aktualisieren Sie die Spans nach Bedarf.

Wenn Sie Spans anhängen, trennen oder neu positionieren, wird die TextView automatisch aktualisiert, um die Änderung am Text widerzuspiegeln. Wenn Sie ein internes Attribut eines vorhandenen Spans ändern, rufen Sie invalidate() auf, um Änderungen an der Darstellung vorzunehmen, oder requestLayout(), um messwertbezogene Änderungen vorzunehmen.

Text mehrmals in TextView festlegen

In einigen Fällen, z. B. bei der Verwendung von RecyclerView.ViewHolder, kann es sinnvoll sein, einen TextView wiederzuverwenden und den Text mehrmals festzulegen. Standardmäßig erstellt TextView eine Kopie des CharSequence-Objekts und speichert es im Arbeitsspeicher, unabhängig davon, ob Sie BufferType festgelegt haben. Dadurch werden alle TextView-Aktualisierungen beabsichtigt. Sie können das ursprüngliche CharSequence-Objekt nicht aktualisieren, um den Text zu aktualisieren. Das bedeutet, dass TextView jedes Mal ein neues Objekt erstellt, wenn Sie neuen Text festlegen.

Wenn Sie mehr Kontrolle über diesen Prozess haben und das Erstellen zusätzlicher Objekte vermeiden möchten, können Sie einen eigenen Spannable.Factory implementieren und newSpannable() überschreiben. Anstatt ein neues Textobjekt zu erstellen, können Sie das vorhandene CharSequence-Objekt umwandeln und als Spannable zurückgeben, wie im folgenden Beispiel gezeigt:

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

Sie müssen beim Festlegen des Texts textView.setText(spannableObject, BufferType.SPANNABLE) verwenden. Andernfalls wird die Quell-CharSequence als Spanned-Instanz erstellt und kann nicht in Spannable umgewandelt werden, wodurch newSpannable() eine ClassCastException ausgibt.

Nachdem Sie newSpannable() überschrieben haben, weisen Sie TextView an, das neue Factory zu verwenden:

Kotlin

textView.setSpannableFactory(spannableFactory)

Java

textView.setSpannableFactory(spannableFactory);

Legen Sie das Spannable.Factory-Objekt einmal fest, direkt nachdem Sie einen Verweis auf Ihre TextView erhalten haben. Wenn Sie ein RecyclerView verwenden, legen Sie das Factory-Objekt fest, wenn Sie die Ansichten zum ersten Mal erweitern. So wird vermieden, dass zusätzliche Objekte erstellt werden, wenn Ihr RecyclerView ein neues Element an Ihre ViewHolder bindet.

Attribute des internen Spans ändern

Wenn Sie nur ein internes Attribut eines änderbaren Spans ändern müssen, z. B. die Farbe der Aufzählungszeichen in einem benutzerdefinierten Span mit Aufzählungszeichen, können Sie den Aufwand für den mehrfachen Aufruf von setText() vermeiden, indem Sie beim Erstellen einen Verweis auf den Span beibehalten. Wenn Sie den Span ändern müssen, können Sie die Referenz ändern und dann invalidate() oder requestLayout() für das TextView aufrufen, je nach Typ des geänderten Attributs.

Im folgenden Codebeispiel hat eine benutzerdefinierte Implementierung mit Aufzählungspunkten die Standardfarbe Rot, die sich in Grau ändert, wenn auf eine Schaltfläche getippt wird:

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-Erweiterungsfunktionen verwenden

Android KTX enthält außerdem Erweiterungsfunktionen, die die Arbeit mit Spans vereinfachen. Weitere Informationen finden Sie in der Dokumentation zum Paket androidx.core.text.