Períodos

Teste 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 objetos de marcação avançados que podem ser usados para definir o estilo do texto no nível do caractere ou do parágrafo. Ao anexar períodos a objetos de texto, é possível alterar o texto de várias maneiras, 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 uma 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 diferem com base em se o texto em si é mutável, se a marcação de texto é mutável e em qual estrutura de dados subjacente contém os dados do 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

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 você precisar anexar um pequeno número de períodos a um único objeto de texto e o texto for somente leitura, use SpannableString.
  • Se você precisar modificar o texto após a criação e anexar períodos ao texto, use SpannableStringBuilder.
  • Se você precisar anexar um grande número de períodos a um objeto de texto, independentemente do 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 a que você vai aplicar o período.

Se você inserir texto dentro dos limites de um período, ele se expandirá 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 se expande para incluir o texto inserido. Use a flag Spannable.SPAN_EXCLUSIVE_INCLUSIVE para incluir o texto inserido e use Spannable.SPAN_EXCLUSIVE_EXCLUSIVE para excluí-lo.

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 ForegroundColorSpan.

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

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 texto extra 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 mostrando um texto com vários períodos: "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 do 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 período
Figura 4. Categorias de períodos do Android.

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

Períodos que afetam a aparência do texto

Alguns períodos que se aplicam ao nível do caractere afetam a aparência do texto, por exemplo, a mudança do texto ou a cor do plano de fundo e a adição de sublinhados ou tachados. Esses períodos estendem a classe CharacterStyle.

O exemplo de código a seguir 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);
Imagem mostrando como sublinhar texto usando um "Sublinhado"
Figura 5. Texto sublinhado com 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 CharacterStyle definem como desenhar texto, fornecendo acesso para atualizar o TextPaint.

Períodos que afetam as métricas de texto

Outros períodos que se aplicam a nível de caracteres afetam métricas de texto, como altura da linha e tamanho do texto. Esses períodos estendem a classe MetricAffectingSpan.

O exemplo de código abaixo cria uma 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 maior usando um RelativeSizeSpan.

A aplicação de um período que afeta as métricas 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, mudar o 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 o redesenho 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 a TextPaint. Como MetricAffectingSpan estende CharacterSpan, as subclasses afetam a aparência do texto no nível do caractere.

Períodos que afetam parágrafos

Um período também pode afetar o texto no nível do parágrafo, por exemplo, alterando o alinhamento ou a margem de um bloco de texto. Períodos que afetam parágrafos inteiros implementam ParagraphStyle. Para usar esses períodos, é necessário anexá-los ao parágrafo inteiro, excluindo o caractere final da 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. Observe que, 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 vai 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);
Imagem que mostra 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 também 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. Pela herança, RelativeSizeSpan estende CharacterStyle e implementa as duas interfaces Update. Como essa classe já fornece callbacks para updateDrawState e updateMeasureState, é possível substituí-los para implementar seu comportamento personalizado. O código a seguir 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 mostra 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 extrair 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 a seguir mostra como testar se os marcadores aparecem conforme 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 ver mais exemplos de testes, 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 na 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 aparecer.

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

É possível testar as interações de Canvas 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 amostras de testes de período em BulletPointSpanTest (link em inglês).

Práticas recomendadas para o uso de períodos

Há várias maneiras eficientes de definir texto em um TextView com eficiência de memória, 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 maneira diferente. 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 são imutáveis. Portanto, quando precisar atualizar o texto ou os períodos, crie um novo objeto Spannable e chame setText() novamente, o que também aciona 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 uma SpannableString, e o objeto CharSequence mantido pelo TextView agora tem marcação mutável e texto imutável. Para atualizar o período, recupere o texto como Spannable e, em seguida, 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 torna todas as atualizações de TextView 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 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 já existente como um Spannable, conforme demonstrado neste exemplo:

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

Use textView.setText(spannableObject, BufferType.SPANNABLE) ao definir 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(), instrua o TextView a 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 ao TextView. Se você estiver usando um RecyclerView, defina o objeto Factory ao inflar as visualizações pela primeira vez. Isso evita a criação de objetos extras quando o 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 de chamar setText() várias vezes mantendo uma referência ao período durante a criação. Quando você precisar modificar o período, 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 de marcador personalizado tem uma cor padrão de vermelho 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 períodos. Para saber mais, consulte a documentação do pacote androidx.core.text.