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 no nível do caractere ou 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, redimensionar 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 no fato de o texto ser mutável, se a marcação do texto é mutável e 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 a ele, use SpannableStringBuilder.
  • Caso precise 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 à qual o período está sendo aplicado.

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 sinalização Spannable.SPAN_EXCLUSIVE_INCLUSIVE para incluir o texto inserido e use Spannable.SPAN_EXCLUSIVE_EXCLUSIVE para excluir o texto inserido.

O exemplo abaixo 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 abaixo 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 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 períodos
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 que se aplicam ao nível dos caracteres afetam a aparência do texto, como mudar a cor do texto ou do plano de fundo e adicionar sublinhados ou tachados. Esses períodos estendem a classe CharacterStyle.

O exemplo de código abaixo mostra como aplicar uma 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 com 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 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 aos caracteres afetam as 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 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. O texto fica maior usando uma 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 garantir 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 um novo desenho do texto.

Os períodos que afetam as métricas de texto estendem a 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 CharacterSpan, as subclasses afetam a aparência do texto no nível dos caracteres.

Períodos que afetam parágrafos

Um período também pode afetar o texto em parágrafos, como mudar 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 abaixo 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 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);
Uma imagem mostrando um exemplo de quoteSpan.
Figura 8. Uma QuoteSpan aplicada 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 nos caracteres ou no 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:

Situação 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 meio da herança, RelativeSizeSpan estende 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 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);
    }
}

Neste exemplo, mostramos 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 com base no 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 ver 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 especificados, 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 aparecerá na tela, e haverá o espaço adequado entre a margem esquerda e o marcador.
  • Se você não aplicar o período, nenhum comportamento personalizado será exibido.

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, passando o objeto simulado para o método drawLeadingMargin() e verificando se os métodos corretos são chamados com os parâmetros corretos.

Veja 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 maneiras eficientes de definir texto em uma 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() de novo, 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 neste exemplo:

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, recupere o texto como um 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ê alterar um atributo interno de um período existente, chame invalidate() para fazer mudanças relacionadas à aparência ou requestLayout() para fazer mudanças relacionadas às métricas.

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 criará um novo objeto.

Para 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, é possível transmitir e retornar o CharSequence 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, a CharSequence de origem será criada como uma instância de Spanned e não poderá ser transmitida para Spannable, fazendo com que newSpannable() gere uma ClassCastException.

Depois de 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 quando inflar as visualizações pela primeira vez. Isso evita a criação de objetos extras quando seu RecyclerView vincula um novo item ao seu ViewHolder.

Mudar atributos de período interno

Se você precisar mudar apenas um atributo interno de um período mutável, como a cor do marcador em um período de marcador personalizado, evite a sobrecarga de chamar setText() várias vezes, mantendo uma referência ao período à medida que ele é criado. Quando você precisar modificar o período, modifique a referência e chame 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 vermelha 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.