Intervalli

Prova la funzionalità Scrivi
Jetpack Compose è il toolkit per l'interfaccia utente consigliato per Android. Scopri come utilizzare il testo in Scrittura.

Gli elementi Span sono potenti oggetti di markup che puoi utilizzare per applicare stili al testo a livello di carattere o paragrafo. Collegando intervalli a oggetti di testo, puoi modificare il testo in diversi modi, ad esempio aggiungendo colore, rendendo il testo cliccabile, scalando la dimensione del testo e disegnandolo in modo personalizzato. Gli elementi span possono anche modificare le proprietà TextPaint, disegnare su un elemento Canvas e modificare il layout del testo.

Android fornisce diversi tipi di span che coprono una serie di pattern di stile di testo comuni. Puoi anche creare gli elementi span per applicare stili personalizzati.

Creare e applicare un intervallo

Per creare un span, puoi utilizzare una delle classi elencate nella tabella seguente. Le classi differiscono in base al fatto che il testo stesso sia modificabile, che il markup del testo sia modificabile e quale struttura di dati sottostante contenga i dati dell'intervallo.

Classe Testo modificabile Markup modificabile Struttura dei dati
SpannedString No No Array lineare
SpannableString No Array lineare
SpannableStringBuilder Albero di intervalli

Tutte e tre le classi estendono l'interfaccia Spanned. SpannableString e SpannableStringBuilder estendono anche l'interfaccia Spannable.

Ecco come decidere quale utilizzare:

  • Se non modifichi il testo o il markup dopo la creazione, utilizza SpannedString.
  • Se devi collegare un numero ridotto di intervalli a un singolo oggetto di testo e il testo stesso è di sola lettura, utilizza SpannableString.
  • Se devi modificare il testo dopo la creazione e devi associare degli intervalli al testo, utilizza SpannableStringBuilder.
  • Se devi collegare un numero elevato di intervalli a un oggetto di testo, indipendentemente dal fatto che il testo stesso sia di sola lettura, utilizza SpannableStringBuilder.

Per applicare un intervallo, chiama setSpan(Object _what_, int _start_, int _end_, int _flags_) su un oggetto Spannable. Il parametro what si riferisce all'intervallo applicato al testo, mentre i parametri start e end indicano la parte del testo a cui viene applicato l'intervallo.

Se inserisci del testo all'interno dei limiti di un intervallo, questo si espande automaticamente per includere il testo inserito. Quando inserisci del testo nei limiti dell'intervallo, ovvero negli indici start o end, il parametro flags determina se l'intervallo si espande per includere il testo inserito. Utilizza Spannable.SPAN_EXCLUSIVE_INCLUSIVE per includere il testo inserito e Spannable.SPAN_EXCLUSIVE_EXCLUSIVE per escluderlo.

L'esempio seguente mostra come associare un ForegroundColorSpan a una stringa:

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
);
Un'immagine che mostra un testo grigio, parzialmente rosso.
Figura 1. Testo con stile a ForegroundColorSpan.

Poiché l'intervallo è impostato utilizzando Spannable.SPAN_EXCLUSIVE_INCLUSIVE, si espande per includere il testo inserito ai confini dell'intervallo, come mostrato nell'esempio seguente:

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)");
Un'immagine che mostra come l'elemento span includa più testo quando viene utilizzato SPAN_EXCLUSIVE_INCLUSIVE.
Figura 2. L'elemento span si espande per includere del testo aggiuntivo quando viene utilizzato Spannable.SPAN_EXCLUSIVE_INCLUSIVE.

Puoi allegare più intervalli allo stesso testo. L'esempio seguente mostra come creare testo in grassetto e rosso:

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
);
Un'immagine che mostra un testo con più intervalli: "ForegroundColorSpan(Color.RED)" e "StyleSpan(BOLD)"
Figura 3. Testo con più intervalli: ForegroundColorSpan(Color.RED) e StyleSpan(BOLD).

Tipi di span per Android

Android fornisce oltre 20 tipi di span nel pacchetto android.text.style. Android classifica gli intervalli in due modi principali:

  • In che modo l'intervallo influisce sul testo: un intervallo può influire sull'aspetto o sulle metriche del testo.
  • Ambito dell'intervallo: alcuni intervalli possono essere applicati a singoli caratteri, mentre altri devono essere applicati a un intero paragrafo.
Un'immagine che mostra diverse categorie di intervalli
Figura 4. Categorie di intervalli Android.

Le sezioni seguenti descrivono queste categorie in modo più dettagliato.

Intervalli che influiscono sull'aspetto del testo

Alcuni intervalli applicati a livello di carattere influiscono sull'aspetto del testo, ad esempio la modifica del testo o del colore di sfondo e l'aggiunta di sottolineature o barrature. Questi elementi estendono la classe CharacterStyle.

Il seguente esempio di codice mostra come applicare un UnderlineSpan per sottolineare il testo:

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);
Un'immagine che mostra come sottolineare il testo utilizzando un "UnderlineSpan"
Figura 5. Testo sottolineato utilizzando un carattere UnderlineSpan.

Gli intervalli che influiscono solo sull'aspetto del testo attivano un nuovo disegno del testo senza attivare un nuovo calcolo del layout. Questi intervalli implementano UpdateAppearance ed estendono CharacterStyle. Le sottoclassi CharacterStyle definiscono come disegnare il testo fornendo l'accesso per aggiornare il TextPaint.

Intervalli che influiscono sulle metriche del testo

Altri intervalli applicati a livello di carattere influiscono sulle metriche del testo, come l'altezza della riga e le dimensioni del testo. Questi intervalli estendono la classe MetricAffectingSpan.

Il seguente esempio di codice crea un RelativeSizeSpan che aumenta le dimensioni del testo del 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);
Un'immagine che mostra l'utilizzo di RelativeSizeSpan
Figura 6. Il testo viene ingrandito utilizzando un RelativeSizeSpan.

L'applicazione di un intervallo che influisce sulle metriche del testo fa sì che un oggetto di osservazione rilevi nuovamente il testo per verificarne il layout e il rendering corretti. Ad esempio, la modifica delle dimensioni del testo potrebbe causare la visualizzazione delle parole su righe diverse. L'applicazione dell'elemento span precedente attiva una nuova misurazione, il ricalcolo del layout del testo e il nuovo disegno del testo.

Gli intervalli che influiscono sulle metriche del testo estendono la classe MetricAffectingSpan, una classe astratta che consente ai sottoclassi di definire in che modo l'intervallo influisce sulla misurazione del testo fornendo l'accesso a TextPaint. Poiché MetricAffectingSpan estende CharacterStyle, le sottoclassi influiscono sull'aspetto del testo a livello di carattere.

Intervalli che influiscono sui paragrafi

Un intervallo può anche influire sul testo a livello di paragrafo, ad esempio modificando l'allineamento o il margine di un blocco di testo. Gli intervalli che interessano interi paragrafi implementano ParagraphStyle. Per utilizzare questi intervalli, devi associarli all'intero paragrafo, escludendo il carattere di nuova riga finale. Se provi ad applicare un intervallo di paragrafo a qualcosa di diverso da un paragrafo intero, Android non applica l'intervallo.

La Figura 8 mostra come Android separa i paragrafi nel testo.

Figura 7. In Android, i paragrafi terminano con un carattere di nuova riga (\n).

Il seguente esempio di codice applica un QuoteSpan a un paragrafo. Tieni presente che se agganci lo spazio a una posizione diversa dall'inizio o dalla fine di un paragrafo, Android non applica lo stile.

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);
Un'immagine che mostra un esempio di QuoteSpan
Figura 8. Un QuoteSpan applicato a un paragrafo.

Creare intervalli personalizzati

Se hai bisogno di più funzionalità rispetto a quelle fornite dagli elementi Android esistenti, puoi implementare un elemento personalizzato. Quando implementi un intervallo, decidi se deve influire sul testo a livello di carattere o di paragrafo e anche se deve influire sul layout o sull'aspetto del testo. In questo modo puoi determinare quali classi di base puoi estendere e quali interfacce potresti dover implementare. Utilizza la seguente tabella come riferimento:

Scenario Classe o interfaccia
L'intervallo influisce sul testo a livello di carattere. CharacterStyle
L'intervallo influisce sull'aspetto del testo. UpdateAppearance
L'intervallo influisce sulle metriche del testo. UpdateLayout
L'intervallo influisce sul testo a livello di paragrafo. ParagraphStyle

Ad esempio, se devi implementare uno spazio personalizzato che modifichi le dimensioni e il colore del testo, espandi RelativeSizeSpan. Tramite l'ereditarietà, RelativeSizeSpan espande CharacterStyle e implementa le due interfacce Update. Poiché questa classe fornisce già callback per updateDrawState e updateMeasureState, puoi eseguire l'override di questi callback per implementare il comportamento personalizzato. Il seguente codice crea uno spazio personalizzato che estende RelativeSizeSpan e sostituisce il callback updateDrawState per impostare il colore del 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);
    }
}

Questo esempio illustra come creare uno spazio personalizzato. Puoi ottenere lo stesso effetto applicando RelativeSizeSpan e ForegroundColorSpan al testo.

Verifica l'utilizzo dell'intervallo di test

L'interfaccia Spanned ti consente di impostare gli intervalli e di recuperarli dal testo. Durante i test, implementa un test JUnit per Android per verificare che gli intervalli corretti vengano aggiunti nelle posizioni corrette. L'app di esempio Stile di testo contiene uno spazio che applica il markup ai punti elenco collegandoBulletPointSpan al testo. Il seguente esempio di codice mostra come verificare se i punti elenco vengono visualizzati come previsto:

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));
}

Per altri esempi di test, consulta MarkdownBuilderTest su GitHub.

Testare gli intervalli personalizzati

Quando testi gli intervalli, verifica che TextPaint contenga le modifiche previste e che gli elementi corretti vengano visualizzati in Canvas. Ad esempio, considera un'implementazione di span personalizzata che antepone un punto elenco a un testo. Il punto elenco ha dimensioni e colore specifici e tra il margine sinistro dell'area disegnabile e il punto elenco è presente un intervallo.

Puoi verificare il comportamento di questa classe implementando un test AndroidJUnit, controllando quanto segue:

  • Se applichi correttamente l'elemento span, nel riquadro viene visualizzato un punto elenco delle dimensioni e del colore specificati e lo spazio appropriato tra il margine sinistro e il punto elenco.
  • Se non applichi l'elemento span, non viene visualizzato alcun comportamento personalizzato.

Puoi vedere l'implementazione di questi test nell'esempio TextStyling su GitHub.

Puoi testare le interazioni con il canvas simulando il canvas, passando l'oggetto simulato al metodo drawLeadingMargin() e verificando che i metodi corretti vengano chiamati con i parametri corretti.

Puoi trovare altri esempi di test di intervallo in BulletPointSpanTest.

Best practice per l'utilizzo degli intervalli

Esistono diversi modi per impostare il testo in un TextView in modo efficiente in termini di memoria, a seconda delle tue esigenze.

Attaccare o staccare un intervallo senza modificare il testo sottostante

TextView.setText() contiene più sovraccarichi che gestiscono gli intervalli in modo diverso. Ad esempio, puoi impostare un oggetto di testo Spannable con il seguente codice:

Kotlin

textView.setText(spannableObject)

Java

textView.setText(spannableObject);

Quando chiami questo sovraccarico di setText(), TextView crea una copia del tuo Spannable come SpannedString e la mantiene in memoria come CharSequence. Ciò significa che il testo e gli intervalli sono immutabili, quindi quando devi aggiornare il testo o gli intervalli, crea un nuovo oggetto Spannable e chiama di nuovo setText(), che attiva anche una nuova misurazione e un nuovo disegno del layout.

Per indicare che gli intervalli devono essere mutabili, puoi utilizzare setText(CharSequence text, TextView.BufferType type), come mostrato nell'esempio seguente:

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 questo esempio, il parametro BufferType.SPANNABLE induce TextView a creare un SpannableString e l'oggetto CharSequence conservato da TextView ora ha markup mutabile e testo immutabile. Per aggiornare l'intervallo, recupera il testo come Spannable e poi aggiorna gli intervalli in base alle esigenze.

Quando colleghi, scolleghi o riposizioni gli intervalli, TextView si aggiorna automaticamente in base alla modifica del testo. Se modifichi un attributo interno di un intervallo esistente, chiama invalidate() per apportare modifiche all'aspetto o requestLayout() per apportare modifiche relative alle metriche.

Impostare il testo in un TextView più volte

In alcuni casi, ad esempio quando utilizzi un RecyclerView.ViewHolder, potresti voler riutilizzare un TextView e impostare il testo più volte. Per impostazione predefinita, indipendentemente dal fatto che tu abbia impostato BufferType, TextView crea una copia dell'oggetto CharSequence e la memorizza. In questo modo, tutti gli aggiornamenti di TextView sono intenzionali: non puoi aggiornare l'oggetto TextView originale per aggiornare il testo.CharSequence Ciò significa che ogni volta che imposti un nuovo testo, TextView crea un nuovo oggetto.

Se vuoi avere un maggiore controllo su questa procedura ed evitare la creazione di oggetti aggiuntivi, puoi implementare il tuo Spannable.Factory e eseguire l'override di newSpannable(). Invece di creare un nuovo oggetto di testo, puoi eseguire il casting e restituire il CharSequence esistente come Spannable, come mostrato nell'esempio seguente:

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;
    }
};

Devi utilizzare textView.setText(spannableObject, BufferType.SPANNABLE) quando imposta il testo. In caso contrario, l'origine CharSequence viene creata come istanza Spanned e non può essere eseguita la conversione in Spannable, causando l'emissione di un ClassCastException da parte di newSpannable().

Dopo aver sostituito newSpannable(), chiedi a TextView di utilizzare il nuovo Factory:

Kotlin

textView.setSpannableFactory(spannableFactory)

Java

textView.setSpannableFactory(spannableFactory);

Imposta l'oggetto Spannable.Factory una volta, subito dopo aver ricevuto un riferimento al tuo TextView. Se utilizzi un RecyclerView, imposta l'oggetto Factory la prima volta che gonfi le visualizzazioni. In questo modo, eviterai di creare oggetti aggiuntivi quando il tuo RecyclerView associa un nuovo elemento al tuo ViewHolder.

Modificare gli attributi degli intervalli interni

Se devi modificare solo un attributo interno di uno spazio modificabile, ad esempio il colore del punto elenco in uno spazio personalizzato, puoi evitare il sovraccarico della chiamata di setText() più volte mantenendo un riferimento allo spazio durante la sua creazione. Quando devi modificare l'intervallo, puoi modificare il riferimento e chiamare invalidate() o requestLayout() su TextView, a seconda del tipo di attributo modificato.

Nel seguente esempio di codice, un'implementazione di un punto elenco personalizzato ha un colore predefinito rosso che diventa grigio quando viene toccato un pulsante:

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();
            }
        });
    }
}

Utilizzare le funzioni di estensione KTX di Android

Android KTX contiene anche funzioni di estensione che semplificano il lavoro con gli elementi spanning. Per scoprire di più, consulta la documentazione del pacchetto androidx.core.text.