Spany

Wypróbuj sposób tworzenia wiadomości
Jetpack Compose to zalecany zestaw narzędzi UI na Androida. Dowiedz się, jak używać tekstu w funkcji Compose

Spany to zaawansowane obiekty znaczników, których możesz używać do określania stylu tekstu na poziomie znaku lub akapitu. Dołączając spany do obiektów tekstowych, możesz zmienić w ten sposób można zmieniać kolor tekstu, dodawać do niego kolory, przez skalowanie rozmiaru tekstu oraz jego dostosowywanie. Spany można też zmień właściwości TextPaint, rysuj na Canvas i zmień układ tekstu.

Android udostępnia kilka rodzajów spanów, które obejmują zróżnicowany tekst wzorów stylizacji. Możesz też utworzyć własne rozpiętości, aby zastosować styl niestandardowy.

Tworzenie i stosowanie spanu

Aby utworzyć span, możesz użyć jednej z klas wymienionych w poniższej tabeli. Klasy różnią się w zależności od tego, czy sam tekst jest zmienny, od tego, czy tekst znaczniki można zmieniać oraz która 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łu

Wszystkie 3 zajęcia wykraczają poza Spanned za pomocą prostego interfejsu online. SpannableString i SpannableStringBuilder rozszerzają też Spannable.

Aby zdecydować, którego z nich użyć:

  • Jeśli po utworzeniu nie modyfikujesz tekstu ani znaczników, użyj SpannedString
  • Jeśli chcesz dołączyć niewielką liczbę spanów do pojedynczego obiektu tekstowego i Sam tekst jest tylko do odczytu, użyj SpannableString.
  • Jeśli po utworzeniu musisz zmodyfikować tekst i musisz dołączyć do niego spany tekst, użyj funkcji SpannableStringBuilder.
  • Jeśli musisz dołączyć do obiektu tekstowego dużą liczbę spanów, aby określić, czy sam 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órym jesteś a parametry start i end wskazują część tekstu, do którego stosujesz rozpiętość.

Jeśli wstawisz tekst w granicach zakresu, rozpiętość zostanie automatycznie rozwinięta do wartości będą zawierać wstawiony tekst. Podczas wstawiania tekstu na spanu Granice – czyli na indeksach początku lub końcowegoflagi określa, czy rozpiętość rozwija się, aby uwzględnić wstawiony tekst. Używaj Spannable.SPAN_EXCLUSIVE_INCLUSIVE , aby uwzględnić wstawiony tekst, oraz użyć Spannable.SPAN_EXCLUSIVE_EXCLUSIVE aby wykluczyć wstawiony tekst.

Poniższy przykład pokazuje, jak dołączyć plik ForegroundColorSpan na ciąg 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
);
Obraz przedstawiający szary tekst, częściowo czerwony.
Rysunek 1. Tekst ze znakiem ForegroundColorSpan

Ustawiony jest za pomocą Spannable.SPAN_EXCLUSIVE_INCLUSIVE, więc rozwija się, aby uwzględnić wstawiony tekst na granicach spanów, tak jak widać to w interfejsie następujący przykład:

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)");
Obraz pokazujący, jak span obejmuje więcej tekstu przy zastosowaniu funkcji SPAN_EXCLUSIVE_INCLUSIVE.
Rysunek 2. Zakres rozwija się, aby uwzględnić dodatkowy tekst w przypadku użycia funkcji Spannable.SPAN_EXCLUSIVE_INCLUSIVE

Do tego samego tekstu możesz dołączyć wiele spanów. Ten przykład pokazuje, aby 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
);
Obraz przedstawiający tekst z wieloma rozpiętościami: `ForegroundColorSpan(Color.RED)` i `StyleSpan(BOLD)`
Rysunek 3. Tekst o wielu rozpiętościach: ForegroundColorSpan(Color.RED) i StyleSpan(BOLD)

Typy spanów Androida

Android udostępnia ponad 20 typów spanów android.text.style. Android dzieli spany na 2 główne sposoby:

  • Wpływ rozpiętości na tekst: może on wpływać na wygląd tekstu lub tekst danych.
  • Zakres spanu: niektóre spany można stosować do pojedynczych znaków, a inne do pojedynczych znaków musi obejmować cały akapit.
.
Obraz przedstawiający różne kategorie spanów
. Rysunek 4. Kategorie spanów Androida.

W kolejnych sekcjach znajdziesz bardziej szczegółowe informacje o tych kategoriach.

Rozpiętości wpływające na wygląd tekstu

Niektóre rozpiętości stosowane na poziomie znaku wpływają na wygląd tekstu, na przykład zmianę koloru tekstu lub tła oraz dodanie podkreśleń lub przekreśleń. Te rozpiętości przedłużają CharacterStyle.

Ten przykładowy kod pokazuje, jak zastosować UnderlineSpan do podkreślenia 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);
Obraz pokazujący, jak podkreślać tekst za pomocą elementu „UnderlineSpan”
Rysunek 5. Tekst podkreślony za pomocą UnderlineSpan

Rozpiętości, które wpływają tylko na wygląd tekstu, powodują ponowne wyświetlenie tekstu bez co aktywuje ponowne obliczanie układu. Te spany implementują UpdateAppearance i przedłuż CharacterStyle Podklasy CharacterStyle określają sposób rysowania tekstu przez przyznanie dostępu do Zaktualizuj TextPaint.

Rozpiętości wpływające na dane tekstowe

Inne rozpiętości stosowane na poziomie znaku wpływają na dane tekstowe, np. linię wysokości i rozmiaru tekstu. Te rozpiętości rozciągają MetricAffectingSpan zajęcia.

Ten przykładowy kod tworzy RelativeSizeSpan 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);
Obraz pokazujący użycie obiektu RelativeSizeSpan
Rysunek 6. Tekst został powiększony za pomocą RelativeSizeSpan

Zastosowanie spanu, które wpływa na dane tekstowe, powoduje, że obserwowany obiekt ponownie zmierzyć tekst pod kątem prawidłowego układu i renderowania – np. zmienić może powodować, że słowa pojawią się w różnych wierszach. Stosuję poprzednie powoduje ponowne zmierzenie, ponowne obliczenie układu tekstu i ponowne tekst.

Rozpiętości, które mają wpływ na dane tekstowe, obejmują klasę MetricAffectingSpan, klasa abstrakcyjna, która pozwala podklasom definiować, jak rozpiętość wpływa na pomiar tekstu przyznając dostęp do: TextPaint. Od MetricAffectingSpan CharacterSpan, podklasy wpływają na wygląd tekstu przy znaku na poziomie 300%.

Rozpiętości, które mają wpływ na akapity

Rozpiętość może też wpływać na tekst na poziomie akapitu, na przykład zmieniając wyrównanie lub margines bloku tekstu. Rozpiętości mające wpływ na całe akapity zaimplementować ParagraphStyle. Do tych zakresów, dołączasz do całego akapitu, z wyłączeniem jego zakończenia znaku nowego wiersza. Jeśli próbujesz zastosować rozpiętość akapitu do tekstu innego niż cały akapit, Android w ogóle nie stosuje zakresu.

Rysunek 8 pokazuje, jak Android rozdziela akapity w tekście.

Rysunek 7. Na Androidzie akapity kończą się znakiem znak nowego wiersza (\n).

Poniższy przykładowy kod stosuje QuoteSpan do akapitu. Pamiętaj, że jeśli dołączysz span w miejscu innym niż początek lub koniec akapit, Android w ogóle nie stosuje 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);
Obraz przedstawiający przykładowy obiekt OfferSpan
Rysunek 8. QuoteSpan zastosowano do akapitu.

Utwórz niestandardowe spany

Jeśli potrzebujesz więcej funkcji niż oferuje dotychczasowa wersja Androida możesz zastosować niestandardowy zakres. Podczas wdrażania własnego spanu zdecyduj czy rozpiętość ma wpływ na tekst na poziomie znaków czy akapitu, a także na układ lub wygląd tekstu. Dzięki temu określanie, które klasy bazowe można rozszerzyć i które interfejsy mogą być potrzebne do wdrożenia. Skorzystaj z tej tabeli:

Scenariusz Klasa lub interfejs
Rozpiętość ma wpływ na tekst na poziomie znaku. CharacterStyle
Rozpiętość wpływa na wygląd tekstu. UpdateAppearance
Rozpiętość wpływa na dane tekstowe. UpdateLayout
Rozpiętość ma wpływ na tekst na poziomie akapitu. ParagraphStyle

Jeśli na przykład chcesz wdrożyć niestandardowy zakres, który zmienia rozmiar tekstu i kolor, rozszerzenie RelativeSizeSpan. Przez dziedziczenie, RelativeSizeSpan Rozszerza zakres CharacterStyle i implementuje 2 interfejsy Update. Ponieważ klasa zawiera już wywołania zwrotne dla updateDrawState i updateMeasureState, możesz zastąpić te wywołania, aby zastosować zachowanie niestandardowe. ten kod tworzy niestandardowy span, który rozciąga się na 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 span. Aby osiągnąć taki sam efekt, przez zastosowanie do tekstu elementów RelativeSizeSpan i ForegroundColorSpan.

Przetestuj użycie spanu

Interfejs Spanned umożliwia ustawianie spanów oraz pobieranie spanów z tekstu. Podczas testów wdróż Android JUnit test, aby sprawdzić, czy zostały dodane prawidłowe spany. w odpowiednich lokalizacjach. Przykład Styl tekstu zawiera rozpiętość, w ramach której do list punktowanych są stosowane znaczniki przez dołączenie BulletPointSpan do tekstu. Poniższy przykładowy kod pokazuje, jak przetestować 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 tutaj MarkdownBuilderTest w GitHubie.

Przetestuj spany niestandardowe

Podczas testowania spanów sprawdź, czy TextPaint zawiera oczekiwaną wartość i żeby w Canvas pojawiły się prawidłowe elementy. Dla: możesz zastosować niestandardową implementację spanów, która dodaje punktor na początku jakiś tekst. Punktor ma określony rozmiar i kolor oraz występuje luka między lewym marginesem obszaru rysowanego a punktem.

Możesz przetestować działanie tej klasy, implementując test AndroidJUnit. sprawdzając, czy:

  • Jeśli prawidłowo zastosujesz rozpiętość, zostanie dodany punktor o określonym rozmiarze jest widoczny na obszarze roboczym, a między lewą krawędzią jest odpowiednia przestrzeń na marginesie i w punkcie.
  • Jeśli nie zastosujesz zakresu, nie pojawi się żadne zachowanie niestandardowe.

Wdrożenie tych testów możesz zobaczyć w sekcji Styl tekstu przykład w GitHubie.

Interakcje z Canvas możesz przetestować, śmiesznie się z kanwy, przekazując symulowane do żądania drawLeadingMargin() i sprawdzenie, czy właściwe metody są wywoływane z poprawnymi .

Więcej próbek testów spanów znajdziesz w BulletPointSpanTest.

Sprawdzone metody korzystania ze spanów

Jest kilka sposobów na ustawienie tekstu w polu TextView, które oszczędzają pamięć dostosowane do Twoich potrzeb.

Dołączanie lub odłączanie spanu bez zmiany tekstu bazowego

TextView.setText() zawiera wiele przeciążeń, które obsługują spany w różny sposób. Możesz na przykład: ustaw obiekt tekstowy Spannable za pomocą tego kodu:

Kotlin

textView.setText(spannableObject)

Java

textView.setText(spannableObject);

Gdy wywołujesz to przeciążenie (setText()), TextView tworzy kopię pliku danych Spannable jako SpannedString i zapisuje go w pamięci jako CharSequence. Oznacza to, że tekst i spany są stałe, a więc gdy trzeba zaktualizuj tekst lub spany, utwórz nowy obiekt Spannable i wywołaj setText(), co powoduje również ponowne pomiary i ponowne narysowanie układ.

Aby wskazać, że spany 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 para klucz-wartość BufferType.SPANNABLE sprawia, że TextView tworzy SpannableString, a Obiekt CharSequence przechowywany przez TextView ma teraz zmienne znaczniki i tekstu stałego. Aby zaktualizować span, pobierz tekst jako Spannable, a następnie w razie potrzeby zaktualizuj spany.

Przy dołączaniu, odłączaniu lub zmianie położenia rozpiętości element TextView aktualizuje się, aby odzwierciedlić zmianę w tekście. Jeśli zmienisz atrybut wewnętrzny istniejącego rozpiętości, wywołaj invalidate(), aby wprowadzić zmiany dotyczące wyglądu lub requestLayout(), aby wprowadzić zmiany związane z danymi.

Ustawianie tekstu w obiekcie TextView wiele razy

W niektórych przypadkach, na przykład w przypadku użycia RecyclerView.ViewHolder możesz ponownie użyć elementu TextView i ustawić tekst wielokrotnie. Według jest domyślne. Niezależnie od tego, czy ustawisz BufferType, TextView tworzy kopii obiektu CharSequence i przechowywania go w pamięci. Dzięki temu wszystkie Aplikacja TextView jest zamierzona – nie można zaktualizować pierwotnej wersji CharSequence obiekt, aby zaktualizować tekst. Oznacza to, że za każdym razem, gdy ustawisz nowy tekstu, TextView tworzy nowy obiekt.

Jeśli chcesz mieć większą kontrolę nad tym procesem i uniknąć dodatkowych obiektów możesz wdrożyć własne Spannable.Factory i zastąp newSpannable() Zamiast tworzyć nowy obiekt tekstowy, możesz rzutować i zwracać istniejący 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;
    }
};

Musisz użyć formy płatności textView.setText(spannableObject, BufferType.SPANNABLE), gdy ustawienie tekstu. W przeciwnym razie źródło CharSequence jest tworzone jako Spanned. i nie można rzutować na Spannable, przez co newSpannable() zgłasza ClassCastException.

Po zastąpieniu wartości newSpannable() poproś TextView o użycie nowego elementu Factory:

Kotlin

textView.setSpannableFactory(spannableFactory)

Java

textView.setSpannableFactory(spannableFactory);

Ustaw obiekt Spannable.Factory raz, zaraz po uzyskaniu odniesienia do swojego TextView Jeśli używasz obiektu RecyclerView, ustaw obiekt Factory podczas by zwiększyć liczbę wyświetleń. Zapobiega to tworzeniu dodatkowych obiektów, gdy RecyclerView tworzy nowy element w dokumencie ViewHolder.

Zmień atrybuty spanów wewnętrznych

Jeśli chcesz zmienić tylko atrybut wewnętrzny o zmiennym zakresie, taki jak w niestandardowym rozpiętości punktora, można uniknąć narzutu na wywoływanie setText() wiele razy, zapisując odwołanie do spanu w trakcie jego tworzenia. Aby zmodyfikować span, możesz zmodyfikować odwołanie, a następnie wywołać invalidate() lub requestLayout() na: TextView, w zależności od typu .

W poniższym przykładzie kodu niestandardowa implementacja punktorów ma domyślny kolor czerwonego, który zmienia kolor na szary po dotknię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

Android KTX zawiera również funkcje rozszerzeń, które pozwalają na pracę ze spanami. . Aby dowiedzieć się więcej, zapoznaj się z dokumentacją androidx.core.text pakietu SDK.