Exibição de texto Android


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 { //    … bgColor = lightGray //    } } } 

Aplique a extensão ao texto:


 //    span spannable.setSpan(CodeBlockSpan(typeface), ...) 

Mas você pode obter exatamente o mesmo resultado combinando duas extensões: pegue nosso CustomTypefaceSpan e BackgroundColorSpan anterior na estrutura do Android:


 //    spannable.setSpan(BackgroundColorSpan(lightGray), ...) //   spannable.setSpan(CustomTypefaceSpan(typeface), ...) 

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


Texto mutávelMarcação variável
String estendidanãonão
Cadeia de caracteres spannablenãosim
Construtor Spannablestringsimsim

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:


 <!-- values-en/strings.xml --> <string name="title">Best practices for text in Android</string> <!-- values-es/strings.xml --> <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" :

 <!-- values-en/strings.xml --> <string name="title">Best practices for <annotation font=”title_emphasis”>text</annotation> in Android</string> <!-- values-es/strings.xml --> <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:


 //      SpannedString,     span' val titleText = getText(R.string.title) as SpannedString //    val annotations = titleText.getSpans(0, titleText.length, Annotation::class.java) //     SpannableString //      val spannableString = SpannableString(titleText) //     for (annotation in annotations) { //     "font" if (annotation.key == "font") { val fontName = annotation.value //   ,     if (fontName == "title_emphasis") { val typeface = getFontCompat(R.font.permanent_marker) //  span    ,    spannableString.setSpan( CustomTypefaceSpan(typeface), titleText.getSpanStart(annotation), titleText.getSpanEnd(annotation), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } } } styledText.text = spannableString 


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) //    val lineBottom = layout.getLineBottom(startLine) //    val startCoor = layout.getPrimaryHorizontal(spanStartIndex).toInt() //    val endCoor = layout.getPrimaryHorizontal(spanEndIndex).toInt() //    //   drawable.setBounds(startCoor, lineTop, endCoor, lineBottom) drawable.draw(canvas) ... 

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:


 // UI thread val params: PrecomputedText.Params = textView.getTextMetricsParams() val ref = WeakReference(textView) executor.execute { // background thread val text = PrecomputedText.create("Hello", params) val textView = ref.get() textView?.post { // UI thread val textView = ref.get() textView?.text = text } } 

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(...) //    val future = PrecomputedTextCompat.getTextFuture( data.text, vh.textView.getTextMetricsParamsCompat(), myExecutor ) //  future  TextView,      onMeasure() vh.textView.setTextFuture(future) } 

RecyclerView , , , .


, getTextFuture() (, ), , , getTextFuture() , , TextView.


, TextView


TextView.setText() :


 if (type == SPANNABLE || movementMethod != null) { text = spannableFactory.newSpannable(spannable) //  } else { text = new SpannedString(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() // re-measure and re-draw // or textView.invalidate() // re-draw 


TextView . autoLink . autoLink=”web” TextView URL URLSpan . , SDK setText() :


 spannable = new SpannableString(string); Matcher m = pattern.matcher(text); while (...) { //      String utl = … URLSpan span = new URLSpan(url); spannable.setSpan(span, ...); } 

UI , autoLink=”web” RecyclerView. . LinkifyCompat :


 //  ,       background thread val spannable = SpannableString(string) LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS) //   RecyclerView override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.textView.setText(spannable, BufferType.SPANNABLE) // ... } 

autoLink map – ( all ). . , WebView, ! SDK Linkify.gatherMapLinks() , :


 while ((address = WebView.findAddress(string)) != null) { ... } 

WebView TODO SDK:


 public static String findAddress(String addr) { // TODO: Rewrite this in Java so it is not needed to start up chromium // Could also be deprecated return getFactory().getStatics().findAddress(addr); } 

? Smart Linkify, Android P (28) , , . :


 // UI thread val text: Spannable = … val request = TextLinks.Request.Builder(text) val ref = WeakReference(textView) executor.execute { // background thread TextClassifier.generateLinks(request).apply(text) val textView = ref.get() textView?.post { // UI thread val textView = ref.get() textView?.text = text } } 

Linkify, . toolbar , Google .


Smart Linkify : , .



Magnifier


Android P (28) , – Magnifier, . .



TextView, EditText WebView, : API .


Conclusão


Android , , :


  • , TextView (, EditText)

- , Google I/O'19 “Best Practices for Using Text in Android” .



Links úteis


Artigos



Relatórios


Source: https://habr.com/ru/post/pt461787/


All Articles