Odcinki to potężne obiekty znaczników, których możesz używać do stylizacji 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, tworzyć klikalne teksty, zmieniać ich rozmiar i rysować tekst w sposób niestandardowy. Za pomocą zakresów możesz 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 elementy, aby zastosować niestandardowy styl.
Tworzenie i stosowanie zakresu
Aby utworzyć zakres, możesz użyć jednej z klas wymienionych w tabeli poniżej. Klasy różnią się między sobą ze względu na to, czy sam tekst jest zmienny, czy znaczniki tekstowe są zmienne i jaka jest podstawowa struktura danych zawierająca dane zakresu.
Kategoria | tekst zmienny, | Zmienny znacznik | Struktura danych |
---|---|---|---|
SpannedString |
Nie | Nie | Tablica liniowa |
SpannableString |
Nie | Tak | Tablica liniowa |
SpannableStringBuilder |
Tak | Tak | Drzewo interwałowe |
Wszystkie 3 klasy rozszerzają interfejs Spanned
. Urządzenia SpannableString
i SpannableStringBuilder
rozszerzają też interfejs Spannable
.
Aby zdecydować, który z nich wybrać:
- Jeśli po utworzeniu nie modyfikujesz tekstu ani kodu, użyj opcji
SpannedString
. - Jeśli musisz dołączyć niewielką liczbę zakresów do pojedynczego obiektu tekstowego, a sam tekst jest tylko do odczytu, użyj
SpannableString
. - Jeśli po utworzeniu tekstu chcesz go zmodyfikować i dołączyć do niego zakresy, użyj
SpannableStringBuilder
. - Jeśli chcesz dołączyć dużą liczbę zakresów do obiektu tekstowego, niezależnie od tego, czy sam tekst jest tylko do odczytu, użyj
SpannableStringBuilder
.
Aby zastosować zakres, wywołaj funkcję 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 obrębie zakresu, zakres zostanie automatycznie rozszerzony, aby uwzględnić wstawiony tekst. Podczas wstawiania tekstu na granicach zakresu, czyli w indeksach na początku lub na końcu, parametr flagi określa, czy zakres ma się rozszerzyć, aby objąć wstawiany tekst. Użyj flagi Spannable.SPAN_EXCLUSIVE_INCLUSIVE
, aby uwzględnić wstawiany tekst, i użyj flagi Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
, aby wykluczyć wstawiany tekst.
Ten przykład pokazuje, jak dołączyć parametr ForegroundColorSpan
do ciągu znaków:
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 ustawiany za pomocą atrybutu Spannable.SPAN_EXCLUSIVE_INCLUSIVE
, rozszerza się on, aby uwzględnić wstawiony tekst na granicach zakresu, 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
, element rozszerzy się, aby uwzględnić dodatkowy tekst.
Do tego samego tekstu możesz dołączyć wiele zakresów. Z przykładu poniżej dowiesz się, 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 elementów Androida
Android udostępnia ponad 20 typów elementów span w pakiecie android.text.style. Android dzieli zakresy na 2 główne sposoby:
- Jak element span wpływa na tekst: element span może wpływać na wygląd tekstu lub na dane dotyczące tekstu.
- Zakres zakresu: niektóre zakresy można stosować do poszczególnych znaków, a inne muszą być stosowane do całego akapitu.

W następnych sekcjach opisujemy te kategorie bardziej szczegółowo.
Odcinki wpływające na wygląd tekstu
Niektóre zakresy stosowane na poziomie znaku wpływają na wygląd tekstu, na przykład poprzez zmianę koloru tekstu lub tła oraz dodanie podkreślenia lub przekreślenia. Te zakresy rozszerzają klasę CharacterStyle
.
Ten przykładowy kod pokazuje, jak zastosować znak UnderlineSpan
do podkreślenia tekstu:
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
.
Przedziały, które wpływają tylko na wygląd tekstu, powodują jego ponowne wyświetlenie bez ponownego obliczania układu. Te zakresy implementują UpdateAppearance
i rozszerzają CharacterStyle
.
Podklasy CharacterStyle
określają sposób rysowania tekstu, zapewniając dostęp do funkcji aktualizacji TextPaint
.
Przedziały, które wpływają na dane dotyczące tekstu
Inne zakresy stosowane na poziomie znaku wpływają na dane dotyczące tekstu, takie jak wysokość wiersza i rozmiar tekstu. Te zakresy rozszerzają klasę MetricAffectingSpan
.
Ten przykład kodu tworzy 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 rozpiętości, która wpływa na dane tekstowe, powoduje, że obiekt obserwujący ponownie mierzy tekst w celu prawidłowego układu i renderowania. Na przykład zmiana rozmiaru tekstu może spowodować, że słowa pojawią się na różnych wierszach. Zastosowanie powyższego zakresu powoduje ponowne pomiar, ponowne obliczenie układu tekstu i ponowne narysowanie tekstu.
Przedziały, które wpływają na dane tekstowe, rozszerzają klasę MetricAffectingSpan
, która jest abstrakcyjną klasą umożliwiającą podklasom definiowanie wpływu przedziału na pomiar tekstu poprzez udostępnianie dostępu do funkcji TextPaint
. Ponieważ klasa MetricAffectingSpan
rozszerza klasę CharacterStyle
, podklasy wpływają na wygląd tekstu na poziomie znaków.
zakresy, które wpływają na akapity;
Element span może też wpływać na tekst na poziomie akapitu, na przykład zmieniać wyrównanie lub marginesy bloku tekstu. Zakresy, które wpływają na całe akapity, implementują ParagraphStyle
. Aby użyć tych zakresów, musisz je dołączyć 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.
Rysunek 8 pokazuje, jak Android oddziela akapity w tekście.

\n
).
W tym przykładzie kodu do paragrafu zastosowano QuoteSpan
. Pamiętaj, że jeśli dodasz element span w dowolnym miejscu poza początkiem lub końcem 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
zastosowano do akapitu.
Tworzenie zakresów niestandardowych
Jeśli potrzebujesz więcej funkcji niż te dostępne w dotychczasowych elementach span w Androidzie, możesz wdrożyć element span niestandardowy. Podczas implementowania własnego zakresu określ, czy ma on wpływ na tekst na poziomie znaku czy akapitu, a także czy wpływa na układ lub wygląd tekstu. Pomoże Ci to określić, które klasy podstawowe możesz rozszerzać i które interfejsy może być konieczne wdrożyć. W tym celu możesz skorzystać z tabeli:
Scenariusz | Klasa lub interfejs |
---|---|
Zakres ma wpływ na tekst na poziomie znaków. | CharacterStyle |
Wpływa on na wygląd tekstu. | UpdateAppearance |
Rozpiętość wpływa na dane dotyczące tekstu. | UpdateLayout |
Zakres ma wpływ na tekst na poziomie akapitu. | ParagraphStyle |
Jeśli na przykład chcesz zastosować element niestandardowy, który zmienia rozmiar i kolor tekstu, użyj elementu RelativeSizeSpan
. Dzięki dziedziczeniu klasa RelativeSizeSpan
rozszerza klasę CharacterStyle
i implementuje 2 interfejsy Update
. Ta klasa udostępnia już funkcje zwracane przez updateDrawState
i updateMeasureState
, więc możesz je zastąpić, aby zaimplementować własne zachowanie. Poniższy kod tworzy element span niestandardowy, który rozszerza element RelativeSizeSpan
i zastępuje wywołanie updateDrawState
, aby ustawić kolor elementu 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ć element niestandardowy. Ten sam efekt możesz uzyskać, stosując do tekstu RelativeSizeSpan
i ForegroundColorSpan
.
Testowanie stosowania zakresu
Interfejs Spanned
umożliwia zarówno ustawianie zakresów, jak i pobieranie ich z tekstu. Podczas testowania zaimplementuj test JUnit na Androida, aby sprawdzić, czy w odpowiednich miejscach zostały dodane odpowiednie zakresy. Aplikacja do stylizacji tekstu zawiera element span, który stosuje znaczniki do punktów wypunktowanych, dołączając do tekstu znak BulletPointSpan
. Poniższy przykład kodu pokazuje, jak sprawdzić, czy punkty wyliczenia 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 w elemencie Canvas
pojawiają się prawidłowe elementy. Rozważ na przykład implementację elementu span, który dodaje do tekstu punkt. Punkt ma określony rozmiar i kolor, a między lewym marginesem obszaru rysunkowego a punktem znajduje się odstęp.
Aby przetestować działanie tej klasy, zaimplementuj test AndroidJUnit, sprawdzając:
- Jeśli prawidłowo zastosujesz element span, na kanwie pojawi się punkt wyliczenia o określonym rozmiarze i kolorze, a między lewym marginesem a punktem wyliczenia będzie odpowiedni odstęp.
- Jeśli nie zastosujesz zakresu, nie pojawi się żadne zachowanie niestandardowe.
Implementację tych testów znajdziesz w przykładowym pliku TextStyling na GitHubie.
Interakcje z Canvas możesz testować, tworząc jego symulację i przekazując symulowane obiekty do metody drawLeadingMargin()
, a następnie sprawdzając, czy wywoływane są prawidłowe metody z odpowiednimi parametrami.
Więcej przykładów testów zakresu znajdziesz w BulletPointSpanTest.
Sprawdzone metody korzystania z zakresów
W zależności od potrzeb możesz ustawić tekst w elementach TextView
na kilka sposobów, które nie wymagają dużej ilości pamięci.
Dołączanie i odłączanie zakresu bez zmiany tekstu źródłowego
TextView.setText()
zawiera wiele przeciążeń, które inaczej obsługują zakresy. Możesz na przykład ustawić obiekt tekstowy Spannable
za pomocą tego kodu:
Kotlin
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
Gdy wywołujesz tę przeciążoną wersję funkcji setText()
, funkcja TextView
tworzy kopię obiektu Spannable
jako obiektu SpannedString
i przechowuje go w pamięci jako obiektu CharSequence
.
Oznacza to, że tekst i zakresy są niezmienne. Jeśli więc chcesz zaktualizować tekst lub zakresy, utwórz nowy obiekt Spannable
i ponownie wywołaj funkcję setText()
, co spowoduje ponowne pomiar i narysowanie układu.
Aby wskazać, że zakresy muszą być zmienne, możesz zamiast tego użyć setText(CharSequence text, TextView.BufferType
type)
, jak 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 obiekt TextView
tworzy obiekt SpannableString
, a obiekt CharSequence
przechowywany przez obiekt TextView
zawiera teraz znaczniki, które można zmienić, oraz tekst, którego nie można zmienić. Aby zaktualizować zakres, pobierz tekst jako Spannable
, a potem w razie potrzeby zaktualizuj zakresy.
Gdy dodasz, usuniesz lub przemieścisz zakresy, TextView
zostanie automatycznie zaktualizowany, aby uwzględnić zmiany w tekście. Jeśli zmienisz wewnętrzny atrybut istniejącego elementu span, wywołaj metodę invalidate()
, aby wprowadzić zmiany związane z wyglądem, lub requestLayout()
, aby wprowadzić zmiany związane z danymi.
Wielokrotne ustawianie tekstu w TextView
W niektórych przypadkach, np. podczas korzystania z RecyclerView.ViewHolder
, możesz chcieć ponownie użyć komponentu TextView
i ustawić tekst kilka razy. Domyślnie, niezależnie od tego, czy ustawisz parametr BufferType
, funkcja TextView
tworzy kopię obiektu CharSequence
i przechowuje ją w pamięci. Dzięki temu wszystkie zmiany w TextView
są celowe – nie możesz zaktualizować oryginalnego obiektu CharSequence
, aby zmienić tekst. Oznacza to, że za każdym razem, gdy ustawisz nowy tekst, funkcja TextView
utworzy nowy obiekt.
Jeśli chcesz mieć większą kontrolę nad tym procesem i uniknąć tworzenia dodatkowych obiektów, możesz wdrożyć własne Spannable.Factory
i zastąpić newSpannable()
.
Zamiast tworzyć nowy obiekt tekstowy, możesz zmienić typ istniejącego obiektu CharSequence
na Spannable
i zwrócić go, jak 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żyć wartości textView.setText(spannableObject, BufferType.SPANNABLE)
. W przeciwnym razie źródło CharSequence
jest tworzone jako instancja Spanned
i nie można go rzutować na typ Spannable
, co powoduje, że funkcja newSpannable()
zgłasza błąd ClassCastException
.
Po zastąpieniu wartości newSpannable()
powiedz TextView
, aby użył nowego Factory
:
Kotlin
textView.setSpannableFactory(spannableFactory)
Java
textView.setSpannableFactory(spannableFactory);
Ustaw obiekt Spannable.Factory
raz, zaraz po uzyskaniu odwołania do TextView
. Jeśli używasz obiektu RecyclerView
, ustaw obiekt Factory
, gdy po raz pierwszy napełniasz widoki. Zapobiega to tworzeniu dodatkowych obiektów, gdy RecyclerView
wiąże nowy element z Twoim ViewHolder
.
Zmiana atrybutów wewnętrznego zakresu
Jeśli musisz zmienić tylko wewnętrzny atrybut zmiennego elementu, np. kolor kropki w elementach niestandardowych, możesz uniknąć nadmiernego obciążenia wywoływania funkcji setText()
, zachowując odniesienie do elementu w momencie jego tworzenia.
Jeśli chcesz zmodyfikować zakres, możesz zmodyfikować odwołanie, a potem wywołać funkcję invalidate()
lub requestLayout()
w elementzie TextView
, w zależności od typu zmienionego atrybutu.
W tym przykładzie kodu niestandardowy punkt 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 rozszerzenia KTX na Androida
Android KTX zawiera też funkcje rozszerzeń, które ułatwiają pracę z przedziałami. Więcej informacji znajdziesz w dokumentacji pakietu androidx.core.text.