Intervalli

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

Gli intervalli sono potenti oggetti di markup che puoi utilizzare per applicare uno stile al testo a livello di carattere o paragrafo. Se colleghi intervalli agli oggetti di testo, puoi modificare il testo in diversi modi, ad esempio aggiungendo colori, rendendo il testo cliccabile, ridimensionando le dimensioni del testo e tracciando il testo in modo personalizzato. Gli intervalli possono anche modificare le proprietà TextPaint, disegnare su Canvas e modificare il layout del testo.

Android fornisce diversi tipi di intervalli che coprono una varietà di modelli di stile del testo comuni. Puoi anche creare sezioni personalizzate per applicare stili personalizzati.

Creare e applicare un intervallo

Per creare un intervallo, puoi utilizzare uno dei corsi elencati nella seguente tabella. Le classi differiscono a seconda che il testo stesso sia modificabile, che il markup del testo sia modificabile e che la struttura dei 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 a intervalli

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

Per decidere quale utilizzare:

  • Se non modifichi il testo o il markup dopo la creazione, utilizza SpannedString.
  • Se devi collegare un numero ridotto di sezioni a un singolo oggetto di testo e il testo stesso è di sola lettura, utilizza SpannableString.
  • Se devi modificare un testo dopo la creazione e devi allegare intervalli al testo, utilizza SpannableStringBuilder.
  • Se devi collegare un numero elevato di sezioni a un oggetto di testo, indipendentemente dal fatto che il testo 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 che stai applicando al testo, mentre i parametri start e end indicano la parte di testo a cui stai applicando l'intervallo.

Se inserisci testo all'interno dei margini di un intervallo, l'intervallo si espande automaticamente per includere il testo inserito. Quando inserisci il testo ai confini dell'intervallo, ovvero agli indici start o end, il parametro flags determina se l'intervallo si espande per includere il testo inserito. Utilizza il flag Spannable.SPAN_EXCLUSIVE_INCLUSIVE per includere il testo inserito e Spannable.SPAN_EXCLUSIVE_EXCLUSIVE per escluderlo.

L'esempio seguente mostra come collegare un elemento 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 in stile ForegroundColorSpan.

Poiché l'intervallo viene impostato utilizzando Spannable.SPAN_EXCLUSIVE_INCLUSIVE, l'intervallo si espande in modo da includere il testo inserito in corrispondenza dei limiti 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 in che modo l'intervallo include più testo quando viene utilizzato SPAN_EXCLUSIVE_INCLUSIVE.
Figura 2. L'intervallo si espande per includere testo aggiuntivo quando utilizzi Spannable.SPAN_EXCLUSIVE_INCLUSIVE.

Puoi allegare più sezioni 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 intervalli Android

Android fornisce oltre 20 tipi di intervalli 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 del testo o sulle metriche del testo.
  • Ambito di 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

Alcune sezioni che si applicano a livello di carattere influiscono sull'aspetto del testo, come la modifica del colore del testo o dello sfondo e l'aggiunta di sottolineature o barrature. Questi intervalli estendono la classe CharacterStyle.

Nell'esempio di codice seguente viene mostrato come applicare 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 oggetto "Sottolinea"
Figura 5. Testo sottolineato con un UnderlineSpan.

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

Intervalli che influiscono sulle metriche del testo

Altri intervalli che si applicano a livello di carattere influiscono sulle metriche del testo, come l'altezza della riga e la dimensione del testo. Questi intervalli estendono la classe MetricAffectingSpan.

L'esempio di codice seguente crea un elemento 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 dell'elemento RELATIVESizeSpan
Figura 6. Testo ingrandito con un elemento RelativeSizeSpan.

L'applicazione di un intervallo che influisce sulle metriche del testo fa sì che un oggetto osservante rimisura il testo per verificarne il layout e il rendering corretti; ad esempio, la modifica delle dimensioni del testo potrebbe far apparire le parole su righe diverse. L'applicazione dell'intervallo precedente attiva una nuova misurazione, il ricalcolo del layout del testo e la rielaborazione del testo.

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

Intervalli che interessano i paragrafi

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

La figura 8 mostra in che modo 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 elemento QuoteSpan a un paragrafo. Tieni presente che se colleghi l'intervallo a una posizione diversa dall'inizio o alla fine di un paragrafo, Android non applicherà 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 elemento QuoteSpan applicato a un paragrafo.

Creare intervalli personalizzati

Se hai bisogno di più funzionalità rispetto a quelle fornite negli intervalli Android esistenti, puoi implementare un intervallo personalizzato. Quando implementi un intervallo personalizzato, decidi se interessa il testo a livello di carattere o di paragrafo e se incide sul layout o sull'aspetto del testo. Ciò ti aiuta a determinare quali classi 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 incide 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 un intervallo personalizzato che modifichi le dimensioni e il colore del testo, estendi RelativeSizeSpan. Tramite l'ereditarietà, RelativeSizeSpan estende CharacterStyle e implementa le due interfacce Update. Poiché questa classe fornisce già callback per updateDrawState e updateMeasureState, puoi eseguirne l'override per implementare il tuo comportamento personalizzato. Il seguente codice crea un intervallo personalizzato che estende RelativeSizeSpan e sostituisce il callback updateDrawState per impostare il colore di 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 un intervallo personalizzato. Puoi ottenere lo stesso effetto applicando RelativeSizeSpan e ForegroundColorSpan al testo.

Utilizzo intervallo di test

L'interfaccia Spanned consente di impostare intervalli e recuperare intervalli dal testo. Durante i test, implementa un test Android JUnit per verificare che gli intervalli corretti vengano aggiunti nelle posizioni corrette. L'app di esempio per gli stili di testo contiene un intervallo che applica il markup ai punti elenco aggiungendo BulletPointSpan al testo. Il seguente esempio di codice mostra come verificare se i punti elenco appaiono 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 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 intervalli personalizzata che antepone un punto elenco a un testo. Il punto dell'elenco ha dimensioni e colore specifici e vi è uno spazio tra il margine sinistro dell'area di disegno e il punto dell'elenco.

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

  • Se applichi correttamente l'intervallo, sul canvas viene visualizzato un punto elenco con le dimensioni e il colore specificati e lo spazio corretto è presente tra il margine sinistro e il punto dell'elenco.
  • Se non applichi l'intervallo, non viene visualizzato alcun comportamento personalizzato.

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

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

Puoi trovare altri esempi di test degli intervalli in BulletPointSpanTest.

Best practice per l'utilizzo degli intervalli

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

Allega o scollega 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 di Spannable come SpannedString e la conserva in memoria come CharSequence. Ciò significa che il testo e gli intervalli sono immutabili, pertanto quando devi aggiornare il testo o gli intervalli, crea un nuovo oggetto Spannable e chiama di nuovo setText(). In questo modo, verrà attivata anche una nuova misurazione e un nuovo disegno del layout.

Per indicare che gli intervalli devono essere modificabili, 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 fa sì che TextView crei un SpannableString e l'oggetto CharSequence mantenuto dal TextView ora ha un markup modificabile e testo immutabile. Per aggiornare l'intervallo, recupera il testo come Spannable e aggiorna gli intervalli in base alle esigenze.

Quando alleghi, scolleghi o riposiziona le sezioni, l'elemento TextView si aggiorna automaticamente per riflettere la modifica al testo. Se modifichi un attributo interno di un intervallo esistente, chiama invalidate() per apportare modifiche relative all'aspetto o requestLayout() per apportare modifiche relative alle metriche.

Impostare il testo più volte in un TextView

In alcuni casi, ad esempio quando utilizzi un elemento RecyclerView.ViewHolder, potresti voler riutilizzare un elemento TextView e impostare il testo più volte. Per impostazione predefinita, a prescindere dalla configurazione di BufferType, TextView crea una copia dell'oggetto CharSequence e la conserva in memoria. In questo modo tutti gli aggiornamenti TextView sono intenzionali: non puoi aggiornare l'oggetto CharSequence originale per aggiornare il testo. Ciò significa che ogni volta che imposti un nuovo testo, TextView crea un nuovo oggetto.

Se vuoi avere un maggiore controllo su questo processo ed evitare la creazione di oggetti aggiuntiva, puoi implementare il tuo Spannable.Factory ed eseguire l'override newSpannable(). Anziché creare un nuovo oggetto di testo, puoi trasmettere e restituire l'oggetto 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 usare textView.setText(spannableObject, BufferType.SPANNABLE) per impostare il testo. In caso contrario, l'origine CharSequence viene creata come istanza Spanned e non può essere trasmessa in Spannable, perciò newSpannable() genererà un valore ClassCastException.

Dopo aver eseguito l'override di 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 ottenuto un riferimento a TextView. Se utilizzi un RecyclerView, imposta l'oggetto Factory quando aumenta le visualizzazioni per la prima volta. Ciò evita la creazione di oggetti aggiuntivi quando RecyclerView associa un nuovo elemento a ViewHolder.

Modifica attributi intervallo interno

Se devi modificare solo un attributo interno di un intervallo modificabile, come il colore del punto elenco personalizzato, puoi evitare di chiamare setText() più volte mantenendo un riferimento all'intervallo mentre viene creato. Quando devi modificare l'intervallo, puoi modificare il riferimento e quindi chiamare invalidate() o requestLayout() su TextView, a seconda del tipo di attributo che hai modificato.

Nel seguente esempio di codice, un'implementazione di un punto elenco personalizzato ha un colore predefinito rosso che diventa grigio quando si tocca 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 delle estensioni KTX per Android

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