A exibição de informações textuais é provavelmente a parte mais básica e importante de muitos aplicativos Android. Este artigo irá falar sobre o TextView. Todo desenvolvedor, começando com "Hello World", é constantemente confrontado com esse elemento da interface do usuário. De tempos em tempos, ao trabalhar com texto, você precisa pensar em implementar várias soluções de design ou melhorar o desempenho ao renderizar a tela.
Vou falar sobre o dispositivo TextView e algumas sutilezas de trabalhar com ele. Dicas importantes foram tiradas de relatórios de E / S anteriores do Google.
TextView sob o capô
Para renderizar texto no Android, uma pilha inteira de bibliotecas diferentes é usada sob o capô. Eles podem ser divididos em duas partes principais - código java e código nativo:
O código Java é essencialmente parte do Android SDK disponível para desenvolvedores de aplicativos e novos recursos podem ser portados para a biblioteca de suporte.
O núcleo do TextView em si é escrito em C ++, o que limita a migração para a biblioteca de suporte de novos recursos implementados lá a partir de novas versões do sistema operacional. O núcleo são as seguintes bibliotecas:
- Minikin é usado para medir o comprimento do texto, quebras de linha e palavras por sílabas.
- A UTI fornece suporte Unicode.
- O HarfBuzz encontra para caracteres Unicode os elementos gráficos correspondentes (glifos) nas fontes.
- O FreeType cria bitmaps de glifos.
- Skia é um mecanismo para desenhar gráficos 2D.
Medindo o comprimento do texto e as quebras de linha
Se você passar a linha para a biblioteca Minikin, que é usada dentro do TextView, a primeira coisa que determina é em quais glifos a linha consiste:
Como você pode ver neste exemplo, a correspondência de caracteres Unicode com glifos nem sempre será um para um: aqui, três caracteres de uma vez corresponderão a um glifo ffi. Além disso, vale a pena prestar atenção que os glifos necessários podem ser encontrados em várias fontes do sistema.
Encontrar glifos apenas nas fontes do sistema pode levar a dificuldades, especialmente se ícones ou emojis são exibidos por meio de caracteres, e é suposto combinar caracteres de fontes diferentes em uma linha. Portanto, a partir do Android Q (29) , tornou-se possível criar sua própria lista de fontes que acompanham o aplicativo. Esta lista será usada para procurar glifos:
textView.typeface = TypeFace.CustomFallbackBuilder( FontFamily.Builder( Font.Builder(assets, “lato.ttf”).build() ).build() ).addCustomFallback( FontFamily.Builder( Font.Builder(assets, “kosugi.ttf”).build() ).build() ).build()
Agora, usando CustomFallbackBuilder
ao combinar caracteres com glifos, o SDK CustomFallbackBuilder
na família de fontes especificada em ordem e, se não puder ser encontrado, a pesquisa continuará nas fontes do sistema (e através do método setSystemFallback()
, você pode especificar a família de fontes preferida do sistema). CustomFallbackBuilder
tem um limite no número de famílias de fontes - você pode adicionar não mais que 64 fontes.
A biblioteca Minikin divide seqüências de caracteres em palavras e mede palavras individuais. Para acelerar o trabalho, começando com Lollipop (21) , é usado um cache de palavras LRU do sistema. Esse cache fornece um enorme ganho de desempenho: uma chamada para Paint.measureText()
para uma palavra em cache levará em média 3% do tempo que calcula seu tamanho pela primeira vez.
Se o texto não corresponder à largura especificada, o Minikin organizará quebras de linha e palavras no texto. Começando com Marshmallow (23), você pode controlar seu comportamento especificando os atributos especiais breakStrategy
e breakStrategy
para o TextView.
Com o valor breakStrategy=simple
biblioteca simplesmente organiza hífens sequencialmente, passando pelo texto: assim que a linha deixa de se ajustar, a hifenização é colocada antes da última palavra.
No valor balanced
biblioteca tentará fazer quebras de linha para que as linhas estejam alinhadas em largura.
high_quality
tem quase o mesmo comportamento que o balanced
, com exceção de algumas diferenças (uma delas: na penúltima linha, a hifenização pode ser não apenas palavras separadas, mas também palavras por sílabas).
O atributo hyphenationFrequency
permite controlar a estratégia de quebra de linha por sílabas. Um valor none
não fará hifenização automática, normal
fará uma pequena frequência de hifenização e full
, consequentemente, usará o número máximo de palavras.
Desempenho da renderização de texto dependendo dos sinalizadores selecionados (medidos no Android P (28) ):
Dado um desempenho bastante forte, os desenvolvedores do Google, começando com a versão Q (29) e AppCompat 1.1.0 , decidiram desativar a hifenização por padrão. Se a quebra de linha é importante no aplicativo, agora você precisa habilitá-lo explicitamente.
Ao usar a quebra de linha, é necessário levar em consideração que o idioma atualmente selecionado no sistema operacional afetará a operação da biblioteca. Dependendo do idioma, o sistema selecionará dicionários especiais com regras de transferência.
Estilos de texto
Existem várias maneiras de estilizar o texto no Android:
- Um estilo único que se aplica a todo o elemento TextView.
- Estilo múltiplo (estilo múltiplo) - vários estilos de uma só vez, que podem ser aplicados ao texto, no nível do parágrafo ou dos caracteres individuais. Existem várias maneiras de fazer isso:
- desenho de texto em tela
- tags html
- elementos de marcação especiais - vãos
Um único estilo implica o uso de estilos XML ou atributos XML na marcação TextView. Nesse caso, o sistema aplicará os valores dos recursos na seguinte ordem: Aparência de texto, tema (Tema), estilo padrão (estilo padrão), estilo do aplicativo e a prioridade mais alta serão os valores dos atributos Exibir.
Usar recursos é uma solução bastante simples, mas, infelizmente, não permite aplicar estilo a partes do texto.
As tags HTML são outra solução simples que fornece recursos como tornar as palavras individuais em negrito, itálico ou até destacar listas com pontos no texto. Tudo o que o desenvolvedor precisa é fazer uma chamada para o método Html.fromHtml()
, que transformará o texto marcado em texto marcado por extensões. Mas essa solução possui recursos limitados, pois reconhece apenas parte das tags html e não suporta estilos CSS.
val text = "My text <ul><li>bullet one</li><li>bullet two</li></ul>" myTextView.text = Html.fromHtml(text)
Vários métodos de estilo do TextView podem ser combinados, mas vale lembrar a prioridade de um método específico, o que afetará o resultado final:
Outra maneira - desenhar texto na tela - fornece ao desenvolvedor controle total sobre a saída de texto: por exemplo, você pode desenhar texto ao longo de uma linha curva. Mas essa solução, dependendo dos requisitos, pode ser bastante difícil de implementar e está além do escopo deste artigo.
Vãos
O TextView usa extensões para ajustar estilos. Usando extensões, você pode alterar a cor de um intervalo de caracteres, formar parte do texto como links, alterar o tamanho do texto, desenhar um ponto na frente de um parágrafo etc.
As seguintes categorias de extensões podem ser distinguidas:
- Intervalo de caracteres - aplicado no nível de caractere de uma sequência.
- Aparência afetando - não altere o tamanho do texto.
- Métrica afetando - redimensione o texto.
- Intervalo de parágrafos - aplicado no nível do parágrafo.
A estrutura do Android possui interfaces e classes abstratas com métodos chamados durante onMeasure()
e renderização do TextView, esses métodos dão a extensões de acesso a objetos de nível inferior como TextPaint
e Canvas
. Usando a extensão, a estrutura do Android verifica quais interfaces esse objeto implementa para chamar os métodos necessários.
A estrutura do Android define mais de 20 extensões, portanto, antes de criar o seu, é melhor verificar se o SDK é adequado.
Aparência versus métrica que afeta os intervalos
A primeira categoria de extensões afeta a aparência dos caracteres na sequência: cor do caractere, cor do plano de fundo, caracteres sublinhados ou riscados, etc. Essas extensões implementam a interface UpdateAppearance
e herdam da classe CharacterStyle
, que fornece acesso ao objeto TextPaint
.
A métrica que afeta o span afeta o tamanho do texto e do layout; portanto, o uso de um span requer não apenas redesenhar o TextView, mas também a chamada onMeasure()
/ onLayout()
. Essas extensões geralmente são herdadas da classe MetricAffectingSpan
, que herda do CharacterStyle
mencionado acima.
Caractere vs parágrafo que afeta os intervalos
A extensão do parágrafo afeta todo um bloco de texto: ele pode alterar o alinhamento, o recuo ou até mesmo inserir um ponto no início de um parágrafo. Tais extensões devem ser herdadas da classe ParagraphStyle
e inseridas no texto exatamente do início ao fim. Se o intervalo estiver incorreto, o intervalo não funcionará.
No Android, os parágrafos são considerados parte do texto separado por novas linhas ( \n
).
Escrevendo seus vãos
Ao escrever seus próprios períodos, você precisa decidir o que o período afetará para escolher de qual classe herdar:
- Afeta o texto no nível do caractere ->
CharacterStyle
- Afeta o texto no nível do parágrafo ->
ParagraphStyle
- Afeta a exibição de texto →
UpdateAppearance
- Afeta o tamanho do texto -
UpdateLayout
Aqui está um exemplo de extensão para alterar a fonte:
class CustomTypefaceSpan(private val font: Typeface?) : MetricAffectingSpan() { override fun updateMeasureState(textPaint: TextPaint) = update(textPaint) override fun updateDrawState(textPaint: TextPaint) = update(textPaint) fun update(textPaint: TextPaint) { textPaint.apply { val old = typeface val oldStyle = old?.style ?: 0 val font = Typeface.create(font, oldStyle) typeface = font
Imagine que queremos criar nosso próprio período para destacar os blocos de código, para isso editaremos o período anterior - depois de adicionar a fonte, também adicionaremos uma alteração na cor de fundo do texto:
class CodeBlockSpan(private val font: Typeface?) : MetricAffectingSpan() { … fun update(textPaint: TextPaint) { textPaint.apply {
Aplique a extensão ao texto:
Mas você pode obter exatamente o mesmo resultado combinando duas extensões: pegue nosso CustomTypefaceSpan
e BackgroundColorSpan
anterior na estrutura do Android:
Essas duas soluções terão uma diferença. O fato é que as extensões auto-escritas não podem implementar a interface Parcelable
, diferente das do sistema.
Ao transmitir uma linha estilizada através do Intent ou da área de transferência, no caso de um intervalo de marcação auto-escrita, não será salva. Ao usar extensões da estrutura, a marcação permanecerá.
Usando extensões no texto
Existem duas interfaces para texto estilizado na estrutura: Spannable
e Spannable
(com marcação inalterada e mutável, respectivamente) e três implementações: SpannedString
(texto inalterado), SpannableString
(texto inalterado) e SpannableStringBuilder
(texto mutável).
SpannableStringBuilder
, por exemplo, é usado dentro de um EditText
que precisa alterar o texto.
Você pode adicionar um novo período a uma linha usando o método:
setSpan(Object what, int start, int end, int flags)
A extensão é passada pelo primeiro parâmetro e, em seguida, o intervalo de índices no texto é indicado. E o último parâmetro pode ser controlado, qual será o comportamento do período ao inserir um novo texto: se o período se espalhará para o texto inserido no ponto inicial ou final (se você inserir um novo texto no meio, o período será aplicado automaticamente, independentemente dos valores do sinalizador) .
As classes listadas acima diferem não apenas semanticamente, mas também na forma como são organizadas internamente: SpannedString
e SpannableString
usam matrizes para armazenar extensões, e SpannableStringBuilder
usa uma árvore de intervalo .
Se você realizar testes para a velocidade de renderização do texto, dependendo do número de extensões, obterá os seguintes resultados: ao usar até 250 extensões em uma linha, SpannableString
e SpannableStringBuilder
funcionam aproximadamente na mesma velocidade, mas se os elementos de marcação se tornarem mais de 250, SpannableString
será iniciado. perder. Portanto, se a tarefa é aplicar um estilo a algum texto, ao escolher uma classe, deve-se orientar por requisitos semânticos: se a linha e os estilos serão mutáveis. Mas se a marcação exigir mais de 250 extensões, você deve sempre dar SpannableStringBuilder
ao SpannableStringBuilder
.
Verifique a extensão no texto
A tarefa surge periodicamente para verificar se uma linha estendida possui uma extensão específica. E no Stackoverflow, você pode encontrar este código:
fun <T> hasSpan(spanned: Spanned, clazz: Class<T>): Boolean { val spans: Array<out T> = spanned.getSpans(0, spanned.length, clazz) return spans.isNotEmpty() }
Essa solução funcionará, mas é ineficiente: você precisa passar por todas as extensões, verificar se cada uma delas pertence ao tipo passado, coletar o resultado em uma matriz e, no final, apenas verificar se a matriz não está vazia.
Uma solução mais eficaz seria usar o método nextSpanTransition()
:
fun <T> hasSpan(spanned: Spanned, clazz: Class<T>): Boolean { val limit = spanned.length return spanned.nextSpanTransition(0, limit, clazz) < limit }
Marcação de texto em vários recursos de idioma
Essa tarefa pode surgir quando você deseja destacar uma palavra específica com a ajuda da marcação em vários recursos de sequência. Por exemplo, precisamos destacar a palavra "texto" na versão em inglês e "texto" em espanhol:
<string name="title">Best practices for text in Android</string> <string name=”title”>Texto en Android: mejores prácticas</string>
Se você precisar de algo simples, por exemplo, para destacar a palavra em negrito, poderá usar as tags html habituais ( <b>
). Na interface do usuário, você só precisa definir o recurso de cadeia de caracteres no TextView:
textView.setText(R.string.title)
Mas se você precisar de algo mais complexo, por exemplo, alterar a fonte, o html não poderá mais ser usado. A solução é usar a <annotation>
especial. Essa tag permite definir qualquer par de valores-chave em um arquivo xml. Quando extraímos uma string de recursos, essas tags são automaticamente convertidas em extensões de Annotation
, organizadas no texto com as chaves e valores correspondentes. Depois disso, você pode analisar a lista de anotações no texto e aplicar os intervalos necessários.
Suponha que precisamos alterar a fonte usando CustomTypefaceSpan
.
Adicione uma tag e defina uma chave "font" para ela e um valor - o tipo de fonte que queremos usar é "title_emphasis" :
<string name="title">Best practices for <annotation font=”title_emphasis”>text</annotation> in Android</string> <string name=”title”><annotation font=”title_emphasis”>Texto</annotation> en Android: mejores prácticas</string>
Puxe a corda dos recursos, encontre as anotações com a tecla "font" e organize os intervalos:
Foi mencionado acima que extensões de fora da estrutura do Android não podem implementar o Parcelable
e são transmitidas via Intent. Mas isso não se aplica a anotações que implementam Parcelable
. Assim, você pode passar a sequência anotada pelo Intent e analisar exatamente da mesma maneira, organizando suas extensões.
Como o texto é colocado em um TextView
O TextView pode exibir não apenas texto, mas também imagens. Você também pode definir vários recuos na frente do texto. Sob o capô, isso funciona para que o TextView crie uma classe filho, Layout, responsável diretamente pela exibição do texto. Esta é uma classe abstrata que possui três implementações; geralmente você não precisa trabalhar diretamente com elas, a menos que escreva seu próprio controle:
- BoringLayout é usado para textos simples, não suporta quebras de linha, RTL e outras coisas, mas é o mais leve. O TextView o utiliza se o texto atender a todas as restrições.
- StaticLayout é usado no TextView para outros casos.
- DynamicLayout é usado para texto mutável em um EditText.
O layout possui muitos métodos que permitem conhecer os vários parâmetros do texto exibido: as coordenadas das linhas, linha de base, as coordenadas do início e do final do texto na linha, etc. (mais detalhes podem ser encontrados na documentação )
Tais métodos podem ser muito úteis. Por exemplo, alguns desenvolvedores enfrentam a tarefa de extrair parte do texto em retângulos arredondados e tentam encontrar sua solução através de extensões que não são aplicáveis na solução desse problema.
Mas os métodos da classe Layout podem ser úteis. Aqui está uma solução de amostra:
Usando anotações, selecionamos as palavras que devem ser circuladas em retângulos.
Em seguida, crie 4 recursos desenháveis para todos os casos de quebra de texto, que devem ser colocados em retângulos:
A seguir, encontramos as anotações necessárias no texto, conforme descrito acima. Agora, temos os índices do início e do fim dessa anotação. Através dos métodos Layout, você pode descobrir o número da linha na qual o texto anotado começa e no qual termina:
val startLine = layout.getLineForOffset(spanStartIndex) val endLine = layout.getLineForOffset(spanEndIndex)
Em seguida, você deve desenhar um ou mais retângulos. Considere o caso simples, quando a parte anotada do texto apareceu em uma linha, então precisamos de apenas um retângulo com quatro cantos arredondados. Defina suas coordenadas e desenhe:
... if (startLine == endLine) { val lineTop = layout.getLineTop(startLine)
Como você pode ver neste exemplo, o Layout armazena muitas informações úteis no texto exibido, o que pode ajudar na implementação de várias tarefas não padrão.
Desempenho do TextView
O TextView, como qualquer View, passa por três fases quando exibido: onMeasure()
, onLayout()
e onDraw()
. Ao mesmo tempo, onMeasure()
leva mais tempo, diferentemente dos outros dois métodos: nesse momento, a classe Layout é recriada e o tamanho do texto é calculado. Portanto, alterar o tamanho do texto (por exemplo, alterar a fonte) exige muito trabalho. Alterar a cor do texto será mais leve, pois requer apenas chamar onDraw()
. Como mencionado acima, o sistema possui um cache global de palavras com tamanhos calculados. Se a palavra já estiver no cache, chamar onMeasure()
pois levará de 11 a 16% do tempo necessário para um cálculo completo.
Aceleração de texto
Em 2015, os desenvolvedores do Instagram aceleraram a exibição de comentários nas fotos usando o cache global. A idéia era praticamente desenhar o texto antes de mostrá-lo na tela, “aquecendo” o cache do sistema. Quando chegou a hora de mostrar o texto, o usuário o viu muito mais rápido, pois o texto já estava medido e estava no cache.
A partir do Android P (28) , os desenvolvedores do Google adicionaram à API a capacidade de realizar a fase de medir o tamanho do texto com antecedência no thread em segundo plano - PrecomputedText
(e o backport da API que começa com o Android I (14) - PrecomputedTextCompat
). Usando a nova API, 90% do trabalho será realizado no encadeamento em segundo plano.
Um exemplo:
Mostrar texto grande
Se você precisar mostrar texto grande, não transfira imediatamente para um TextView. Caso contrário, o aplicativo pode parar de funcionar sem problemas ou congelar completamente, pois fará muito trabalho no thread principal para mostrar um texto enorme que o usuário pode nem rolar até o final. A solução é dividir o texto em partes (por exemplo, parágrafos) e exibir as partes individuais no RecyclerView. Para uma aceleração ainda maior, você pode pré-calcular o tamanho dos blocos de texto usando o PrecomputedText.
Para facilitar a incorporação do PrecomputedText no RecyclerView, os desenvolvedores do Google criaram métodos especiais para PrecomputedTextCompat.getTextFuture()
e AppCompatTextView.setTextFuture()
:
fun onBindViewHolder(vh: ViewHolder, position: Int) { val data = getData(position) vh.textView.setTextSize(...) vh.textView.setFontVariationSettings(...)
RecyclerView , , , .
, getTextFuture()
(, ), , , getTextFuture()
, , TextView.
, TextView
TextView.setText()
:
if (type == SPANNABLE || movementMethod != null) { text = spannableFactory.newSpannable(spannable)
span' TextView, setText()
, .
, . TextView , -, . , . , , TextView spannableFactory
:
class MySpannableFactory : Spannable.Factory() { override fun newSpannable(source: CharSequence): Spannable { return source as? Spannable ?: super.newSpannable(source) } } textView.spannableFactory = MySpannableFactory()
textView.setText(spannable, BufferType.SPANNABLE)
, .
Google span' RecyclerView, .
TextView, span, setText()
. TextView span. TextView spannable- span', :
val spannable = textView.getText() as Spannable val span = CustomTypefaceSpan(span) spannable.setSpan(span, ...)
span, TextView, TextView . , invalidate()
, – requestLayout()
:
val spannable = textView.getText() as Spannable val span = CustomTypefaceSpan(span) spannable.setSpan(span, ...) span.setTypeface(anotherTypeface) textView.requestLayout()
autoLink
TextView . autoLink
. autoLink=”web”
TextView URL URLSpan
. , SDK setText()
:
spannable = new SpannableString(string); Matcher m = pattern.matcher(text); while (...) {
UI , autoLink=”web”
RecyclerView. . LinkifyCompat
:
autoLink
map
– ( all
). . , WebView, ! SDK Linkify.gatherMapLinks()
, :
while ((address = WebView.findAddress(string)) != null) { ... }
WebView TODO SDK:
public static String findAddress(String addr) {
? Smart Linkify, Android P (28) , , . :
Linkify, . toolbar , Google .
Smart Linkify : , .
Magnifier
Android P (28) , – Magnifier, . .
TextView, EditText WebView, : API .
Conclusão
Android , , :
- , Google I/O'19 “Best Practices for Using Text in Android” .
Links úteis
Artigos
Relatórios