Los intervalos son objetos de marcado eficaces que puedes usar para dar estilo al texto a nivel de carácter o párrafo. Si adjuntas intervalos a los objetos de texto, puedes cambiar el texto de varias maneras, como agregar color, agregar la posibilidad de hacer clic, ajustar el tamaño y dibujarlo de manera personalizada. Los intervalos también pueden
cambiar TextPaint propiedades, dibujar en un
Canvas o incluso cambiar el diseño del texto.
Android proporciona varios tipos de intervalos que abarcan una variedad de patrones de estilo de texto comunes. También puedes crear tus propios intervalos para aplicar un estilo personalizado.
Cómo crear y aplicar un intervalo
Para crear un intervalo, puedes usar una de las clases que se enumeran en la siguiente tabla. Las clases difieren según si el texto en sí es mutable, si el lenguaje de marcado del texto es mutable y qué estructura de datos subyacente contiene los datos del intervalo.
| Clase | Texto mutable | Lenguaje de marcado mutable | Estructura de datos |
|---|---|---|---|
SpannedString |
No | No | Arreglo lineal |
SpannableString |
No | Sí | Arreglo lineal |
SpannableStringBuilder |
Sí | Sí | Árbol de intervalos |
Las tres clases extienden la Spanned
interfaz. SpannableString y SpannableStringBuilder también extienden la
Spannable interfaz.
A continuación, se explica cómo decidir cuál usar:
- Si no tienes pensado modificar el texto o el lenguaje de marcado después de crearlo, usa
SpannedString. - Si necesitas adjuntar una pequeña cantidad de intervalos a un solo objeto de texto y el texto en sí es de solo lectura, usa
SpannableString. - Si necesitas modificar el texto después de crearlo y adjuntar intervalos al texto, usa
SpannableStringBuilder. - Si necesitas adjuntar una gran cantidad de intervalos a un objeto de texto, independientemente de si el texto en sí es de solo lectura, usa
SpannableStringBuilder.
Para aplicar un intervalo, llama a setSpan(Object _what_, int _start_, int _end_, int
_flags_)
en un objeto Spannable. El parámetro what hace referencia al intervalo que aplicas al texto, y los parámetros start y end indican la parte del texto a la que aplicas el intervalo.
Si insertas texto dentro de los límites de un intervalo, se expandirá automáticamente para incluir el texto insertado. Cuando insertas texto en los límites del intervalo, es decir, en los índices start o end, el parámetro flags determina si el intervalo se expande para incluir el texto insertado. Usa
la
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
marca para incluir el texto insertado y usa
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
para excluirlo.
En el siguiente ejemplo, se muestra cómo adjuntar un
ForegroundColorSpan a una
cadena:
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 );
ForegroundColorSpan.
Dado que se lo configuró con Spannable.SPAN_EXCLUSIVE_INCLUSIVE, el intervalo se expande para incluir el texto insertado dentro de sus límites, como se muestra en el siguiente ejemplo:
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)");
Spannable.SPAN_EXCLUSIVE_INCLUSIVE.
Puedes adjuntar varios intervalos al mismo texto. En el siguiente ejemplo, se muestra cómo crear texto en negrita y rojo:
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 );
ForegroundColorSpan(Color.RED) y
StyleSpan(BOLD).
Tipos de intervalo de Android
Android ofrece más de 20 tipos de intervalo en el paquete android.text.style. Android categoriza los intervalos de dos formas principales:
- Cómo afecta al texto un intervalo: Un intervalo puede afectar la apariencia o las métricas del texto.
- El alcance del intervalo: Algunos intervalos se pueden aplicar a caracteres individuales, mientras que otros se deben aplicar a un párrafo completo.
En las siguientes secciones, se describen estas categorías con más detalle.
Intervalos que afectan la apariencia del texto
Algunos intervalos que se aplican a nivel de carácter afectan la apariencia del texto; por ejemplo, pueden cambiar el color del texto o del fondo, y agregar contenido subrayado o tachado. Estos
intervalos extienden la
CharacterStyle clase.
En el siguiente ejemplo de código, se muestra cómo aplicar un UnderlineSpan para subrayar el texto:
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);
UnderlineSpan.
Los intervalos que afectan solo la apariencia del texto activan un rediseño del texto sin activar un nuevo cálculo del diseño. Estos intervalos implementan
UpdateAppearance y extienden
CharacterStyle.
Las subclases CharacterStyle definen cómo dibujar texto proporcionando acceso para actualizar el TextPaint.
Intervalos que afectan las métricas del texto
Otros intervalos que se aplican a nivel de carácter afectan las métricas del texto, como la altura de la línea y el tamaño del texto. Estos intervalos extienden la
MetricAffectingSpan
clase.
En el siguiente ejemplo de código, se crea un
RelativeSizeSpan que
aumenta el tamaño del texto en un 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);
RelativeSizeSpan.
Aplicar un intervalo que afecta las métricas del texto hace que un objeto de observación vuelva a medir el texto para un diseño y un procesamiento correctos; por ejemplo, si se cambia el tamaño del texto, es posible que las palabras aparezcan en diferentes líneas. La aplicación del intervalo anterior activa una nueva medición, así como un nuevo cálculo de diseño y un nuevo diseño del texto.
Los intervalos que afectan las métricas del texto extienden la clase MetricAffectingSpan, una clase abstracta que permite que las subclases definan cómo el intervalo afecta la medición del texto al permitir el acceso a TextPaint. Dado que MetricAffectingSpan extiende CharacterStyle, las subclases afectan la apariencia del texto a nivel de carácter.
Intervalos que afectan párrafos
Un intervalo también puede afectar el texto a nivel de párrafo, como cambiar la alineación o el margen de un bloque de texto. Los intervalos que afectan los párrafos completos
implementan ParagraphStyle. Para usar esos intervalos, deberás adjuntarlos al párrafo completo y excluir el carácter final de la nueva línea. Si intentas aplicar un intervalo de párrafo a algo que no sea un párrafo completo, Android no aplicará el intervalo en absoluto.
En la Figura 8, se muestra cómo Android separa los párrafos en el texto.
\n).
En el siguiente ejemplo de código, se aplica un
QuoteSpan a un párrafo. Ten en cuenta que, si adjuntas el intervalo a cualquier posición que no sea el principio o el final de un párrafo, Android no aplicará el estilo en absoluto.
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);
QuoteSpan
aplicado a un párrafo.
Cómo crear intervalos personalizados
Si necesitas más funciones que las que se ofrecen en los intervalos existentes de Android, puedes implementar un intervalo personalizado. Cuando implementes tu propio intervalo, decide si afecta el texto a nivel de carácter o de párrafo, y si afecta el diseño o la apariencia del texto. De esta forma, podrás determinar qué clases básicas puedes extender y qué interfaces deberías implementar. Usa la siguiente tabla como referencia:
| Situación | Clase o interfaz |
|---|---|
| El intervalo afecta el texto a nivel de carácter. | CharacterStyle |
| El intervalo afecta la apariencia del texto. | UpdateAppearance |
| El intervalo afecta las métricas del texto. | UpdateLayout |
| El intervalo afecta el texto a nivel de párrafo. | ParagraphStyle |
Por ejemplo, si necesitas implementar un intervalo personalizado que modifique el tamaño y el color del texto, extiende RelativeSizeSpan. A través de la herencia, RelativeSizeSpan extiende CharacterStyle e implementa las dos interfaces Update. Dado que esta clase ya proporciona devoluciones de llamada para updateDrawState y updateMeasureState, puedes anular estas devoluciones de llamada para implementar tu comportamiento personalizado. En el siguiente código, se crea un intervalo personalizado que extiende RelativeSizeSpan y anula la devolución de llamada updateDrawState para establecer el color 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); } }
En este ejemplo, se muestra cómo crear un intervalo personalizado. Puedes lograr el mismo efecto si aplicas un RelativeSizeSpan y un ForegroundColorSpan al texto.
Uso del intervalo de prueba
La interfaz Spanned te permite configurar y recuperar intervalos de texto. Cuando hagas la prueba, debes implementar una prueba JUnit de Android
para verificar que se agreguen los intervalos correctos
en las ubicaciones adecuadas. La app de ejemplo de estilo de texto
contiene un intervalo que aplica el lenguaje de marcado a las viñetas adjuntando
BulletPointSpan al texto. En el siguiente ejemplo de código, se muestra cómo probar si las viñetas aparecen como se espera:
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)); }
Para obtener más ejemplos de pruebas, consulta MarkdownBuilderTest en GitHub.
Prueba intervalos personalizados
Cuando pruebes intervalos, verifica que TextPaint contenga las modificaciones esperadas y que los elementos correctos aparezcan en tu Canvas. Por ejemplo, piensa en la implementación de un intervalo personalizado que anexa una viñeta a alguna parte del texto. La viñeta tiene un color y un tamaño especificados, y existe un espacio entre el margen izquierdo del área del elemento de diseño y la viñeta.
Puedes probar el comportamiento de esta clase mediante la implementación de una prueba AndroidJUnit y verificar lo siguiente:
- Si aplicas correctamente el intervalo, aparecerá una viñeta del tamaño y el color especificados en el lienzo, y se aplicará el espacio adecuado entre el margen izquierdo y la viñeta.
- Si no aplicas el intervalo, no aparecerá ninguno de los comportamientos personalizados.
Puedes ver la implementación de estas pruebas en el TextStyling ejemplo en GitHub.
Puedes probar las interacciones de Canvas si simulas el lienzo, pasas el objeto simulado
al
drawLeadingMargin()
método y verificas que se llame a los métodos correctos con los parámetros
adecuados.
Puedes encontrar más ejemplos de pruebas de intervalo en BulletPointSpanTest.
Prácticas recomendadas para usar intervalos
Existen varias maneras eficientes en términos de memoria de configurar el texto en una TextView según tus necesidades.
Cómo adjuntar o separar un intervalo sin cambiar el texto subyacente
TextView.setText()
contiene varias sobrecargas que controlan los intervalos de diferente manera. Por ejemplo, puedes configurar un objeto de texto Spannable con el siguiente código:
Kotlin
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
Cuando llamas a esta sobrecarga de setText(), TextView crea una copia de tu Spannable como SpannedString y la guarda en la memoria como CharSequence.
Esto significa que el texto y los intervalos son inmutables. Por lo tanto, cuando necesitas actualizar el texto o los intervalos, debes crear un nuevo objeto Spannable y volver a llamar a setText(), que también activa una nueva medición y un nuevo dibujo del diseño.
Para indicar que los intervalos deben ser mutables, puedes usar
setText(CharSequence text, TextView.BufferType
type),
como se muestra en el siguiente ejemplo:
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);
En este ejemplo, el
BufferType.SPANNABLE
parámetro hace que TextView cree un SpannableString, y el
CharSequence objeto que mantiene TextView ahora tiene lenguaje de marcado mutable y
texto inmutable. Para actualizar el intervalo, recupera el texto como un Spannable y, luego, actualiza los intervalos según sea necesario.
Cuando adjuntas, separas o cambias de posición los intervalos, TextView se actualiza automáticamente para reflejar el cambio en el texto. Si cambias un atributo interno de un intervalo existente, llama a invalidate() para realizar cambios relacionados con la apariencia o requestLayout() para realizar cambios relacionados con las métricas.
Cómo configurar texto en TextView varias veces
En algunos casos, como cuando se usa un
RecyclerView.ViewHolder,
es posible que quieras reutilizar un TextView y configurar el texto varias veces. De forma predeterminada, independientemente de si configuras el BufferType, TextView crea una copia del objeto CharSequence y la guarda en la memoria. Esto hace que todas las actualizaciones de TextView sean intencionales. No puedes actualizar el objeto CharSequence original para actualizar el texto. Esto significa que cada vez que configuras un nuevo texto, TextView crea un nuevo objeto.
Si deseas tener más control sobre este proceso y evitar la creación de objetos adicionales, puedes implementar tu propio
Spannable.Factory y anular
newSpannable().
En vez de crear un nuevo objeto de texto, simplemente puedes convertir y mostrar el CharSequence existente como Spannable, como se muestra en el siguiente ejemplo:
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; } };
Debes usar textView.setText(spannableObject, BufferType.SPANNABLE) cuando configures el texto. De lo contrario, la fuente CharSequence se crea como una instancia Spanned
y no se puede convertir a Spannable, lo que hace que newSpannable() arroje una
ClassCastException.
Después de anular newSpannable(), dile a TextView que use la nueva Factory:
Kotlin
textView.setSpannableFactory(spannableFactory)
Java
textView.setSpannableFactory(spannableFactory);
Configura el objeto Spannable.Factory una vez, justo después de obtener una referencia a tu TextView. Si estás usando un RecyclerView, configura el objeto Factory cuando aumentes las vistas por primera vez. Esto evita la creación de objetos adicionales cuando tu RecyclerView vincula un elemento nuevo a tu ViewHolder.
Cómo cambiar los atributos de intervalos internos
Si necesitas cambiar solo un atributo interno de un intervalo mutable, como el color de la viñeta en un intervalo personalizado, puedes evitar que la sobrecarga llame a setText() varias veces si mantienes una referencia al intervalo a medida que se crea.
Cuando necesites modificar el intervalo, puedes modificar la referencia y, luego, llamar a invalidate() o requestLayout() en TextView, según el tipo de atributo que cambiaste.
En el siguiente ejemplo de código, una implementación personalizada de viñetas es de color rojo de forma predeterminada, y cambia a color gris cuando se presiona un botón:
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(); } }); } }
Cómo usar las funciones de extensión de Android KTX
Android KTX también contiene funciones de extensión que facilitan el trabajo con los intervalos. Para obtener más información, consulta la documentación del paquete androidx.core.text.