Segments

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

Les spans sont des objets de balisage puissants que vous pouvez utiliser pour mettre en forme du texte au niveau des caractères ou des paragraphes. En associant des étendues à des objets de texte, vous pouvez modifier le texte de différentes manières, par exemple en ajoutant de la couleur, en rendant le texte cliquable, en modifiant la taille du texte et en dessinant le texte de manière personnalisée. Les spans peuvent également modifier les propriétés TextPaint, dessiner sur un Canvas et modifier la mise en page du texte.

Android propose plusieurs types de spans qui couvrent différents modèles courants de mise en forme de texte. Vous pouvez également créer vos propres étendues pour appliquer un style personnalisé.

Créer et appliquer un délai

Pour créer un span, vous pouvez utiliser l'une des classes listées dans le tableau suivant. Les classes diffèrent selon que le texte lui-même est mutable, que le balisage du texte est mutable et quelle structure de données sous-jacente contient les données de portée.

Classe Texte modifiable Balisage modifiable Structure des données
SpannedString Non Non Tableau linéaire
SpannableString Non Oui Tableau linéaire
SpannableStringBuilder Oui Oui Arbre d'intervalles

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

Voici comment choisir celle à utiliser :

  • Si vous ne modifiez pas le texte ni le balisage après la création, utilisez SpannedString.
  • Si vous devez associer un petit nombre de spans à un seul objet de 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 que vous devez y associer des spans, utilisez SpannableStringBuilder.
  • Si vous devez associer un grand nombre de spans à un objet texte, que le texte lui-même soit en lecture seule ou non, utilisez SpannableStringBuilder.

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

Si vous insérez du texte dans les limites d'une étendue, celle-ci se développe automatiquement pour inclure le texte inséré. Lorsque vous insérez du texte aux limites de la plage (c'est-à-dire aux index start ou end), le paramètre flags détermine si la plage s'étend 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 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 montrant un texte gris, partiellement rouge.
Figure 1 : Texte mis en forme avec un ForegroundColorSpan.

Étant donné que la portée est définie à l'aide de Spannable.SPAN_EXCLUSIVE_INCLUSIVE, elle s'étend pour inclure le texte inséré aux limites de la portée, 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 la portée inclut plus de texte lorsque SPAN_EXCLUSIVE_INCLUSIVE est utilisé.
Figure 2 : L'étendue se développe pour inclure du texte supplémentaire lorsque vous utilisez Spannable.SPAN_EXCLUSIVE_INCLUSIVE.

Vous pouvez associer plusieurs étendues 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 spans : `ForegroundColorSpan(Color.RED)` et `StyleSpan(BOLD)`
Figure 3 : Texte avec plusieurs étendues : ForegroundColorSpan(Color.RED) et StyleSpan(BOLD).

Types de spans Android

Android propose plus de 20 types de spans dans le package android.text.style. Android catégorise les étendues de deux manières principales :

  • Comment la portée affecte le texte : une portée peut affecter l'apparence ou les métriques du texte.
  • Étendue des spans : certains spans peuvent être appliqués à des caractères individuels, tandis que d'autres doivent être appliqués à un paragraphe entier.
Image montrant différentes catégories de portée
Figure 4. Catégories de spans Android.

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

Spans qui affectent l'apparence du texte

Certaines étendues qui s'appliquent au niveau des caractères affectent l'apparence du texte, par exemple en modifiant la couleur du texte ou de l'arrière-plan, et en ajoutant des soulignements ou des barrages. Ces étendues étendent la classe CharacterStyle.

L'exemple de code suivant montre comment appliquer un 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 spans qui n'affectent que l'apparence du texte déclenchent un réaffichage du texte sans déclencher un nouveau calcul de la mise en page. Ces étendues implémentent UpdateAppearance et étendent CharacterStyle. Les sous-classes CharacterStyle définissent comment dessiner du texte en fournissant un accès pour mettre à jour TextPaint.

Portées ayant une incidence sur les métriques de texte

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

L'exemple de code suivant crée un 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'une étendue qui affecte les métriques de texte entraîne une nouvelle mesure du texte par un objet d'observation pour une mise en page et un rendu corrects. Par exemple, la modification de la taille du texte peut entraîner l'affichage de mots sur différentes lignes. L'application de la portée précédente déclenche une nouvelle mesure, un nouveau calcul de la mise en page du texte et un nouveau dessin du texte.

Les étendues qui affectent les métriques de texte étendent la classe MetricAffectingSpan, une classe abstraite qui permet aux sous-classes de définir comment l'étendue affecte la mesure du texte en donnant accès à TextPaint. Comme MetricAffectingSpan étend CharacterStyle, les sous-classes affectent l'apparence du texte au niveau des caractères.

Spans qui affectent les paragraphes

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

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

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

L'exemple de code suivant applique un QuoteSpan à un paragraphe. Notez que si vous associez la plage à une position autre que le 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 de QuoteSpan
Figure 8 : QuoteSpan appliqué à un paragraphe.

Créer des spans personnalisés

Si vous avez besoin de plus de fonctionnalités que celles fournies dans les étendues Android existantes, vous pouvez implémenter une étendue personnalisée. Lorsque vous implémentez votre propre span, déterminez s'il affecte le texte au niveau du caractère ou du paragraphe, et 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. Pour vous aider, consultez le tableau suivant :

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

Par exemple, si vous devez implémenter une étendue personnalisée 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 remplacer ces rappels pour implémenter votre comportement personnalisé. Le code suivant crée une étendue personnalisée qui s'étend sur RelativeSizeSpan et remplace 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 délai personnalisé. Vous pouvez obtenir le même effet en appliquant un RelativeSizeSpan et un ForegroundColorSpan au texte.

Tester l'utilisation des étendues

L'interface Spanned vous permet à la fois de définir et de récupérer des étendues à partir du texte. Lors des tests, implémentez un test JUnit Android pour vérifier que les portées appropriées sont ajoutées aux bons emplacements. L'exemple d'application de style de texte contient une étendue qui applique une mise en forme aux puces en associant BulletPointSpan au texte. L'exemple de code suivant montre comment tester 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 obtenir d'autres exemples de tests, consultez MarkdownBuilderTest sur GitHub.

Tester les spans personnalisés

Lorsque vous testez des étendues, vérifiez que TextPaint contient les modifications attendues et que les éléments corrects s'affichent sur votre Canvas. Prenons l'exemple d'une implémentation de span personnalisée qui ajoute une puce à du texte. La puce a une taille et une couleur spécifiques, et il existe un espace entre la marge de gauche de la zone dessinable 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 l'étendue, aucun comportement personnalisé ne s'affiche.

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

Vous pouvez tester les interactions Canvas en simulant le canevas, en transmettant l'objet simulé à 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 portée dans BulletPointSpanTest.

Bonnes pratiques pour l'utilisation des spans

Il existe plusieurs façons de définir du texte dans un TextView de manière efficace en termes de mémoire, en fonction de vos besoins.

Attacher ou détacher une étendue sans modifier le texte sous-jacent

TextView.setText() contient plusieurs surcharges qui gèrent les étendues différemment. Par exemple, vous pouvez définir un objet de 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 votre texte et vos étendues sont immuables. Par conséquent, lorsque vous devez mettre à jour le texte ou les étendues, créez un objet Spannable et appelez à nouveau setText(), ce qui déclenche également une nouvelle mesure et un nouveau dessin de la mise en page.

Pour indiquer que les portées doivent être mutables, vous pouvez utiliser setText(CharSequence text, TextView.BufferType type), comme indiqué 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 amène TextView à créer un SpannableString, et l'objet CharSequence conservé par TextView possède désormais un balisage mutable et un texte immutable. Pour mettre à jour la portée, récupérez le texte en tant que Spannable, puis mettez à jour les portées si nécessaire.

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

Définir le texte dans un TextView plusieurs fois

Dans certains cas, par exemple lorsque vous utilisez un RecyclerView.ViewHolder, vous pouvez réutiliser un TextView et définir le texte plusieurs fois. Par défaut, que vous définissiez ou non BufferType, TextView crée une copie de l'objet CharSequence et la conserve en mémoire. Cela signifie que toutes les mises à jour de TextView sont intentionnelles. Vous ne pouvez pas mettre à jour l'objet CharSequence d'origine pour modifier le texte. Cela signifie que chaque fois que vous définissez un 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 remplacer newSpannable(). Au lieu de créer un objet de texte, vous pouvez caster et renvoyer l'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 CharSequence source est créée en tant qu'instance Spanned et ne peut pas être convertie en Spannable, ce qui entraîne la génération d'une ClassCastException par newSpannable().

Après avoir remplacé newSpannable(), indiquez à 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 vos vues pour la première fois. Cela évite la création d'objets supplémentaires lorsque votre RecyclerView lie un nouvel élément à votre ViewHolder.

Modifier les attributs de portée interne

Si vous n'avez besoin de modifier qu'un attribut interne d'une étendue mutable, comme la couleur de la puce dans une étendue de puce personnalisée, vous pouvez éviter la surcharge liée à l'appel de setText() à plusieurs reprises en conservant une référence à l'étendue lors de sa création. Lorsque vous devez modifier la portée, vous pouvez modifier la référence, puis appeler invalidate() ou requestLayout() sur TextView, en fonction du type d'attribut que vous avez modifié.

Dans l'exemple de code suivant, une implémentation de puce personnalisée a une couleur par défaut rouge qui devient grise lorsqu'un bouton est appuyé :

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 spans. Pour en savoir plus, consultez la documentation du package androidx.core.text.