Spans sind leistungsstarke Markup-Objekte, mit denen Sie Text auf Zeichen- oder Absatzebene gestalten können. Durch das Anhängen von Spans an Textobjekte können Sie Text auf verschiedene Arten ändern, z. B. durch Hinzufügen von Farbe, Anpassen des Textes, Skalieren der Textgröße und Benutzerdefiniertes Zeichnen von Text. Außerdem können mit Spans die TextPaint
-Eigenschaften geändert, auf einem Canvas
gezeichnet werden und das Textlayout geändert werden.
Android bietet mehrere 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 von Spans können Sie eine der in der folgenden Tabelle aufgeführten Klassen verwenden. Die Klassen unterscheiden sich je nachdem, ob der Text selbst änderbar ist, ob das Text-Markup änderbar ist und welche zugrunde liegende Datenstruktur die Span-Daten enthält.
Klasse | Veränderlicher Text | Veränderliches Markup | Datenstruktur |
---|---|---|---|
SpannedString |
Nein | Nein | Lineares Array |
SpannableString |
Nein | Ja | Lineares Array |
SpannableStringBuilder |
Ja | Ja | Intervallstruktur |
Alle drei Klassen erweitern die Spanned
-Schnittstelle. SpannableString
und SpannableStringBuilder
erweitern außerdem die Spannable
-Schnittstelle.
So entscheiden Sie, welche Version Sie verwenden möchten:
- Wenn Sie den Text oder das Markup nach dem Erstellen nicht ändern, verwenden Sie
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öchten, verwenden Sie
SpannableStringBuilder
. - Wenn Sie eine große Anzahl von Spans an ein Textobjekt anhängen möchten, unabhängig davon, ob der Text selbst schreibgeschützt ist, verwenden Sie
SpannableStringBuilder
.
Rufen Sie zum Anwenden eines Spans setSpan(Object _what_, int _start_, int _end_, int
_flags_)
für ein Spannable
-Objekt auf. Der Parameter what bezieht sich auf den Span, den Sie auf den Text anwenden, und die Parameter start und end geben den Teil des Textes an, auf den Sie den Span anwenden.
Wenn Sie Text innerhalb der Grenzen eines Spans einfügen, wird der Span automatisch so erweitert, dass er den eingefügten Text enthält. Wird Text an den Span-Grenzen, d. h. an den Indexen start oder end, eingefügt, bestimmt der Parameter flags, ob der Span so erweitert wird, dass er den eingefügten Text enthält. 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 so erweitert, dass er an den Span-Grenzen eingefügten Text enthält, 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 mehrere Spans an denselben Text anhängen. Das folgende Beispiel zeigt, wie fett und roter Text erstellt wird:
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
Android bietet über 20 Span-Typen im Paket android.text.style. Android kategorisiert Spans hauptsächlich auf zwei Arten:
- Auswirkungen des Spans auf Text: Ein Span kann die Textdarstellung oder Textmesswerte beeinflussen.
- Span-Bereich: Einige Spans können auf einzelne Zeichen angewendet werden, während andere auf einen ganzen Absatz angewendet werden müssen.
In den folgenden Abschnitten werden diese Kategorien ausführlicher beschrieben.
Spans, die sich auf die Textdarstellung auswirken
Einige Spannen, die auf Zeichenebene angewendet werden, beeinflussen die Textdarstellung, z. B. ändern Sie die Text- oder Hintergrundfarbe und fügen Unterstreichen oder Durchstreichen hinzu. Diese Spans erweitern die Klasse CharacterStyle
.
Das folgende Codebeispiel zeigt, wie Sie den Text mit einem UnderlineSpan
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);
Spans, die sich nur auf die Textdarstellung auswirken, lösen eine Neuzeichnung des Textes ohne Neuberechnung des Layouts aus. Diese Spans implementieren UpdateAppearance
und erweitern CharacterStyle
.
Abgeleitete CharacterStyle
-Klassen definieren, wie Text gezeichnet wird, indem sie Zugriff zum Aktualisieren von TextPaint
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, der die Textgröße um 50 % erhöht:
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 eine Spanne anwenden, die sich auf Textmesswerte auswirkt, misst ein beobachtendes Objekt den Text noch einmal, um das richtige Layout und Rendering zu gewährleisten. Wenn Sie beispielsweise die Textgröße ändern, können Wörter in verschiedenen Zeilen erscheinen. Das Anwenden des vorherigen Spans löst eine erneute Messung, eine Neuberechnung des Textlayouts und eine Neuzeichnung des Textes aus.
Spans, die sich auf Textmesswerte auswirken, erweitern die Klasse MetricAffectingSpan
, eine abstrakte Klasse, mit der Unterklassen definieren können, wie sich der Span auf die Textmessung auswirkt, indem er Zugriff auf TextPaint
gewährt. Da MetricAffectingSpan
CharacterSpan
erweitert, beeinflussen Unterklassen die Darstellung des Textes auf Zeichenebene.
Spans, die sich auf Absätze auswirken
Ein Bereich kann sich auch auf den Text auf Absatzebene auswirken, z. B. zum Ändern der Ausrichtung oder des Rands eines Textblocks. ParagraphStyle
wird für Spans implementiert, die ganze Absätze betreffen. Um diese Spans zu verwenden, hängen Sie sie an den gesamten Absatz an, ohne das Zeilenende am Ende. Wenn Sie versuchen, eine Absatzspanne auf etwas anderes als einen ganzen Absatz anzuwenden, wendet Android den Span überhaupt nicht an.
Abbildung 8 zeigt, wie Android Absätze im Text trennt.
Im folgenden Codebeispiel wird ein QuoteSpan
auf einen Absatz angewendet. Wenn Sie die Span an eine andere Position als den Anfang oder das Ende eines Absatzes anhängen, wendet Android den Stil überhaupt nicht an.
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 benutzerdefinierte Spans implementieren. Wenn Sie eine eigene Span implementieren, entscheiden Sie, ob sie sich auf den Text auf Zeichen- oder Absatzebene auswirkt und ob er sich auf das Layout oder die Darstellung des Texts auswirkt. So können Sie besser bestimmen, welche Basisklassen Sie erweitern können und welche Schnittstellen Sie gegebenenfalls implementieren müssen. Die folgende Tabelle dient als Referenz:
Szenario | Klasse oder Benutzeroberfläche |
---|---|
Die Spanne wirkt sich auf den Text auf Zeichenebene aus. | CharacterStyle |
Die Spanne beeinflusst die Textdarstellung. | UpdateAppearance |
Die Spanne wirkt sich auf die Textmesswerte aus. | UpdateLayout |
Die Spanne wirkt sich auf den Text auf Absatzebene aus. | ParagraphStyle |
Wenn Sie beispielsweise einen benutzerdefinierten Span implementieren müssen, der Textgröße und -farbe ändert, erweitern Sie RelativeSizeSpan
. Durch Vererbung 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 Callback updateDrawState
ü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 können Sie erzielen, wenn Sie RelativeSizeSpan
und ForegroundColorSpan
auf den Text anwenden.
Span-Nutzung testen
Über die Schnittstelle Spanned
können Sie sowohl Spans festlegen als auch Spans aus Text abrufen. Implementieren Sie dabei einen Android JUnit-Test, um zu prüfen, ob die richtigen Spans an den richtigen Orten hinzugefügt werden. Die Beispiel-App für Textstile enthält eine Span, bei der Aufzählungspunkte durch Anhängen von BulletPointSpan
mit Markup ausgezeichnet werden. 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. Stellen Sie sich beispielsweise eine benutzerdefinierte Span-Implementierung vor, bei der Text ein Aufzählungspunkt vorangestellt wird. Für den Aufzählungspunkt ist eine bestimmte Größe und Farbe festgelegt und zwischen dem linken Rand des gezeichneten Bereichs und dem Aufzählungspunkt befindet sich eine Lücke.
Sie können das Verhalten dieser Klasse testen, indem Sie einen AndroidJUnit-Test implementieren. Achten Sie dabei auf Folgendes:
- Wenn Sie die Spanne korrekt anwenden, wird ein Aufzählungspunkt in der angegebenen Größe und Farbe auf dem Canvas angezeigt und der korrekte Abstand zwischen dem linken Rand und dem Aufzählungspunkt ist vorhanden.
- Wenn Sie den Span nicht anwenden, wird keines der benutzerdefinierten Funktionsweisen angezeigt.
Die Implementierung dieser Tests finden Sie im TextStyling-Beispiel auf GitHub.
Sie können Canvas-Interaktionen testen, indem Sie das Canvas simulieren, das Mock-Objekt an die Methode drawLeadingMargin()
übergeben und prüfen, ob die richtigen Methoden mit den richtigen Parametern aufgerufen werden.
Weitere Beispiele für Span-Tests finden Sie unter BulletPointSpanTest.
Best Practices für die Verwendung von Spans
Je nach Ihren Anforderungen gibt es mehrere speichereffiziente Möglichkeiten, um Text in einem TextView
festzulegen.
Spans anhängen oder entfernen, ohne den zugrunde liegenden Text zu ändern
TextView.setText()
enthält mehrere Überlastungen, die Spans unterschiedlich verarbeiten. Sie können beispielsweise das Textobjekt Spannable
mit dem folgenden Code festlegen:
Kotlin
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
Beim Aufrufen dieser Überlastung von setText()
erstellt TextView
eine Kopie von Spannable
als SpannedString
und speichert sie als CharSequence
im Arbeitsspeicher.
Das bedeutet, dass Ihr Text und die Spans unveränderlich sind. Wenn Sie also den Text oder die Spans aktualisieren müssen, erstellen Sie ein neues Spannable
-Objekt und rufen Sie setText()
noch einmal auf. Dadurch wird auch das Layout noch einmal gemessen und neu gezeichnet.
Um anzugeben, 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 sorgt der Parameter BufferType.SPANNABLE
dafür, dass TextView
eine SpannableString
erstellt. Das von TextView
beibehaltene Objekt CharSequence
hat jetzt änderbares Markup und unveränderlichen Text. Rufen Sie zum Aktualisieren eines Spans den Text als Spannable
ab und aktualisieren Sie die Spans dann nach Bedarf.
Wenn Sie Spans anhängen, trennen oder neu positionieren, wird TextView
automatisch aktualisiert, um die Änderung im Text widerzuspiegeln. Wenn Sie ein internes Attribut eines vorhandenen Spans ändern, rufen Sie invalidate()
auf, um die Darstellung zu ändern, oder requestLayout()
, um messwertbezogene Änderungen vorzunehmen.
Text in TextView mehrmals festlegen
In einigen Fällen, z. B. bei Verwendung eines RecyclerView.ViewHolder
, können Sie TextView
wiederverwenden und den Text mehrmals festlegen. Unabhängig davon, ob Sie BufferType
festlegen, erstellt TextView
standardmäßig eine Kopie des CharSequence
-Objekts und speichert es im Speicher. 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, wenn Sie neuen Text festlegen, ein neues Objekt erstellt.
Wenn Sie diesen Prozess besser steuern und die zusätzliche Erstellung von Objekten vermeiden möchten, können Sie Ihr eigenes Spannable.Factory
implementieren und newSpannable()
überschreiben.
Anstatt ein neues Textobjekt zu erstellen, können Sie das vorhandene CharSequence
als Spannable
umwandeln und 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 textView.setText(spannableObject, BufferType.SPANNABLE)
verwenden, wenn Sie den Text festlegen. Andernfalls wird die Quelle CharSequence
als Spanned
-Instanz erstellt und kann nicht in Spannable
umgewandelt werden. In diesem Fall gibt newSpannable()
eine ClassCastException
aus.
Nachdem Sie newSpannable()
überschrieben haben, weisen Sie TextView
an, die 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 die TextView
erhalten haben. Wenn Sie ein RecyclerView
verwenden, legen Sie das Factory
-Objekt fest, wenn Sie Ihre Ansichten zum ersten Mal aufblähen. Dadurch wird vermieden, dass zusätzliche Objekte erstellt werden, wenn RecyclerView
ein neues Element an ViewHolder
bindet.
Interne Span-Attribute ändern
Wenn Sie nur ein internes Attribut eines änderbaren Spans ändern müssen, z. B. die Farbe der Aufzählungszeichen in einem benutzerdefinierten Aufzählungspunkt, können Sie den Aufwand durch das mehrfache Aufrufen von setText()
vermeiden, indem Sie einen Verweis auf die erstellte Span beibehalten.
Wenn Sie den Span ändern müssen, können Sie den Verweis ändern und dann invalidate()
oder requestLayout()
für TextView
aufrufen, je nachdem, welchen Attributtyp Sie geändert haben.
Im folgenden Codebeispiel hat eine benutzerdefinierte Implementierung von 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(); } }); } }
Funktionen der Android KTX-Erweiterung verwenden
Android KTX enthält außerdem Erweiterungsfunktionen, die die Arbeit mit Spans erleichtern. Weitere Informationen finden Sie in der Dokumentation zum Paket androidx.core.text.