Períodos

Testar o Compose
O Jetpack Compose é o kit de ferramentas de interface recomendado para Android. Aprenda a usar texto no Compose.

Os períodos são poderosos objetos de marcação que podem ser usados para definir o estilo do texto em nível de caractere ou parágrafo. Ao anexar períodos a objetos de texto, é possível mudar o texto de diversas formas, incluindo adicionar cor, tornar o texto clicável, dimensionar o tamanho do texto e desenhar o texto de maneira personalizada. Os períodos também podem mudar as propriedades de TextPaint, desenhar em Canvas e mudar o layout do texto.

O Android oferece vários tipos de períodos que abrangem uma variedade de padrões comuns de estilo de texto. Você também pode criar seus próprios períodos para aplicar estilos personalizados.

Criar e aplicar um período

Para criar um período, você pode usar uma das classes listadas na tabela a seguir. As classes são diferentes, dependendo de se o texto é mutável, se a marcação de texto é mutável e de qual estrutura de dados subjacente contém os dados de período.

Classe Texto mutável Marcação mutável Estrutura de dados
SpannedString Não Não Matriz linear
SpannableString Não Sim Matriz linear
SpannableStringBuilder Sim Sim Árvore de intervalo

Todas as três classes estendem a interface Spanned. SpannableString e SpannableStringBuilder também estendem a interface Spannable.

Veja como decidir qual usar:

  • Caso não pretenda modificar o texto ou a marcação após a criação, use SpannedString.
  • Se precisar anexar um pequeno número de períodos a um único objeto de texto e o texto for somente leitura, use SpannableString.
  • Caso precise modificar o texto após a criação e anexar períodos ao texto, use SpannableStringBuilder.
  • Se precisar anexar um grande número de períodos a um objeto de texto, independentemente de o texto ser somente leitura, use SpannableStringBuilder.

Para aplicar um período, chame setSpan(Object _what_, int _start_, int _end_, int _flags_) em um objeto Spannable. O parâmetro what se refere ao período que você está aplicando ao texto, e os parâmetros start e end indicam a parte do texto em que você está aplicando o período.

Se você inserir texto dentro dos limites de um período, ele será expandido automaticamente para incluir o texto inserido. Ao inserir texto nos limites do período, ou seja, nos índices de start ou end, o parâmetro flags determina se o período será expandido para incluir o texto inserido. Use a flag Spannable.SPAN_EXCLUSIVE_INCLUSIVE para incluir e Spannable.SPAN_EXCLUSIVE_EXCLUSIVE para excluir o texto inserido.

O exemplo a seguir mostra como anexar um ForegroundColorSpan a uma string:

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
);
Uma imagem mostrando um texto cinza, parcialmente vermelho.
Figura 1. Texto estilizado com um ForegroundColorSpan.

Como o período foi definido usando Spannable.SPAN_EXCLUSIVE_INCLUSIVE, ele se expande para incluir o texto inserido nos limites do período, conforme mostrado no exemplo abaixo:

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)");
Uma imagem mostrando como o período inclui mais texto quando SPAN_EXCLUSIVE_INCLUSIVE é usado.
Figura 2. O período se expande para incluir mais texto ao usar Spannable.SPAN_EXCLUSIVE_INCLUSIVE.

Você pode anexar vários períodos ao mesmo texto. O exemplo a seguir mostra como criar um texto em negrito e vermelho:

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
);
Uma imagem que mostra um texto com vários spans: "ForegroundColorSpan(Color.RED)" e "StyleSpan(BOLD)"
Figura 3. Texto com vários períodos: ForegroundColorSpan(Color.RED) e StyleSpan(BOLD).

Tipos de períodos do Android

O Android oferece mais de 20 tipos de períodos no pacote android.text.style. Ele categoriza os períodos de duas formas principais:

  • Como o período afeta o texto: um período pode afetar a aparência ou as métricas de texto.
  • Escopo do período: alguns períodos podem ser aplicados a caracteres individuais, enquanto outros precisam ser aplicados a um parágrafo inteiro.
Uma imagem mostrando diferentes categorias de extensão
Figura 4. Categorias de períodos do Android.

As seções a seguir descrevem essas categorias em mais detalhes.

Períodos que afetam a aparência do texto

Alguns períodos aplicados no nível do caractere afetam a aparência do texto, por exemplo, mudando a cor do texto ou do plano de fundo e adicionando sublinhados ou tachados. Esses períodos estendem a classe CharacterStyle.

O exemplo de código abaixo mostra como aplicar um UnderlineSpan para sublinhar o 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);
Uma imagem mostrando como sublinhar texto usando um UnderlineSpan
Figura 5. Texto sublinhado usando um UnderlineSpan.

Períodos que afetam apenas a aparência do texto acionam um redesenho do texto sem acionar um novo cálculo do layout. Esses períodos implementam UpdateAppearance e ampliam CharacterStyle. As subclasses de CharacterStyle definem como desenhar texto, fornecendo acesso para atualizar o TextPaint.

Períodos que afetam as métricas de texto

Outros períodos aplicados no nível do caractere afetam as métricas de texto, como a altura da linha e o tamanho do texto. Esses períodos estendem a classe MetricAffectingSpan.

O exemplo de código abaixo cria um RelativeSizeSpan que aumenta o tamanho do texto em 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);
Uma imagem mostrando o uso de RelativeSizeSpan
Figura 6. Texto aumentado usando um RelativeSizeSpan.

Aplicar um período que afeta a métrica de texto faz com que um objeto de observação meça novamente o texto para conseguir o layout e a renderização corretos. Por exemplo, a alteração do tamanho do texto pode fazer com que as palavras apareçam em linhas diferentes. A aplicação do período anterior aciona uma nova medição, um novo cálculo do layout do texto e um novo desenho do texto.

Os períodos que afetam as métricas de texto estendem a classe MetricAffectingSpan, uma classe abstrata que permite que as subclasses definam como o período afeta a medição de texto fornecendo acesso ao TextPaint. Como MetricAffectingSpan estende CharacterStyle, as subclasses afetam a aparência dos caracteres do texto.

Períodos que afetam parágrafos

Um período também pode afetar os parágrafos de um texto, por exemplo, mudando o alinhamento ou a margem de um bloco de texto. Períodos que afetam parágrafos inteiros implementam ParagraphStyle. Para usar esses períodos, anexe-os ao parágrafo inteiro, excluindo o caractere final de nova linha. Se você tentar aplicar um período de parágrafo a algo que não seja um parágrafo inteiro, o Android não o aplicará.

A figura 8 mostra como o Android separa os parágrafos no texto.

Figura 7. No Android, os parágrafos terminam com um caractere de nova linha (\n).

O exemplo de código a seguir aplica um QuoteSpan a um parágrafo. Se você anexar o período a qualquer posição que não seja o início ou o fim de um parágrafo, o Android não aplicará o estilo.

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);
Uma imagem mostrando um exemplo de QuoteSpan
Figura 8. Um QuoteSpan aplicado a um parágrafo.

Criar períodos personalizados

Caso você precise de mais funcionalidades do que as fornecidas nos períodos existentes do Android, é possível implementar um período personalizado. Ao implementar seu próprio período, decida se ele afeta o texto no nível do caractere ou do parágrafo e se afeta o layout ou a aparência do texto. Isso ajuda a determinar quais classes base podem ser estendidas e quais interfaces pode ser necessário implementar. Use a tabela a seguir como referência:

Cenário Classe ou interface
Seu período afeta os caracteres do texto. CharacterStyle
Seu período afeta a aparência do texto. UpdateAppearance
Seu período afeta as métricas de texto. UpdateLayout
Seu período afeta os parágrafos do texto. ParagraphStyle

Por exemplo, se você precisar implementar um período personalizado que modifique o tamanho e a cor do texto, estenda RelativeSizeSpan. Por herança, RelativeSizeSpan amplia CharacterStyle e implementa as duas interfaces Update. Como essa classe já fornece callbacks para updateDrawState e updateMeasureState, você pode substituí-los para implementar seu comportamento personalizado. O código abaixo cria um período personalizado que estende RelativeSizeSpan e substitui o callback updateDrawState para definir a cor do 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);
    }
}

Este exemplo ilustra como criar um período personalizado. Você pode conseguir o mesmo efeito aplicando RelativeSizeSpan e ForegroundColorSpan ao texto.

Uso do período de teste

A interface Spanned permite definir e recuperar períodos do texto. Ao testar, implemente um teste Android JUnit para verificar se os períodos corretos foram adicionados nos locais corretos. O app de exemplo de Estilo de texto contém um período que aplica a marcação a marcadores anexando BulletPointSpan ao texto. O exemplo de código abaixo mostra como testar se os marcadores aparecem conforme o esperado:

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 mais exemplos de teste, consulte MarkdownBuilderTest (link em inglês) no GitHub.

Testar períodos personalizados

Ao testar períodos, verifique se o TextPaint contém as modificações esperadas e se os elementos corretos aparecem no Canvas. Por exemplo, considere a implementação de um período personalizado que inclui marcadores em um texto. O marcador tem tamanho e cor específicos, e há uma lacuna entre a margem esquerda da área do drawable e o marcador.

Você pode testar o comportamento dessa classe implementando um teste AndroidJUnit para verificar o seguinte:

  • Se você aplicar o período corretamente, um marcador do tamanho e da cor especificados vai aparecer na tela, e haverá espaço adequado entre a margem esquerda e o marcador.
  • Se você não aplicar o período, nenhum comportamento personalizado vai ser usado.

Confira a implementação desses testes no exemplo TextStyling (link em inglês) no GitHub.

Você pode testar as interações de tela simulando a tela, transmitindo o objeto simulado para o método drawLeadingMargin() e verificando se os métodos corretos são chamados com os parâmetros corretos.

Você pode encontrar mais exemplos de testes de períodos em BulletPointSpanTest (link em inglês).

Práticas recomendadas para o uso de períodos

Há várias formas eficientes de memória para definir texto em um TextView, dependendo das suas necessidades.

Anexar ou remover um período sem alterar o texto subjacente

TextView.setText() contém várias sobrecargas que processam períodos de modos diferentes. Por exemplo, você pode definir um objeto de texto Spannable com o seguinte código:

Kotlin

textView.setText(spannableObject)

Java

textView.setText(spannableObject);

Ao chamar essa sobrecarga de setText(), o TextView cria uma cópia do Spannable como um SpannedString e o mantém na memória como um CharSequence. Isso significa que o texto e os períodos ficam imutáveis. Portanto, quando você precisar atualizar o texto ou os períodos, será necessário criar um novo objeto Spannable e chamar setText() novamente, o que também acionará uma nova medição e um novo desenho do layout.

Para indicar que os períodos precisam ser mutáveis, você pode usar setText(CharSequence text, TextView.BufferType type), conforme mostrado no exemplo a seguir:

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

Nesse exemplo, o parâmetro BufferType.SPANNABLE faz com que o TextView crie um SpannableString, e o objeto CharSequence mantido pelo TextView agora tem marcação mutável e texto imutável. Para atualizar o período, extraia o texto como Spannable e atualize os períodos conforme necessário.

Quando você anexa, desanexa ou reposiciona períodos, o TextView é atualizado automaticamente para refletir a mudança no texto. Se você mudar um atributo interno de um período existente, chame invalidate() para fazer mudanças relacionadas à aparência ou requestLayout() para fazer mudanças relacionadas à métrica.

Configurar texto em um TextView várias vezes

Em alguns casos, como ao usar um RecyclerView.ViewHolder, você pode reutilizar um TextView e configurar o texto várias vezes. Por padrão, independentemente de você definir BufferType, o TextView cria uma cópia do objeto CharSequence e a mantém na memória. Isso faz com que todas as atualizações de TextView sejam intencionais. Não é possível atualizar o objeto CharSequence original para atualizar o texto. Isso significa que sempre que você definir um novo texto, o TextView vai criar um novo objeto.

Se você quiser ter mais controle sobre esse processo e evitar a criação de objetos extras, implemente seu próprio Spannable.Factory e substitua newSpannable(). Em vez de criar um novo objeto de texto, você pode transmitir e retornar o CharSequence existente como um Spannable, conforme demonstrado no exemplo a seguir:

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

É necessário usar textView.setText(spannableObject, BufferType.SPANNABLE) ao configurar o texto. Caso contrário, o CharSequence de origem será criado como uma instância de Spanned e não poderá ser transmitido para Spannable, fazendo com que newSpannable() gere uma ClassCastException.

Após substituir newSpannable(), diga ao TextView para usar o novo Factory:

Kotlin

textView.setSpannableFactory(spannableFactory)

Java

textView.setSpannableFactory(spannableFactory);

Defina o objeto Spannable.Factory uma vez, logo depois de receber uma referência para o TextView. Se você estiver usando um RecyclerView, defina o objeto Factory quando inflar suas visualizações pela primeira vez. Isso evita a criação de objetos extras quando RecyclerView vincula um novo item ao ViewHolder.

Mudar atributos de período interno

Se você precisar mudar apenas um atributo interno de um período mutável, como a cor de marcador em um período de marcador personalizado, evite a sobrecarga que chamar setText() várias vezes gera mantendo uma referência ao período quando ele for criado. Quando for necessário modificar o período, você poderá modificar a referência e chamar invalidate() ou requestLayout() no TextView, dependendo do tipo de atributo alterado.

No exemplo de código abaixo, uma implementação personalizada de marcador tem uma cor vermelha padrão que muda para cinza quando um botão é tocado:

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

Usar as funções de extensão do Android KTX

O Android KTX também contém funções de extensão que facilitam o trabalho com spans. Para saber mais, consulte a documentação do pacote androidx.core.text.