Objets Span

Essayer Compose
Jetpack Compose est le kit d'outils d'interface utilisateur recommandé pour Android. Découvrez comment utiliser du texte dans Compose.

Les segments sont de puissants objets de balisage que vous pouvez utiliser pour appliquer un style au texte au niveau des caractères ou des paragraphes. En associant des objets Span aux objets texte, vous pouvez modifier le texte de différentes manières, y compris en ajoutant de la couleur, en rendant le texte cliquable, en ajustant la taille du texte et en dessinant du texte de manière personnalisée. Les segments peuvent également modifier les propriétés TextPaint, dessiner sur un Canvas et modifier la mise en page du texte.

Android fournit plusieurs types de segments qui couvrent divers modèles de style de texte courants. Vous pouvez également créer vos propres objets Span pour appliquer un style personnalisé.

Créer et appliquer un span

Pour créer un objet Span, vous pouvez utiliser l'une des classes répertoriées dans le tableau suivant. Les classes diffèrent selon que le texte lui-même est modifiable ou non, que le balisage du texte est modifiable et que la structure de données sous-jacente contient les données de segment.

Classe Texte modifiable Balisage modifiable Structure des données
SpannedString Non Non Tableau linéaire
SpannableString Non Oui Tableau linéaire
SpannableStringBuilder Oui Oui Arborescence à intervalles

Les trois classes étendent l'interface Spanned. SpannableString et SpannableStringBuilder étendent également l'interface Spannable.

Voici comment faire votre choix:

  • Si vous ne modifiez pas le texte ou le balisage après la création, utilisez SpannedString.
  • Si vous devez associer un petit nombre de segments à un seul objet texte et que le texte lui-même est en lecture seule, utilisez SpannableString.
  • Si vous devez modifier du texte après sa création et y joindre des objets Span, utilisez SpannableStringBuilder.
  • Si vous devez associer un grand nombre de segments à un objet texte, que le texte soit en lecture seule ou non, utilisez SpannableStringBuilder.

Pour appliquer un segment, appelez setSpan(Object _what_, int _start_, int _end_, int _flags_) sur un objet Spannable. Le paramètre what (objet) fait référence au segment que vous appliquez au texte, et les paramètres start et end indiquent la partie du texte à laquelle vous appliquez l'objet Span.

Si vous insérez du texte à l'intérieur des limites d'un segment, celui-ci se développe automatiquement pour inclure le texte inséré. Lorsque vous insérez du texte au niveau des limites de segment, c'est-à-dire aux index start ou end, le paramètre flags détermine si l'élément span se développe pour inclure le texte inséré. Utilisez l'option Spannable.SPAN_EXCLUSIVE_INCLUSIVE pour inclure le texte inséré et Spannable.SPAN_EXCLUSIVE_EXCLUSIVE pour l'exclure.

L'exemple suivant montre comment associer un objet ForegroundColorSpan à une chaîne:

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
);
Image présentant du texte gris et partiellement rouge.
Figure 1 : Style de texte avec ForegroundColorSpan

Étant donné que l'objet Span est défini à l'aide de Spannable.SPAN_EXCLUSIVE_INCLUSIVE, il se développe pour inclure le texte inséré au niveau des limites des segments, comme illustré dans l'exemple suivant:

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)");
Image montrant comment le segment inclut davantage de texte lorsque SPAN_EXCLUSIVE_INCLUSIVE est utilisé.
Figure 2 : Le segment se développe pour inclure du texte supplémentaire lorsque vous utilisez Spannable.SPAN_EXCLUSIVE_INCLUSIVE.

Vous pouvez associer plusieurs spans au même texte. L'exemple suivant montre comment créer du texte en gras et en rouge:

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
);
Image montrant un texte avec plusieurs segments : "ForegroundColorSpan(Color.RED)" et "StyleSpan(BOLD)"
Figure 3 : Texte avec plusieurs segments : ForegroundColorSpan(Color.RED) et StyleSpan(BOLD).

Types de segments Android

Android fournit plus de 20 types de segments dans le package android.text.style. Android classe les objets Span de deux manières principales:

  • Effet de l'élément span sur le texte: un segment peut avoir une incidence sur l'apparence ou les métriques du texte.
  • Champ d'application des segments: certains segments peuvent être appliqués à des caractères individuels, tandis que d'autres doivent s'appliquer à un paragraphe entier.
Image montrant différentes catégories de segments
Figure 4. Catégories de segments Android.

Les sections suivantes décrivent ces catégories plus en détail.

les segments ayant une incidence sur l'apparence du texte ;

Certains spans qui s'appliquent au niveau du caractère affectent l'apparence du texte, par exemple en modifiant la couleur du texte ou de l'arrière-plan, et en ajoutant des traits de soulignement ou des barrés. Ces segments étendent la classe CharacterStyle.

L'exemple de code suivant montre comment appliquer un élément UnderlineSpan pour souligner le texte:

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);
Image montrant comment souligner du texte à l'aide d'un "UnderlineSpan"
Figure 5 : Texte souligné à l'aide d'un UnderlineSpan.

Les segments qui n'affectent que l'apparence du texte déclenchent un redessin du texte sans déclencher de recalcul de la mise en page. Ces segments implémentent UpdateAppearance et étendent CharacterStyle. Les sous-classes CharacterStyle définissent comment dessiner du texte en fournissant un accès permettant de mettre à jour le TextPaint.

les segments ayant une incidence sur les métriques textuelles ;

D'autres spans qui s'appliquent au niveau des caractères affectent les métriques de texte, telles que la hauteur de ligne et la taille du texte. Ces segments étendent la classe MetricAffectingSpan.

L'exemple de code suivant crée un élément RelativeSizeSpan qui augmente la taille du texte de 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);
Image montrant l'utilisation de RelativeSizeSpan
Figure 6. Texte agrandi à l'aide d'un RelativeSizeSpan.

L'application d'un segment qui affecte les métriques de texte oblige un objet d'observation à mesurer à nouveau le texte pour vérifier la mise en page et le rendu corrects. Par exemple, la modification de la taille du texte peut entraîner l'affichage des mots sur différentes lignes. L'application du segment précédent déclenche une nouvelle mesure, un nouveau calcul de la mise en page du texte et un redessin du texte.

Les segments qui affectent les métriques de texte étendent la classe MetricAffectingSpan, une classe abstraite qui permet aux sous-classes de définir l'impact du segment sur la mesure du texte en donnant accès à TextPaint. Étant donné que MetricAffectingSpan étend CharacterSpan, les sous-classes affectent l'apparence du texte au niveau des caractères.

les segments ayant une incidence sur les paragraphes ;

Un span peut également affecter le texte au niveau du paragraphe, par exemple en modifiant l'alignement ou la marge d'un bloc de texte. Les délais qui affectent des paragraphes entiers implémentent ParagraphStyle. Pour utiliser ces segments, vous devez les associer à l'ensemble du paragraphe, à l'exclusion du caractère de fin de la ligne. Si vous essayez d'appliquer un span de paragraphe à autre chose qu'un paragraphe entier, Android ne l'applique pas du tout.

La figure 8 montre comment Android sépare les paragraphes dans un texte.

Figure 7. Sous Android, les paragraphes se terminent par un caractère de nouvelle ligne (\n).

L'exemple de code suivant applique un élément QuoteSpan à un paragraphe. Notez que si vous l'associez à une position autre qu'au début ou à la fin d'un paragraphe, Android n'applique pas du tout le style.

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);
Image montrant un exemple d'utilisation de CitationSpan
Figure 8 : QuoteSpan appliqué à un paragraphe.

Créer des segments personnalisés

Si vous avez besoin de plus de fonctionnalités que celles fournies dans les segments Android existants, vous pouvez implémenter un segment personnalisé. Lorsque vous implémentez votre propre objet span, déterminez si votre span affecte le texte au niveau du caractère ou du paragraphe, et également s'il affecte la mise en page ou l'apparence du texte. Cela vous aide à déterminer les classes de base que vous pouvez étendre et les interfaces que vous devrez peut-être implémenter. Référez-vous au tableau suivant:

Scénario Classe ou interface
Votre délai affecte le texte au niveau des caractères. CharacterStyle
Votre segment affecte l'apparence du texte. UpdateAppearance
Votre segment affecte les métriques textuelles. UpdateLayout
Votre segment affecte le texte au niveau du paragraphe. ParagraphStyle

Par exemple, si vous devez implémenter un span personnalisé qui modifie la taille et la couleur du texte, étendez RelativeSizeSpan. Grâce à l'héritage, RelativeSizeSpan étend CharacterStyle et implémente les deux interfaces Update. Étant donné que cette classe fournit déjà des rappels pour updateDrawState et updateMeasureState, vous pouvez les ignorer pour implémenter votre comportement personnalisé. Le code suivant crée un délai personnalisé qui étend RelativeSizeSpan et ignore le rappel updateDrawState pour définir la couleur de 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);
    }
}

Cet exemple montre comment créer un objet Span personnalisé. Vous pouvez obtenir le même effet en appliquant RelativeSizeSpan et ForegroundColorSpan au texte.

Tester l'utilisation des segments

L'interface Spanned vous permet à la fois de définir des segments et de les récupérer à partir d'un texte. Lors des tests, implémentez un test JUnit Android pour vérifier que les segments appropriés sont ajoutés aux emplacements appropriés. L'application exemple de style de texte contient un segment qui applique un balisage aux puces en associant BulletPointSpan au texte. L'exemple de code suivant montre comment vérifier si les puces s'affichent comme prévu:

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

Pour plus d'exemples de test, consultez MarkdownBuilderTest sur GitHub.

Tester des segments personnalisés

Lorsque vous testez des segments, vérifiez que TextPaint contient les modifications attendues et que les bons éléments apparaissent dans votre Canvas. Prenons l'exemple d'une implémentation de segment personnalisée qui ajoute une puce au début du texte. La taille et la couleur de la puce sont spécifiées, et il y a un espace entre la marge gauche de la zone drawable et la puce.

Vous pouvez tester le comportement de cette classe en implémentant un test AndroidJUnit et en vérifiant les éléments suivants:

  • Si vous appliquez correctement l'étendue, une puce de la taille et de la couleur spécifiées s'affiche sur le canevas, et l'espace approprié existe entre la marge de gauche et la puce.
  • Si vous n'appliquez pas le segment, aucun des comportements personnalisés ne s'affiche.

Vous pouvez voir l'implémentation de ces tests dans l'exemple TextStyling sur GitHub.

Vous pouvez tester les interactions de Canvas en effectuant une simulation du canevas, en transmettant l'objet fictif à la méthode drawLeadingMargin() et en vérifiant que les méthodes appropriées sont appelées avec les paramètres appropriés.

Vous trouverez d'autres exemples de tests de segments dans BulletPointSpanTest.

Bonnes pratiques d'utilisation des objets Span

Il existe plusieurs moyens efficaces de définir du texte dans un élément TextView en fonction de vos besoins.

Associer ou dissocier un segment sans modifier le texte sous-jacent

TextView.setText() contient plusieurs surcharges qui gèrent les segments différemment. Par exemple, vous pouvez définir un objet texte Spannable avec le code suivant:

Kotlin

textView.setText(spannableObject)

Java

textView.setText(spannableObject);

Lorsque vous appelez cette surcharge de setText(), TextView crée une copie de votre Spannable en tant que SpannedString et la conserve en mémoire en tant que CharSequence. Cela signifie que le texte et les segments sont immuables. Par conséquent, lorsque vous devez les mettre à jour, créez un objet Spannable et appelez à nouveau setText(), ce qui déclenche également une re-mesure et un redessin de la mise en page.

Pour indiquer que les segments doivent être modifiables, vous pouvez utiliser setText(CharSequence text, TextView.BufferType type), comme illustré dans l'exemple suivant:

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

Dans cet exemple, le paramètre BufferType.SPANNABLE entraîne la création par TextView d'une SpannableString, et l'objet CharSequence conservé par TextView comporte désormais un balisage modifiable et du texte immuable. Pour mettre à jour les objets Span, récupérez-les en tant que Spannable, puis mettez-les à jour si nécessaire.

Lorsque vous associez, dissociez ou repositionnez des segments, le TextView est automatiquement mis à jour pour refléter la modification apportée au texte. Si vous modifiez un attribut interne d'un segment existant, appelez invalidate() pour apporter des modifications liées à l'apparence ou requestLayout() pour apporter des modifications liées aux métriques.

Définir plusieurs fois du texte dans un TextView

Dans certains cas, par exemple lorsque vous utilisez un élément RecyclerView.ViewHolder, vous pouvez réutiliser un élément TextView et définir le texte plusieurs fois. Par défaut, que vous définissiez BufferType ou non, TextView crée une copie de l'objet CharSequence et la conserve en mémoire. Toutes les mises à jour de TextView sont donc intentionnelles. Vous ne pouvez pas mettre à jour l'objet CharSequence d'origine pour mettre à jour le texte. Cela signifie que chaque fois que vous définissez du nouveau texte, TextView crée un objet.

Si vous souhaitez mieux contrôler ce processus et éviter la création d'objets supplémentaires, vous pouvez implémenter votre propre Spannable.Factory et ignorer newSpannable(). Au lieu de créer un objet texte, vous pouvez caster et renvoyer le CharSequence existant en tant que Spannable, comme illustré dans l'exemple suivant:

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

Vous devez utiliser textView.setText(spannableObject, BufferType.SPANNABLE) lorsque vous définissez le texte. Sinon, la source CharSequence est créée en tant qu'instance Spanned et ne peut pas être castée en Spannable, ce qui entraîne la génération d'une ClassCastException par newSpannable().

Après avoir remplacé newSpannable(), demandez à TextView d'utiliser le nouveau Factory:

Kotlin

textView.setSpannableFactory(spannableFactory)

Java

textView.setSpannableFactory(spannableFactory);

Définissez l'objet Spannable.Factory une seule fois, juste après avoir obtenu une référence à votre TextView. Si vous utilisez un RecyclerView, définissez l'objet Factory lorsque vous gonflez pour la première fois vos vues. Cela évite la création d'objets supplémentaires lorsque votre RecyclerView associe un nouvel élément à votre ViewHolder.

Modifier les attributs de segment internes

Si vous ne devez modifier qu'un attribut interne d'un segment modifiable, comme la couleur de la puce dans un segment de puce personnalisé, vous pouvez éviter que la surcharge n'appelle setText() plusieurs fois en conservant une référence à l'intervalle lors de sa création. Lorsque vous devez modifier le segment, vous pouvez modifier la référence, puis appeler invalidate() ou requestLayout() sur l'élément TextView, en fonction du type d'attribut que vous avez modifié.

Dans l'exemple de code suivant, une implémentation de liste à puces personnalisée présente une couleur par défaut rouge qui passe au gris lorsque l'utilisateur appuie sur un bouton:

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

Utiliser les fonctions d'extension Android KTX

Android KTX contient également des fonctions d'extension qui facilitent l'utilisation des objets Span. Pour en savoir plus, consultez la documentation du package androidx.core.text.