Spany to zaawansowane obiekty znaczników, których możesz używać do określania stylu tekstu na poziomie znaków lub akapitu. Dołączając spany do obiektów tekstowych, możesz modyfikować tekst na różne sposoby, m.in. dodając kolory, umożliwiając klikanie tekstu, skalując jego rozmiar i rysując tekst w niestandardowy sposób. Spany mogą też zmieniać właściwości TextPaint
, rysować w elementach Canvas
i zmieniać układ tekstu.
Android oferuje kilka typów zakresów obejmujących różne typowe wzorce stylów tekstu. Możesz też tworzyć własne zakresy i stosować niestandardowe style.
Tworzenie i stosowanie spanu
Aby utworzyć span, możesz użyć jednej z zajęć wymienionych w poniższej tabeli. Klasy różnią się w zależności od tego, czy sam tekst jest zmienny, czy znaczniki tekstowe są zmienne i jaka bazowa struktura danych zawiera dane spanów.
Kategoria | Tekst zmienny | Zmienne znaczniki | Struktura danych |
---|---|---|---|
SpannedString |
Nie | Nie | Tablica liniowa |
SpannableString |
Nie | Tak | Tablica liniowa |
SpannableStringBuilder |
Tak | Tak | Drzewo interwałów |
Wszystkie 3 klasy poszerzają interfejs Spanned
. SpannableString
i SpannableStringBuilder
rozszerzają też interfejs Spannable
.
Oto jak zdecydować, którego z nich użyć:
- Jeśli po utworzeniu nie modyfikujesz tekstu ani znaczników, użyj polecenia
SpannedString
. - Jeśli chcesz dołączyć niewielką liczbę rozpiętości do jednego obiektu tekstowego, a tekst jest tylko do odczytu, użyj
SpannableString
. - Jeśli musisz zmienić tekst po jego utworzeniu i chcesz dołączyć do niego spany, użyj polecenia
SpannableStringBuilder
. - Jeśli chcesz dołączyć do obiektu tekstowego dużą liczbę rozpiętości, niezależnie od tego, czy tekst jest tylko do odczytu, użyj
SpannableStringBuilder
.
Aby zastosować span, wywołaj setSpan(Object _what_, int _start_, int _end_, int
_flags_)
na obiekcie Spannable
. Parametr what odnosi się do zakresu, który ma być stosowany do tekstu, a parametry start i end wskazują fragment tekstu, do którego ma on zastosowanie.
Jeśli wstawisz tekst wewnątrz granic spanu, span zostanie automatycznie rozwinięty, aby uwzględnić wstawiony tekst. Gdy wstawiasz tekst na granicach zakresu, czyli w indeksach start lub end, parametr flags określa, czy span ma zostać rozszerzony, by uwzględnić wstawiony tekst. Użyj flagi Spannable.SPAN_EXCLUSIVE_INCLUSIVE
, aby uwzględnić wstawiony tekst, i Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
, aby go wykluczyć.
Poniższy przykład pokazuje, jak dołączyć 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 );
Rozpiętość jest ustawiana za pomocą właściwości Spannable.SPAN_EXCLUSIVE_INCLUSIVE
, dlatego jest ona poszerzona o wstawiony tekst znajdujący się przy granicach spanu, jak 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)");
Do tego samego tekstu możesz dołączyć wiele elementów „span”. Poniższy przykład pokazuje, jak utworzyć pogrubiony i czerwony tekst:
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 );
Typy spanów na Androidzie
Android zapewnia w pakiecie android.text.style ponad 20 typów spanów. Android kategoryzuje spany na 2 główne sposoby:
- Wpływ spanu na tekst: może on wpływać na wygląd tekstu lub dane tekstu.
- Zakres spanu: niektóre spany można stosować do pojedynczych znaków, a inne do całego akapitu.
W poniższych sekcjach opisano te kategorie bardziej szczegółowo.
Spaliny, które wpływają na wygląd tekstu
Niektóre rozpiętości stosowane na poziomie znaków wpływają na wygląd tekstu, np. zmiana koloru tekstu lub tła czy dodanie podkreśleń lub przekreśleń. Te spany rozszerzają klasę CharacterStyle
.
Poniższy przykładowy kod pokazuje, jak zastosować element 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);
Rozpiętości, które mają wpływ tylko na wygląd tekstu, powodują ponowne przerysowanie tekstu bez konieczności ponownego obliczania układu. Te spany implementują UpdateAppearance
i rozszerzają zakres CharacterStyle
.
Podklasy CharacterStyle
określają, jak rysować tekst, zapewniając dostęp do aktualizowania klasy TextPaint
.
Spany, które wpływają na dane dotyczące tekstu
Inne rozpiętości stosowane na poziomie znaków wpływają na dane tekstowe, takie jak wysokość wiersza i rozmiar tekstu. Te spany rozszerzają klasę MetricAffectingSpan
.
Ten przykładowy kod 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);
Zastosowanie spanu, który ma wpływ na dane tekstowe, powoduje, że obserwowany obiekt ponownie mierzy tekst, aby zapewnić jego prawidłowy układ i renderowanie. Na przykład zmiana rozmiaru tekstu może spowodować, że słowa pojawią się w innych wierszach. Zastosowanie poprzedniego rozpiętości wymaga ponownego pomiaru oraz obliczeń układu tekstu i przerysowania tekstu.
Spany, które wpływają na dane tekstowe, poszerzają klasę MetricAffectingSpan
– abstrakcyjną klasę, która pozwala podklasom określać sposób, w jaki rozpiętość wpływa na pomiar tekstu, zapewniając dostęp do klasy TextPaint
. MetricAffectingSpan
rozszerza CharacterSpan
, więc na wygląd tekstu na poziomie znaków wpływają podklasy.
Rozpiętości mające wpływ na akapity
Rozpiętość może też wpływać na tekst na poziomie akapitu, na przykład przez zmianę wyrównania lub marginesu bloku tekstu. Spany, które mają wpływ na całe akapity, stosują ParagraphStyle
. Aby użyć tych spanów, musisz je dołączyć do całego akapitu z wyłączeniem nowego wiersza na końcu. Gdy próbujesz zastosować rozpiętość akapitu do czegoś innego niż
cały akapit, Android w ogóle jej nie zastosuje.
Rysunek 8 pokazuje, w jaki sposób Android dzieli akapity w tekście.
W tym przykładzie kodu do akapitu zastosowano tag QuoteSpan
. Pamiętaj, że jeśli dołączysz span do dowolnej innej pozycji niż początek lub koniec akapitu, Android w ogóle nie zastosuje tego 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);
Utwórz niestandardowe spany
Jeśli potrzebujesz większej funkcjonalności niż ta dostępna w istniejących spanach na Androida, możesz wdrożyć span niestandardowy. Gdy implementujesz własny span, zastanów się, czy ma on wpływ na tekst na poziomie znaku czy akapitu, i czy ma wpływ na układ lub wygląd tekstu. Pomaga to określić, które klasy podstawowe możesz rozszerzyć i które interfejsy należy zaimplementować. Skorzystaj z tej tabeli:
Scenariusz | Klasa lub interfejs |
---|---|
Rozpiętość wpływa na tekst na poziomie znaków. | CharacterStyle |
span wpływa na wygląd tekstu. | UpdateAppearance |
Rozpiętość wpływa na dane tekstowe. | UpdateLayout |
Rozpiętość wpływa na tekst na poziomie akapitu. | ParagraphStyle |
Jeśli na przykład musisz wdrożyć niestandardowe rozpiętość, która zmienia rozmiar i kolor tekstu, rozszerz element RelativeSizeSpan
. Dzięki dziedziczeniu RelativeSizeSpan
rozszerza zakres CharacterStyle
i implementuje 2 interfejsy Update
. Ponieważ ta klasa udostępnia już wywołania zwrotne dla updateDrawState
i updateMeasureState
, możesz je zastąpić, aby wdrożyć swoje działanie niestandardowe. Ten kod tworzy niestandardowy zakres, który rozszerza zakres RelativeSizeSpan
i zastępuje wywołanie zwrotne updateDrawState
, aby ustawić kolor pola 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ć niestandardowe spany. Możesz uzyskać ten sam efekt, dodając do tekstu RelativeSizeSpan
i ForegroundColorSpan
.
Testuj wykorzystanie spanów
Interfejs Spanned
umożliwia zarówno ustawianie spanów, jak i ich pobieranie z tekstu. Podczas testowania zaimplementuj test JUnit (Android), aby sprawdzić, czy prawidłowe spany są dodane w prawidłowych lokalizacjach. Przykładowa aplikacja do stosowania stylów tekstu zawiera rozpiętość, która pozwala zastosować znaczniki do punktorów, dołączając do tekstu BulletPointSpan
. Z tego przykładowego kodu dowiesz się, jak sprawdzić, czy listy punktowane wyświetlają się 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 na stronie MarkdownBuilderTest w GitHubie.
Przetestuj niestandardowe spany
Podczas testowania spanów sprawdź, czy TextPaint
zawiera oczekiwane modyfikacje i czy Canvas
zawiera właściwe elementy. Rozważmy np. implementację niestandardowego spanu, która będzie dołączać punktor przed tekstem. Punktor ma określony rozmiar i kolor, a między lewym marginesem obszaru możliwego do rysowania a punktorem znajduje się przerwa.
Możesz przetestować działanie tej klasy, implementując test AndroidJUnit i sprawdzając, czy:
- Jeśli prawidłowo zastosujesz rozpiętość, na obszarze roboczym pojawi się punktor o podanym rozmiarze i kolorze, a między jego lewym marginesem a punktorem pojawi się odpowiednia przestrzeń.
- Jeśli nie zastosujesz zakresu, nie pojawi się żadne niestandardowe zachowanie.
Implementację tych testów możesz zobaczyć w przykładzie TextStyling na GitHubie.
Interakcje Canvas możesz przetestować, podszywając się pod obiekt canvas, przesyłając imitację obiektu do metody drawLeadingMargin()
i sprawdzając, czy właściwe metody są wywoływane z prawidłowymi parametrami.
Więcej próbek testowych spanów znajdziesz w bulletPointSpanTest.
Sprawdzone metody korzystania ze spanów
Istnieje kilka sposobów ustawiania tekstu w elemencie TextView
, które oszczędzają pamięć, w zależności od potrzeb.
Dołącz lub odłącz rozpiętość bez zmiany bazowego tekstu
TextView.setText()
zawiera wiele przeciążeń, które w różny sposób obsługują spany. Możesz na przykład ustawić obiekt tekstowy Spannable
za pomocą tego kodu:
Kotlin
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
Wywołując to przeciążenie związane z tabelą setText()
, TextView
tworzy kopię Spannable
jako SpannedString
i zachowuje ją w pamięci jako CharSequence
.
Oznacza to, że tekst i zakresy spanów są stałe, więc gdy musisz zaktualizować tekst lub spany, utwórz nowy obiekt Spannable
i ponownie wywołaj setText()
, co również uruchamia ponowne pomiary i rysowanie układu.
Aby wskazać, że spany muszą być zmienne, użyj parametru 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
sprawia, że TextView
tworzy SpannableString
, a obiekt CharSequence
przechowywany przez TextView
ma teraz zmienne znaczniki i stały tekst. Aby zaktualizować span, pobierz tekst jako Spannable
, a następnie w razie potrzeby zaktualizuj spany.
Gdy dołączysz lub odłączysz spany lub zmienisz ich położenie, TextView
automatycznie zaktualizuje się, aby odzwierciedlić zmianę w tekście. Jeśli zmienisz wewnętrzny atrybut istniejącego spanu, wywołaj invalidate()
, aby wprowadzić zmiany związane z wyglądem, lub requestLayout()
, aby wprowadzić zmiany związane z danymi.
Wielokrotne ustawienie tekstu w komponencie TextView
W niektórych przypadkach, np. gdy używasz elementu RecyclerView.ViewHolder
, możesz użyć właściwości TextView
i wpisać tekst kilka razy. Domyślnie, niezależnie od tego, czy ustawisz BufferType
, TextView
tworzy kopię obiektu CharSequence
i przechowuje ją w pamięci. Dzięki temu wszystkie aktualizacje TextView
są celowe – nie można zaktualizować oryginalnego obiektu CharSequence
, 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 dodatkowego obiektu, możesz wdrożyć własny Spannable.Factory
i zastąpić metodę newSpannable()
.
Zamiast tworzyć nowy obiekt tekstowy, możesz rzutować 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; } };
Aby ustawić tekst, musisz użyć polecenia textView.setText(spannableObject, BufferType.SPANNABLE)
. W przeciwnym razie źródło CharSequence
jest tworzone jako wystąpienie Spanned
i nie można go rzutować na Spannable
, przez co newSpannable()
zgłasza ClassCastException
.
Po zastąpieniu kolumny newSpannable()
poproś użytkownika TextView
o używanie nowego Factory
:
Kotlin
textView.setSpannableFactory(spannableFactory)
Java
textView.setSpannableFactory(spannableFactory);
Ustaw obiekt Spannable.Factory
raz, zaraz po otrzymaniu odwołania do TextView
. Jeśli używasz właściwości RecyclerView
, ustaw obiekt Factory
podczas pierwszego powiększania widoków. Unikniesz w ten sposób dodatkowego tworzenia obiektu, gdy RecyclerView
wiąże nowy element z ViewHolder
.
Zmień atrybuty spanu wewnętrznego
Jeśli chcesz zmienić tylko wewnętrzny atrybut zmiennej spanu, np. kolor punktu w niestandardowym rozpiętości punktora, możesz uniknąć wielokrotnego wywoływania funkcji setText()
, zachowując podczas jego tworzenia odwołanie do tego zakresu.
Gdy musisz zmienić span, możesz zmodyfikować plik referencyjny, a potem wywołać metodę invalidate()
lub requestLayout()
w zależności od typu zmienionego atrybutu TextView
.
W tym przykładowym kodzie niestandardowa implementacja punktora 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 Androidzie
KTX na Androidzie zawiera też funkcje rozszerzeń, które ułatwiają pracę ze spanami. Więcej informacji znajdziesz w dokumentacji pakietu androidx.core.text.