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.](https://developer.android.com/static/develop/ui/views/text-and-emoji/images/spans-fg-color.png?authuser=5&hl=pt)
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.](https://developer.android.com/static/develop/ui/views/text-and-emoji/images/spans-fg-color-2.png?authuser=5&hl=pt)
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)"](https://developer.android.com/static/develop/ui/views/text-and-emoji/images/spans-bold-red.png?authuser=5&hl=pt)
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](https://developer.android.com/static/develop/ui/views/text-and-emoji/images/spans-span-categories.png?authuser=5&hl=pt)
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`](https://developer.android.com/static/develop/ui/views/text-and-emoji/images/spans-underlinespan.png?authuser=5&hl=pt)
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](https://developer.android.com/static/develop/ui/views/text-and-emoji/images/spans-relativesizespan.png?authuser=5&hl=pt)
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.
![](https://developer.android.com/static/develop/ui/views/text-and-emoji/images/spans-paragraphs.png?authuser=5&hl=pt)
\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.](https://developer.android.com/static/develop/ui/views/text-and-emoji/images/spans-quotespan.png?authuser=5&hl=pt)
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.