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 você pode usar para definir o estilo do texto na em nível de caractere ou parágrafo. Ao anexar períodos a objetos de texto, é possível mudar o texto de várias maneiras, como 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 TextPaint, desenhar em uma Canvas e altere 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 o 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 o Spanned interface gráfica do usuário. 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 em si é somente leitura, use SpannableString.
  • Se você precisar modificar o texto após a criação e anexar períodos a o texto, use SpannableStringBuilder.
  • Se você precisar anexar um grande número de períodos a um objeto de texto, independentemente de o texto em si 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á são aplicadas ao texto, e os parâmetros start e end indicam a porção do texto ao qual você está aplicando o período.

Se você inserir texto dentro dos limites de um período, ele se expandirá automaticamente para incluem o texto inserido. Ao inserir texto no período limites, ou seja, nos índices de início ou fim, as sinalizações determina se o período se expande para incluir o texto inserido. Usar as Spannable.SPAN_EXCLUSIVE_INCLUSIVE para incluir o texto inserido e usar Spannable.SPAN_EXCLUSIVE_EXCLUSIVE para excluir o texto inserido.

O exemplo a seguir mostra como anexar um ForegroundColorSpan para um 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 é definido usando Spannable.SPAN_EXCLUSIVE_INCLUSIVE, ele expande-se para incluir o texto inserido nos limites do período, como mostrado 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 adicional ao usar Spannable.SPAN_EXCLUSIVE_INCLUSIVE:

Você pode anexar vários períodos ao mesmo texto. O exemplo a seguir mostra como para 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 o texto do texto métricas.
  • Escopo do período: alguns períodos podem ser aplicados a caracteres individuais, enquanto outros precisa ser aplicada 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 aos caracteres afetam a aparência do texto, como alterar a cor do texto ou do plano de fundo e adicionar sublinhados ou tachados. Esses os períodos estendem classe CharacterStyle.

O exemplo de código abaixo mostra como aplicar uma UnderlineSpan ao sublinhado 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 uma 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 a atualize a 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 e tamanho do texto. Esses vãos estendem MetricAffectingSpan .

O exemplo de código a seguir 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 ampliado usando um RelativeSizeSpan:

Aplicar um período que afeta as métricas de texto faz com que um objeto de observação medir novamente o texto para garantir o layout e a renderização corretos, por exemplo, alterando tamanho do texto pode fazer com que as palavras apareçam em linhas diferentes. A aplicação da regra período aciona uma nova medição, um novo cálculo do layout do texto e um redesenho o 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 se estende CharacterSpan, as subclasses afetam a aparência do texto no caractere nível

Períodos que afetam parágrafos

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

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

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

O exemplo de código a seguir aplica uma 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 uma 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 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 o período afeta o texto em nível de caracteres ou parágrafos e também se isso afeta o layout ou a aparência do texto. Isso ajuda você determinar quais classes de base podem ser estendidas e quais interfaces podem ser necessárias; precisam ser implementados. 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 do texto e cor, estenda RelativeSizeSpan. Pela herança, RelativeSizeSpan estende CharacterStyle e implementa as duas interfaces Update. Como este já fornece callbacks para updateDrawState e updateMeasureState. substitua esses callbacks para implementar seu comportamento personalizado. A 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 aplicando uma RelativeSizeSpan e uma ForegroundColorSpan ao texto.

Uso do período de teste

A interface Spanned permite definir e recuperar períodos de em textos. Ao testar, implemente uma interface Android JUnit test para verificar se os períodos corretos foram adicionados nos locais corretos. O exemplo de Estilo de texto App 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 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 testes, consulte MarkdownBuilderTest (link em inglês) no GitHub.

Testar períodos personalizados

Ao testar períodos, verificar se TextPaint contém o modificações 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 desenhável e o marcador.

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

  • Se você aplicar corretamente o período, um marcador do tamanho especificado e aparece na tela, e o espaço adequado existe entre os lados margem e o marcador.
  • Se você não aplicar o período, nenhum comportamento personalizado será exibido.

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

É possível testar as interações de Canvas simulando a tela, passando o ao objeto drawLeadingMargin() e verificando se os métodos corretos são chamados com o parâmetros.

Você pode encontrar mais amostras de teste 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 uma TextView com eficiência de memória, dependendo de acordo com 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, é possível defina 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 você precisar atualize 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 novo desenho da o mesmo layout organizacional.

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

Neste exemplo, BufferType.SPANNABLE faz com que TextView crie um SpannableString, e o O objeto CharSequence mantido pelo TextView agora tem uma marcação mutável e um texto imutável. Para atualizar o período, extraia o texto como uma Spannable e, em seguida, atualizar 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. De padrão, independentemente de você definir o BufferType, o TextView cria uma cópia do objeto CharSequence e a mantém na memória. Isso faz com que TextView atualizações intencionais. Não é possível atualizar a versão original. Objeto CharSequence para atualizar o texto. Ou seja, toda vez que você definir texto, o TextView cria um novo objeto.

Se quiser ter mais controle sobre esse processo e evitar o objeto extra é possível implementar sua própria Spannable.Factory e substituir newSpannable() Em vez de criar um novo objeto de texto, é possível transmitir e retornar o objeto CharSequence 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;
    }
};

Use textView.setText(spannableObject, BufferType.SPANNABLE) quando definindo o texto. Caso contrário, a CharSequence de origem será criada como um Spanned. instância e não pode ser transmitido 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 seu TextView. Se você estiver usando um RecyclerView, defina o objeto Factory ao primeiro, infle suas visualizações. 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 o cor de marcador em um período de marcador personalizado, poderá evitar a sobrecarga de chamar setText() várias vezes, mantendo uma referência ao período à medida que ele é criado. Quando precisar modificar o período, modifique a referência e chame invalidate() ou requestLayout() na TextView, dependendo do tipo de que você alterou.

No exemplo de código a seguir, uma implementação de marcador personalizada tem um 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 tornam o trabalho com períodos mais fácil. Para saber mais, consulte a documentação do pacote androidx.core.text.