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