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 à 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 redimensionnant sa taille et en dessinant le 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 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, si son balisage est ou non, et selon la structure de données sous-jacente qui 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 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 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 lui-même 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 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 le segment.
Si vous insérez du texte à l'intérieur des limites d'un span, celui-ci se développe automatiquement pour inclure le texte inséré. Lorsque vous insérez du texte au niveau des limites de l'intervalle, c'est-à-dire aux index start ou end, le paramètre flags détermine si l'objet 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 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 );
Comme le span est défini à l'aide de Spannable.SPAN_EXCLUSIVE_INCLUSIVE
, il se développe pour inclure le texte inséré dans ses limites, 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)");
Vous pouvez joindre plusieurs segments 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 );
Types de segments Android
Android fournit plus de 20 types de segments dans le package android.text.style. Android classe les segments de deux manières principales:
- Impact du segment sur le texte: il peut affecter l'apparence du texte ou ses métriques.
- 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.
Les sections suivantes décrivent ces catégories plus en détail.
Segments qui affectent l'apparence du texte
Certains intervalles qui s'appliquent au niveau des caractères affectent l'apparence du texte. Par exemple, vous pouvez modifier la couleur du texte ou de l'arrière-plan, et le souligner ou le barrer. Ces délais é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);
Les segments qui n'affectent que l'apparence du texte déclenchent une redessination 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 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 de texte, telles que la hauteur de la ligne et la taille du texte. Ces délais étendent la classe MetricAffectingSpan
.
L'exemple de code suivant crée un objet 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);
L'application d'un segment qui affecte les métriques de texte oblige un objet d'observation à mesurer à nouveau le texte pour une mise en page et un rendu corrects. Par exemple, la modification de la taille du texte peut entraîner l'apparition de mots sur des lignes différentes. L'application du segment précédent déclenche une nouvelle mesure, le recalcul de la mise en page du texte et le 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.
Segments qui affectent les paragraphes
Un intervalle peut également affecter le texte au niveau du paragraphe, en modifiant l'alignement ou la marge d'un bloc de texte, par exemple. Les segments qui affectent des paragraphes entiers implémentent ParagraphStyle
. Pour utiliser ces segments, vous devez les associer à l'intégralité du paragraphe, à l'exception du caractère de fin de ligne. Si vous essayez d'appliquer un segment 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 le texte.
L'exemple de code suivant applique un élément QuoteSpan
à un paragraphe. Notez que si vous associez l'objet span à 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);
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 délai personnalisé. Lorsque vous implémentez votre propre segment, déterminez s'il 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 permet de déterminer les classes de base que vous pouvez étendre et les interfaces que vous devrez peut-être implémenter. 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 objet span personnalisé qui modifie la taille et la couleur du texte, étendez RelativeSizeSpan
. Par 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 ignorer ces rappels 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 segment 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 et de récupérer des objets Span à partir d'un texte. Lors des tests, implémentez un test Android JUnit pour vérifier que les délais corrects sont ajoutés aux bons emplacements. L'application exemple de style de texte contient un élément span qui applique un balisage aux puces en joignant BulletPointSpan
au texte. L'exemple de code suivant montre comment vérifier 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 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 d'un texte. La taille et la couleur de la puce sont spécifiées, et il existe 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 apparaît sur le canevas, et l'espace approprié existe entre la marge de gauche 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 l'exemple TextStyling sur GitHub.
Vous pouvez tester les interactions de canevas 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 bons paramètres.
Vous trouverez d'autres exemples de tests de délai 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
, 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 délais 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 votre texte et les objets Span sont immuables. Par conséquent, lorsque vous devez mettre à jour le texte ou les objets Span, 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 segments doivent être modifiables, utilisez plutôt 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
oblige TextView
à créer un SpannableString
, et l'objet CharSequence
conservé par TextView
comporte désormais un balisage modifiable et un texte immuable. Pour mettre à jour le segment, récupérez le texte sous forme de Spannable
, puis mettez à jour les objets Span si nécessaire.
Lorsque vous associez, dissociez ou repositionnez des objets Span, TextView
se met automatiquement à 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 effectuer des modifications liées aux métriques.
Définir du texte plusieurs fois dans un TextView
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 BufferType
ou non, TextView
crée une copie de l'objet CharSequence
et la conserve en mémoire. Ainsi, toutes les mises à jour de TextView
sont 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 texte, TextView
crée un objet.
Si vous souhaitez avoir davantage de contrôle sur 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
. newSpannable()
génère donc une exception 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 vos vues pour la première fois. 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, tel que la couleur de la puce dans un segment personnalisé, vous pouvez éviter d'appeler setText()
plusieurs fois en conservant une référence au segment lors de sa création.
Lorsque vous devez modifier le segment, vous pouvez modifier la référence, puis appeler invalidate()
ou requestLayout()
au niveau de l'élément TextView
, selon le type d'attribut que vous avez modifié.
Dans l'exemple de code suivant, une implémentation personnalisée de puces a une couleur par défaut (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 facilitent l'utilisation des objets Span. Pour en savoir plus, consultez la documentation du package androidx.core.text.