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.
<ph type="x-smartling-placeholder"></ph> Plusieurs styles dans le texte →

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

Android propose plusieurs types de segments qui couvrent divers textes courants des modèles de style. Vous pouvez également créer vos propres segments pour appliquer un style personnalisé.

Créer et appliquer un segment

Pour créer un segment, 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, le balisage est modifiable et quelle 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 Spanned de commande. SpannableString et SpannableStringBuilder étendent également Spannable.

Voici comment faire votre choix:

  • 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 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 la création et que vous devez joindre des objets Span le texte, utilisez SpannableStringBuilder.
  • Si vous devez associer un grand nombre de segments à un objet texte, quel que soit pour savoir si le texte est en lecture seule, utilisez SpannableStringBuilder.

Pour appliquer un segment, appelez setSpan(Object _what_, int _start_, int _end_, int _flags_) sur un objet Spannable. Le paramètre what fait référence au segment dont vous au texte. Les paramètres start et end indiquent la partie du texte auquel vous appliquez le segment.

Si vous insérez du texte à l'intérieur des limites d'un span, celui-ci se développe automatiquement au format afin d'inclure le texte inséré. Lorsque vous insérez du texte à l'intervalle des limites, c'est-à-dire aux index start ou end, les indicateurs détermine si l'élément span doit se développer pour inclure le texte inséré. Utilisez la Spannable.SPAN_EXCLUSIVE_INCLUSIVE pour inclure le texte inséré et utilisez Spannable.SPAN_EXCLUSIVE_EXCLUSIVE pour exclure le texte inséré.

L'exemple suivant montre comment associer un ForegroundColorSpan en 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 stylisé avec ForegroundColorSpan

Comme l'objet Span est défini à l'aide de Spannable.SPAN_EXCLUSIVE_INCLUSIVE, se développe pour inclure le texte inséré au-delà des limites de l'espace, comme indiqué dans les 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 plus de texte lorsque SPAN_EXCLUSIVE_INCLUSIVE est utilisé.
Figure 2 : Le segment est étendu pour inclure texte supplémentaire lors de l'utilisation Spannable.SPAN_EXCLUSIVE_INCLUSIVE

Vous pouvez joindre plusieurs segments au même texte. L'exemple suivant montre comment pour 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 comportant 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 android.text.style. Android classe les segments de deux manières principales:

  • Effet du segment: ce segment peut influer sur l'apparence du texte ou du texte métriques.
  • Champ d'application des segments: certains segments peuvent être appliqués à des caractères individuels, tandis que d'autres doit s'appliquer à un paragraphe entier.
<ph type="x-smartling-placeholder">
</ph> Image montrant différentes catégories de segments <ph type="x-smartling-placeholder">
</ph> Figure 4. Catégories de segments Android.

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

Segments qui affectent l'apparence du texte

Certains segments qui s'appliquent au niveau du caractère affectent l'apparence du texte. Par exemple : modifier la couleur du texte ou de l'arrière-plan, et souligner ou barrer le texte. Ces étendues étendent CharacterStyle.

L'exemple de code suivant montre comment appliquer un élément UnderlineSpan pour souligner des caractères. 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&#39;aide d&#39;un élément &quot;underlineSpan&quot;
Figure 5 : Texte souligné à l'aide d'une UnderlineSpan

Les segments qui affectent uniquement l'apparence du texte déclenchent une refonte du texte sans ce qui déclenche un recalcul de la mise en page. Ces segments mettent en œuvre UpdateAppearance et étendre CharacterStyle Les sous-classes CharacterStyle définissent comment dessiner du texte en donnant accès à mettre à jour TextPaint.

Segments ayant une incidence sur les métriques textuelles

D'autres segments qui s'appliquent au niveau des caractères ont une incidence sur les métriques textuelles, comme les lignes la hauteur et la taille du texte. Ces segments étendent MetricAffectingSpan .

L'exemple de code suivant crée 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&#39;utilisation de RelativeSizeSpan
Figure 6. Agrandissement du texte à l'aide d'une RelativeSizeSpan

Si vous appliquez un segment qui affecte les métriques de texte, l'objet observé Mesurez à nouveau le texte pour obtenir une mise en page et un rendu corrects (par exemple, en modifiant la taille du texte peut entraîner l'apparition des mots sur des lignes différentes. En appliquant ce qui précède l'intervalle déclenche une nouvelle mesure, le recalcul de la mise en page du texte et le redessin le 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. Puisque MetricAffectingSpan s'étend CharacterSpan, les sous-classes affectent l'apparence du texte au niveau du caractère d'application.

Segments qui affectent les paragraphes

Un segment peut également affecter du texte au niveau du paragraphe, par exemple en modifiant la l'alignement ou la marge d'un bloc de texte. Segments qui affectent des paragraphes entiers implémenter ParagraphStyle À utilisez ces segments, vous les associez à la totalité du paragraphe, à l'exception de la fin de ligne. Si vous essayez d'appliquer un segment de paragraphe à autre chose un paragraphe entier, Android n'applique pas du tout le segment.

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

Figure 7. Dans Android, les paragraphes se terminent par nouvelle ligne (\n).

L'exemple de code suivant applique QuoteSpan en paragraphe. Notez que si vous attachez l'étendue à d'autres positions que le début ou la fin d'un le style n'est pas appliqué par Android.

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 CitationSpan
Figure 8 : QuoteSpan appliquée à un paragraphe.

Créer des segments personnalisés

Si vous avez besoin de plus de fonctionnalités que celles offertes par la version vous pouvez implémenter un segment personnalisé. Lorsque vous implémentez votre propre segment, si votre espace affecte le texte au niveau des caractères ou au niveau du paragraphe, et également s'il affecte la mise en page ou l'apparence du texte. Vous pourrez ainsi Déterminer les classes de base que vous pouvez étendre et les interfaces dont vous pourriez avoir besoin à mettre en œuvre. Utilisez le tableau suivant pour référence:

Scénario Classe ou interface
Le segment affecte le texte au niveau des caractères. CharacterStyle
Le segment a une incidence sur l'apparence du texte. UpdateAppearance
Le segment a une incidence sur les métriques textuelles. UpdateLayout
L'étendue affecte le texte au niveau du paragraphe. ParagraphStyle

Par exemple, si vous devez implémenter un segment personnalisé qui modifie la taille du texte et couleur, étendez RelativeSizeSpan. Par héritage, RelativeSizeSpan étend CharacterStyle et implémente les deux interfaces Update. Étant donné que fournit déjà des rappels pour updateDrawState et updateMeasureState, vous pouvez ignorer ces rappels pour implémenter votre comportement personnalisé. La le code suivant crée un segment personnalisé qui étend 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 segment personnalisé. Vous pouvez obtenir le même résultat en appliquant RelativeSizeSpan et ForegroundColorSpan au texte.

Tester l'utilisation des segments

L'interface Spanned vous permet à la fois de définir et de récupérer des objets Span texte. Lors des tests, implémentez un Android JUnit. Test pour vérifier que les bons segments ont été ajoutés aux bons emplacements. L'exemple de style de texte l'appli contient un segment qui applique le balisage aux puces en joignant BulletPointSpan au texte. L'exemple de code suivant montre comment tester si les puces apparaissent 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 plus d'exemples de tests, consultez MarkdownBuilderTest sur GitHub

Tester les segments personnalisés

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

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

  • Si vous appliquez correctement l'étendue, une puce de la taille spécifiée et couleur apparaît sur la toile, et l'espace approprié existe entre la partie gauche la marge et la puce.
  • Si vous n'appliquez pas le segment, aucun comportement personnalisé ne s'affiche.

Vous pouvez voir la mise en œuvre de ces tests dans la section TextStyling exemple sur GitHub.

Vous pouvez tester les interactions du canevas en vous moquant du canevas, en lui transmettant à l'objet drawLeadingMargin() et en vérifiant que les méthodes appropriées sont appelées avec la bonne méthode paramètres.

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

Bonnes pratiques d'utilisation des objets Span

Il existe plusieurs méthodes économes en mémoire pour définir du texte dans un élément TextView, selon 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éfinissez 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 le conserve en mémoire en tant que CharSequence. Cela signifie que votre texte et les objets Span ne peuvent pas être modifiés. Ainsi, lorsque vous devez modifiez le texte ou les segments, créez un objet Spannable et appelez setText(), ce qui déclenche également une nouvelle mesure et un redessin de la mise en page.

Pour indiquer que les segments doivent être modifiables, vous pouvez utiliser à la place 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, BufferType.SPANNABLE oblige TextView à créer un SpannableString, et le paramètre L'objet CharSequence conservé par TextView comporte désormais un balisage modifiable et texte immuable. Pour mettre à jour le segment, récupérez le texte en tant que Spannable, puis et mettre à jour les segments si nécessaire.

Lorsque vous associez, détachez ou repositionnez des segments, le TextView est automatiquement pour refléter les modifications apportées 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 du texte plusieurs fois dans un TextView

Dans certains cas, par exemple lors de l'utilisation d'un RecyclerView.ViewHolder, vous pouvez réutiliser un TextView et définir le texte plusieurs fois. Par 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. Ainsi, toutes TextView mise à jour intentionnelle (vous ne pouvez pas mettre à jour la version d'origine) CharSequence pour mettre à jour le texte. Autrement dit, chaque fois que vous définissez du texte, TextView crée un objet.

Si vous souhaitez avoir plus de contrôle sur ce processus et éviter l'objet supplémentaire vous pouvez implémenter vos propres modèles Spannable.Factory et ignorer newSpannable() Au lieu de créer un objet textuel, vous pouvez caster et renvoyer le texte existant CharSequence 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 pour définir le texte. Sinon, la source CharSequence est créée en tant que Spanned. et ne peuvent pas être castées en Spannable. newSpannable() génère alors une ClassCastException

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 d'abord vos vues. Cela permet d'éviter la création d'objets supplémentaires 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, tel que le dans une liste à puces personnalisée, ce qui vous évite d'avoir à appeler setText() plusieurs fois en conservant une référence au segment lors de sa création. Lorsque vous devez modifier l'objet Span, vous pouvez modifier la référence, puis appeler invalidate() ou requestLayout() sur le TextView, selon le type de que vous avez modifié.

Dans l'exemple de code suivant, une implémentation personnalisée avec puce couleur par défaut du rouge qui devient 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 permettent d'utiliser des objets Span. plus facile. Pour en savoir plus, consultez la documentation androidx.core.text. d'un package.