Spany to zaawansowane obiekty znaczników, których możesz używać do stylizowania tekstu na poziomie znaku lub akapitu. Dołączając zakresy do obiektów tekstowych, możesz zmieniać tekst na różne sposoby, np. dodawać kolor, sprawiać, że tekst będzie klikalny, skalować rozmiar tekstu i rysować tekst w niestandardowy sposób. Zakresy mogą też zmieniać właściwości TextPaint
, rysować na Canvas
i zmieniać układ tekstu.
Android udostępnia kilka typów zakresów, które obejmują różne typowe wzorce stylizacji tekstu. Możesz też tworzyć własne zakresy, aby stosować niestandardowe style.
Tworzenie i stosowanie zakresu
Aby utworzyć zakres, możesz użyć jednej z klas wymienionych w tabeli poniżej. Klasy różnią się w zależności od tego, czy sam tekst jest modyfikowalny, czy modyfikowalny jest znacznik tekstu oraz jaka struktura danych zawiera dane zakresu.
Kategoria | Tekst z możliwością zmiany | Znaczniki modyfikowalne | Struktura danych |
---|---|---|---|
SpannedString |
Nie | Nie | Tablica liniowa |
SpannableString |
Nie | Tak | Tablica liniowa |
SpannableStringBuilder |
Tak | Tak | Drzewo przedziałowe |
Wszystkie 3 klasy rozszerzają interfejs Spanned
. SpannableString
i SpannableStringBuilder
rozszerzają też interfejs Spannable
.
Aby zdecydować, który z nich wybrać:
- Jeśli po utworzeniu nie modyfikujesz tekstu ani znaczników, użyj
SpannedString
. - Jeśli musisz dołączyć niewielką liczbę zakresów do jednego obiektu tekstowego, a sam tekst jest tylko do odczytu, użyj
SpannableString
. - Jeśli po utworzeniu tekstu musisz go zmodyfikować i dołączyć do niego zakresy, użyj znaku
SpannableStringBuilder
. - Jeśli musisz dołączyć do obiektu tekstowego dużą liczbę zakresów, niezależnie od tego, czy sam tekst jest tylko do odczytu, użyj
SpannableStringBuilder
.
Aby zastosować zakres, wywołaj setSpan(Object _what_, int _start_, int _end_, int
_flags_)
na obiekcie Spannable
. Parametr what odnosi się do zakresu, który stosujesz do tekstu, a parametry start i end wskazują część tekstu, do której stosujesz zakres.
Jeśli wstawisz tekst w zakresie obejmującym fragment, zakres automatycznie się rozszerzy, aby uwzględnić wstawiony tekst. Podczas wstawiania tekstu na granicach zakresu, czyli na indeksach początkowym lub końcowym, parametr flags określa, czy zakres ma się rozszerzyć, aby uwzględnić wstawiony tekst. Użyj flagi Spannable.SPAN_EXCLUSIVE_INCLUSIVE
, aby uwzględnić wstawiony tekst, a flagi Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
, aby go wykluczyć.
Poniższy przykład pokazuje, jak dołączyć ForegroundColorSpan
do ciągu:
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 );

ForegroundColorSpan
.
Ponieważ zakres jest ustawiony za pomocą Spannable.SPAN_EXCLUSIVE_INCLUSIVE
, rozszerza się on o wstawiony tekst na jego granicach, jak pokazano w tym przykładzie:
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)");

Spannable.SPAN_EXCLUSIVE_INCLUSIVE
.
Do tego samego tekstu możesz dołączyć wiele zakresów. Przykład poniżej pokazuje, jak utworzyć tekst pogrubiony i czerwony:
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)
i StyleSpan(BOLD)
.
Typy zakresów Androida
Android udostępnia ponad 20 typów zakresów w pakiecie android.text.style. Android dzieli zakresy na 2 główne sposoby:
- Jak zakres wpływa na tekst: zakres może wpływać na wygląd tekstu lub jego dane.
- Zakres: niektóre zakresy można stosować do poszczególnych znaków, a inne muszą być stosowane do całego akapitu.

Więcej informacji o tych kategoriach znajdziesz w kolejnych sekcjach.
Zakresy wpływające na wygląd tekstu
Niektóre zakresy, które są stosowane na poziomie znaku, wpływają na wygląd tekstu, np. zmieniają kolor tekstu lub tła oraz dodają podkreślenia lub przekreślenia. Te zakresy rozszerzają klasę CharacterStyle
.
Poniższy przykład kodu pokazuje, jak zastosować znak UnderlineSpan
, aby podkreślić tekst:
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
.
Zakresy, które wpływają tylko na wygląd tekstu, powodują ponowne narysowanie tekstu bez ponownego obliczania układu. Te zakresy implementują interfejs UpdateAppearance
i rozszerzają interfejs CharacterStyle
.
CharacterStyle
podklasy określają sposób rysowania tekstu, zapewniając dostęp do aktualizacji TextPaint
.
Zakresy, które wpływają na dane tekstowe
Inne zakresy, które mają zastosowanie na poziomie znaku, wpływają na dane tekstowe, takie jak wysokość wiersza i rozmiar tekstu. Te zakresy rozszerzają klasę MetricAffectingSpan
.
Ten przykład kodu tworzy element RelativeSizeSpan
, który zwiększa rozmiar tekstu o 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
.
Zastosowanie zakresu, który wpływa na dane tekstowe, powoduje ponowne zmierzenie tekstu przez obiekt obserwujący w celu prawidłowego układu i renderowania. Na przykład zmiana rozmiaru tekstu może spowodować, że słowa pojawią się w innych wierszach. Zastosowanie powyższego zakresu powoduje ponowne pomiary, ponowne obliczenie układu tekstu i ponowne narysowanie tekstu.
Zakresy, które wpływają na dane tekstowe, rozszerzają klasę MetricAffectingSpan
, czyli klasę abstrakcyjną, która umożliwia podklasom określanie, jak zakres wpływa na pomiar tekstu, poprzez zapewnienie dostępu do TextPaint
. Ponieważ klasa MetricAffectingSpan
rozszerza klasę CharacterStyle
, podklasy wpływają na wygląd tekstu na poziomie znaku.
Zakresy wpływające na akapity
Zakres może też wpływać na tekst na poziomie akapitu, np. zmieniać wyrównanie lub margines bloku tekstu. Zakresy, które wpływają na całe akapity, implementują interfejs ParagraphStyle
. Aby użyć tych zakresów, dołącz je do całego akapitu, z wyjątkiem znaku nowego wiersza na końcu. Jeśli spróbujesz zastosować zakres akapitu do czegoś innego niż cały akapit, Android w ogóle nie zastosuje zakresu.
Ilustracja 8 pokazuje, jak Android oddziela akapity w tekście.

\n
).
Poniższy przykład kodu stosuje element QuoteSpan
do akapitu. Pamiętaj, że jeśli dołączysz zakres do pozycji innej niż początek lub koniec akapitu, Android w ogóle nie zastosuje stylu.
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
zastosowana do akapitu.
Tworzenie niestandardowych zakresów
Jeśli potrzebujesz więcej funkcji niż te, które są dostępne w przypadku istniejących zakresów Androida, możesz wdrożyć niestandardowy zakres. Podczas wdrażania własnego zakresu zdecyduj, czy ma on wpływać na tekst na poziomie znaku czy akapitu, a także czy ma wpływać na układ czy wygląd tekstu. Pomoże Ci to określić, które klasy bazowe możesz rozszerzyć i które interfejsy musisz zaimplementować. Skorzystaj z tej tabeli:
Scenariusz | Klasa lub interfejs |
---|---|
Zakres wpływa na tekst na poziomie znaku. | CharacterStyle |
Zakres wpływa na wygląd tekstu. | UpdateAppearance |
Zakres ma wpływ na dane tekstowe. | UpdateLayout |
Zakres wpływa na tekst na poziomie akapitu. | ParagraphStyle |
Jeśli na przykład chcesz wdrożyć niestandardowy zakres, który modyfikuje rozmiar i kolor tekstu, rozszerz klasę RelativeSizeSpan
. Dzięki dziedziczeniu klasa RelativeSizeSpan
rozszerza CharacterStyle
i implementuje 2 interfejsy Update
. Ta klasa udostępnia już wywołania zwrotne dla updateDrawState
i updateMeasureState
, więc możesz je zastąpić, aby wdrożyć niestandardowe działanie. Poniższy kod tworzy niestandardowy zakres, który rozszerza RelativeSizeSpan
i zastępuje wywołanie zwrotne updateDrawState
, aby ustawić kolor 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); } }
Ten przykład pokazuje, jak utworzyć niestandardowy zakres. Ten sam efekt możesz uzyskać, stosując do tekstu RelativeSizeSpan
i ForegroundColorSpan
.
Wykorzystanie zakresu testu
Interfejs Spanned
umożliwia ustawianie zakresów i pobieranie ich z tekstu. Podczas testowania zaimplementuj test Android JUnit, aby sprawdzić, czy prawidłowe zakresy są dodawane we właściwych miejscach. Przykładowa aplikacja do stylizowania tekstu zawiera zakres, który stosuje znaczniki do punktów, dołączając do tekstu element BulletPointSpan
. Poniższy przykład kodu pokazuje, jak sprawdzić, czy punkty są wyświetlane zgodnie z oczekiwaniami:
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)); }
Więcej przykładów testów znajdziesz w MarkdownBuilderTest na GitHubie.
Testowanie niestandardowych zakresów
Podczas testowania zakresów sprawdź, czy element TextPaint
zawiera oczekiwane modyfikacje i czy na stronie Canvas
wyświetlają się prawidłowe elementy. Rozważmy na przykład niestandardową implementację zakresu, która dodaje punkt do tekstu. Punktor ma określony rozmiar i kolor, a między lewym marginesem obszaru rysowania a punktorem jest odstęp.
Działanie tej klasy możesz przetestować, implementując test AndroidJUnit, który sprawdzi:
- Jeśli prawidłowo zastosujesz zakres, na obszarze roboczym pojawi się punkt o określonym rozmiarze i kolorze, a między lewym marginesem a punktem będzie odpowiednia spacja.
- Jeśli nie zastosujesz zakresu, żadne niestandardowe zachowanie się nie pojawi.
Implementację tych testów możesz zobaczyć w przykładzie TextStyling na GitHubie.
Możesz przetestować interakcje z elementem Canvas, tworząc jego imitację, przekazując ją do metody drawLeadingMargin()
i sprawdzając, czy wywoływane są odpowiednie metody z prawidłowymi parametrami.
Więcej przykładów testów zakresu znajdziesz w klasie BulletPointSpanTest.
Sprawdzone metody korzystania z zakresów
Istnieje kilka sposobów na ustawienie tekstu w TextView
, które nie wymagają dużej ilości pamięci. Zależy to od Twoich potrzeb.
Dołączanie i odłączanie zakresu bez zmiany tekstu źródłowego
TextView.setText()
zawiera wiele przeciążeń, które różnie obsługują zakresy. Możesz na przykład ustawić obiekt tekstowy Spannable
za pomocą tego kodu:
Kotlin
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
Podczas wywoływania tego przeciążenia funkcji setText()
funkcja TextView
tworzy kopię obiektu Spannable
jako SpannedString
i przechowuje ją w pamięci jako CharSequence
.
Oznacza to, że tekst i zakresy są niezmienne, więc gdy chcesz zaktualizować tekst lub zakresy, utwórz nowy obiekt Spannable
i ponownie wywołaj setText()
. Spowoduje to również ponowne pomiary i ponowne narysowanie układu.
Aby wskazać, że zakresy muszą być modyfikowalne, możesz zamiast tego użyć
setText(CharSequence text, TextView.BufferType
type)
,
jak pokazano w tym przykładzie:
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);
W tym przykładzie parametr BufferType.SPANNABLE
powoduje, że TextView
tworzy SpannableString
, a obiekt CharSequence
przechowywany przez TextView
ma teraz zmienne znaczniki i niezmienny tekst. Aby zaktualizować zakres, pobierz tekst jako Spannable
, a następnie w razie potrzeby zaktualizuj zakresy.
Gdy dołączysz, odłączysz lub zmienisz położenie zakresów, TextView
automatycznie zaktualizuje się, aby odzwierciedlić zmianę w tekście. Jeśli zmienisz atrybut wewnętrzny istniejącego zakresu, wywołaj metodę invalidate()
, aby wprowadzić zmiany związane z wyglądem, lub metodę requestLayout()
, aby wprowadzić zmiany związane z danymi.
Wielokrotne ustawianie tekstu w obiekcie TextView
W niektórych przypadkach, np. podczas korzystania z RecyclerView.ViewHolder
, możesz chcieć ponownie użyć TextView
i kilkakrotnie ustawić tekst. Domyślnie, niezależnie od tego, czy ustawisz BufferType
, funkcja TextView
tworzy kopię obiektu CharSequence
i przechowuje ją w pamięci. Dzięki temu wszystkie aktualizacje są TextView
celowe – nie możesz zaktualizować oryginalnego CharSequence
obiektu, aby zaktualizować tekst. Oznacza to, że za każdym razem, gdy ustawiasz nowy tekst, TextView
tworzy nowy obiekt.
Jeśli chcesz mieć większą kontrolę nad tym procesem i uniknąć tworzenia dodatkowych obiektów, możesz zaimplementować własną funkcję Spannable.Factory
i zastąpić funkcję newSpannable()
.
Zamiast tworzyć nowy obiekt tekstowy, możesz przekształcić i zwrócić istniejący obiekt CharSequence
jako Spannable
, jak pokazano w tym przykładzie:
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; } };
Podczas ustawiania tekstu musisz używać znaku textView.setText(spannableObject, BufferType.SPANNABLE)
. W przeciwnym razie źródło CharSequence
jest tworzone jako instancja Spanned
i nie można jej przekształcić w Spannable
, co powoduje, że newSpannable()
zgłasza wyjątek ClassCastException
.
Po zastąpieniu newSpannable()
poinformuj TextView
, aby używał nowego Factory
:
Kotlin
textView.setSpannableFactory(spannableFactory)
Java
textView.setSpannableFactory(spannableFactory);
Ustaw obiekt Spannable.Factory
tylko raz, zaraz po uzyskaniu odwołania do TextView
. Jeśli używasz RecyclerView
, ustaw obiekt Factory
podczas pierwszego rozszerzania widoków. Pozwala to uniknąć tworzenia dodatkowych obiektów, gdy RecyclerView
wiąże nowy produkt z ViewHolder
.
Zmienianie atrybutów wewnętrznego zakresu
Jeśli musisz zmienić tylko atrybut wewnętrzny zakresu modyfikowalnego, np. kolor punktora w zakresie niestandardowego punktora, możesz uniknąć obciążenia związanego z wielokrotnym wywoływaniem funkcji setText()
, zachowując odwołanie do zakresu podczas jego tworzenia.
Gdy musisz zmodyfikować zakres, możesz zmodyfikować odwołanie, a następnie wywołać funkcję
invalidate()
lub requestLayout()
na obiekcie TextView
w zależności od typu zmienionego atrybutu.
W przykładzie kodu poniżej niestandardowy punkt wypunktowania ma domyślny kolor czerwony, który zmienia się na szary po kliknięciu przycisku:
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(); } }); } }
Korzystanie z funkcji rozszerzających Androida KTX
Android KTX zawiera też funkcje rozszerzające, które ułatwiają pracę z zakresami. Więcej informacji znajdziesz w dokumentacji pakietu androidx.core.text.