Pantalla de texto de Android


Mostrar información textual es probablemente la parte más básica e importante de muchas aplicaciones de Android. Este artículo hablará sobre TextView. Todos los desarrolladores, comenzando con "Hello World", se enfrentan constantemente con este elemento de interfaz de usuario. De vez en cuando, al trabajar con texto, debe pensar en implementar varias soluciones de diseño o mejorar el rendimiento al renderizar la pantalla.


Hablaré sobre el dispositivo TextView y algunas sutilezas de trabajar con él. Se tomaron consejos clave de informes de E / S de Google anteriores.


TextView debajo del capó


Para renderizar texto en Android, se utiliza una pila completa de bibliotecas diferentes debajo del capó. Se pueden dividir en dos partes principales: código java y código nativo:



El código Java es esencialmente parte del SDK de Android disponible para los desarrolladores de aplicaciones, y las nuevas características de este se pueden portar a la biblioteca de soporte.


El núcleo de TextView en sí está escrito en C ++, lo que limita la transferencia a la biblioteca de soporte de nuevas características implementadas allí desde nuevas versiones del sistema operativo. El núcleo son las siguientes bibliotecas:


  • Minikin se usa para medir la longitud del texto, los saltos de línea y las palabras por sílabas.
  • ICU proporciona soporte Unicode.
  • HarfBuzz encuentra para los caracteres Unicode los elementos gráficos correspondientes (glifos) en las fuentes.
  • FreeType crea mapas de bits de glifos.
  • Skia es un motor para dibujar gráficos 2D.

Medición de longitud de texto y saltos de línea


Si pasa la línea a la biblioteca Minikin, que se usa dentro de TextView, lo primero que determina es en qué glifos está compuesta la línea:



Como puede ver en este ejemplo, la coincidencia de caracteres Unicode con glifos no siempre será uno a uno: aquí 3 caracteres a la vez corresponderán a un glifo ffi. Además, vale la pena prestar atención a que los glifos necesarios se pueden encontrar en varias fuentes del sistema.


Encontrar glifos solo en las fuentes del sistema puede generar dificultades, especialmente si los iconos o emojis se muestran a través de los caracteres, y se supone que combina caracteres de diferentes fuentes en una línea. Por lo tanto, comenzando con Android Q (29) , fue posible crear su propia lista de fuentes que vienen con la aplicación. Esta lista se usará para buscar 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() 

Ahora, con CustomFallbackBuilder al hacer coincidir caracteres con glifos, el SDK CustomFallbackBuilder sobre la familia de fuentes especificada en orden, y si no se puede encontrar, la búsqueda continuará en las fuentes del sistema (y a través del método setSystemFallback() puede especificar la familia de fuentes del sistema preferida). CustomFallbackBuilder tiene un límite en el número de familias de fuentes: no puede agregar más de 64 fuentes.


La biblioteca Minikin divide cadenas en palabras y mide palabras individuales. Para acelerar el trabajo, comenzando con Lollipop (21) , se utiliza un sistema de caché de palabras LRU . Tal caché proporciona una gran ganancia de rendimiento: una llamada a Paint.measureText() para una palabra en caché tomará un promedio del 3% del tiempo que calcula por primera vez su tamaño.



Si el texto no se ajusta al ancho especificado, Minikin organiza saltos de línea y palabras en el texto. Comenzando con Marshmallow (23), puede controlar su comportamiento especificando los atributos especiales breakStrategy e HyphenationFrequency para TextView.


Con el valor breakStrategy=simple biblioteca simplemente organizará guiones secuencialmente, pasando por el texto: tan pronto como la línea deje de encajar, la separación se colocará antes de la última palabra.



En el valor balanced biblioteca intentará hacer saltos de línea para que las líneas estén alineadas en ancho.



high_quality tiene casi el mismo comportamiento que el balanced , con la excepción de algunas diferencias (una de ellas: en la penúltima línea, la separación silábica puede ser no solo palabras separadas, sino también palabras por sílabas).


El atributo hyphenationFrequency permite controlar la estrategia para el ajuste de palabras por sílabas. Un valor de none no hará la separación silábica automática, normal hará una pequeña frecuencia de separación silábica y full , en consecuencia, utilizará el número máximo de palabras.



Rendimiento de representación de texto según los indicadores seleccionados (medidos en Android P (28) ):



Dado un éxito bastante fuerte en el rendimiento, los desarrolladores de Google, comenzando con la versión Q (29) y AppCompat 1.1.0 , decidieron desactivar la separación silábica por defecto. Si el ajuste de palabras es importante en la aplicación, ahora debe habilitarlo explícitamente.


Cuando se utiliza el ajuste de palabras, se debe tener en cuenta que el idioma seleccionado actualmente en el sistema operativo afectará el funcionamiento de la biblioteca. Dependiendo del idioma, el sistema seleccionará diccionarios especiales con reglas de transferencia.


Estilos de texto


Hay varias formas de diseñar texto en Android:


  • Un estilo único que se aplica a todo el elemento TextView.
  • Estilo múltiple (estilo múltiple) : varios estilos a la vez, que se pueden aplicar al texto, a nivel de párrafo o caracteres individuales. Hay varias formas de hacer esto:
    • dibujar texto sobre lienzo
    • etiquetas html
    • elementos de marcado especiales - tramos

Un estilo único implica el uso de estilos XML o atributos XML en el marcado TextView. En este caso, el sistema aplicará los valores de los recursos en el siguiente orden: Apariencia de texto, tema (Tema), estilo predeterminado (Estilo predeterminado), estilo de la aplicación y la prioridad más alta son los valores de los atributos de Vista.


El uso de recursos es una solución bastante simple, pero, desafortunadamente, no le permite aplicar estilo a partes del texto.


Las etiquetas HTML son otra solución simple que proporciona características tales como poner palabras en negrita, cursiva o incluso resaltar listas con puntos en el texto. Todo lo que el desarrollador necesita es hacer una llamada al método Html.fromHtml() , que convertirá el texto etiquetado en texto marcado por tramos. Pero esta solución tiene capacidades limitadas, ya que reconoce solo una parte de las etiquetas html y no admite estilos CSS.


 val text = "My text <ul><li>bullet one</li><li>bullet two</li></ul>" myTextView.text = Html.fromHtml(text) 

Se pueden combinar varios métodos de diseño de TextView, pero vale la pena recordar la prioridad de un método en particular, que afectará el resultado final:



Otra forma, dibujar texto en el lienzo, le da al desarrollador control total sobre la salida de texto: por ejemplo, puede dibujar texto a lo largo de una línea curva. Pero tal solución, dependiendo de los requisitos, puede ser bastante difícil de implementar y está más allá del alcance de este artículo.


Se extiende


TextView utiliza tramos para ajustar estilos. Usando tramos, puede cambiar el color de un rango de caracteres, hacer parte del texto como enlaces, cambiar el tamaño del texto, dibujar un punto delante de un párrafo, etc.


Se pueden distinguir las siguientes categorías de tramos:


  • Tramos de caracteres : aplicados al nivel de caracteres de una cadena.
    • Apariencia que afecta : no cambie el tamaño del texto.
    • Afectación métrica : cambia el tamaño del texto.
  • Tramo de párrafo : aplicado a nivel de párrafo.


El marco de Android tiene interfaces y clases abstractas con métodos que se onMeasure() durante onMeasure() y la representación de TextView, estos métodos brindan acceso a objetos de nivel inferior como TextPaint y Canvas . Usando el span, el marco de Android verifica qué interfaces implementa este objeto para invocar los métodos necesarios.



El marco de Android define aproximadamente más de 20 tramos, por lo que antes de hacer el suyo, es mejor verificar si el SDK es adecuado.


Apariencia vs métrica que afecta a los tramos


La primera categoría de tramos afecta el aspecto de los caracteres de la cadena: color de caracteres, color de fondo, caracteres subrayados o tachados, etc. Estos tramos implementan la interfaz UpdateAppearance y heredan de la clase CharacterStyle , que proporciona acceso al objeto TextPaint .



El intervalo que afecta la métrica afecta el tamaño del texto y el diseño, por lo tanto, el uso de dicho intervalo requiere no solo volver a dibujar TextView, sino también la llamada onMeasure() / onLayout() . Estos tramos generalmente se heredan de la clase MetricAffectingSpan , que hereda del CharacterStyle mencionado anteriormente.



Carácter vs párrafo que afecta a los tramos


La extensión del párrafo afecta a un bloque completo de texto: puede cambiar la alineación, la sangría o incluso insertar un punto al comienzo de un párrafo. Dichos tramos deben heredarse de la clase ParagraphStyle e insertarse en el texto exactamente desde el principio del párrafo hasta su final. Si el rango es incorrecto, el intervalo no funcionará.



En Android, los párrafos se consideran la parte del texto separada por nuevas líneas ( \n ).



Escribiendo tus tramos


Al escribir sus propios tramos, debe decidir qué afectará el lapso para elegir de qué clase heredar:


  • Afecta el texto a nivel de caracteres -> CharacterStyle
  • Afecta el texto a nivel de párrafo -> ParagraphStyle
  • Afecta la vista de texto → UpdateAppearance
  • Afecta el tamaño del texto - UpdateLayout

Aquí hay un ejemplo de un lapso para cambiar la fuente:


 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 crear nuestro propio espacio para resaltar bloques de código, para esto editaremos nuestro espacio anterior; después de agregar la fuente, también agregaremos un cambio en el color de fondo del texto:


 class CodeBlockSpan(private val font: Typeface?) : MetricAffectingSpan() { … fun update(textPaint: TextPaint) { textPaint.apply { //    … bgColor = lightGray //    } } } 

Aplicar span al texto:


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

Pero puede obtener exactamente el mismo resultado combinando dos tramos: tome nuestro CustomTypefaceSpan y BackgroundColorSpan anteriores del marco de Android:


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

Estas dos soluciones tendrán una diferencia. El hecho es que los tramos Parcelable no pueden implementar la interfaz Parcelable , a diferencia de los sistemas.


Al transmitir una línea estilizada a través de Intent o el portapapeles en caso de un lapso de marcado autoescrito, no se guardará. Al usar tramos desde el marco, el marcado permanecerá.



Usar tramos en el texto


Hay dos interfaces para texto estilizado en el marco: Spanned y Spannable (con marcado sin cambios y mutable, respectivamente) y tres implementaciones: SpannedString (texto sin cambios), SpannableString (texto sin cambios) y SpannableStringBuilder (texto mutable).


Texto mutableMarcado variable
Cadena extendidanono
Cuerda spannablenosi
Constructor de cadenas de spannablessisi

SpannableStringBuilder , por ejemplo, se usa dentro de un EditText que necesita cambiar el texto.


Puede agregar un nuevo tramo a una línea utilizando el método:


setSpan(Object what, int start, int end, int flags)


El intervalo se pasa a través del primer parámetro, luego se indica el rango de índices en el texto. Y se puede controlar el último parámetro, cuál será el comportamiento del intervalo al insertar texto nuevo: si el intervalo se extenderá al texto insertado en el punto inicial o final (si inserta texto nuevo en el medio, el intervalo se aplicará automáticamente, independientemente de los valores del indicador) .


Las clases enumeradas anteriormente difieren no solo semánticamente, sino también en cómo están organizadas internamente: SpannedString y SpannableString usan matrices para almacenar tramos, y SpannableStringBuilder usa un árbol de intervalos .


Si realiza pruebas para la velocidad de representación del texto en función del número de tramos, obtendrá los siguientes resultados: al usar hasta ~ 250 tramos seguidos, SpannableString y SpannableStringBuilder funcionan aproximadamente a la misma velocidad, pero si los elementos de marcado llegan a más de 250, entonces comienza SpannableString a perder Por lo tanto, si la tarea es aplicar un estilo a algún texto, al elegir una clase, uno debe guiarse por los requisitos semánticos: si la línea y los estilos serán mutables. Pero si el marcado requiere más de 250 tramos, siempre debe dar SpannableStringBuilder a SpannableStringBuilder .


Verifique el espacio en el texto


La tarea surge periódicamente para verificar si una línea extendida tiene un alcance específico. Y en Stackoverflow puedes 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() } 

Tal solución funcionará, pero es ineficiente: debe pasar por todos los tramos, verificar si cada uno pertenece al tipo pasado, recopilar el resultado en una matriz y, al final, simplemente verificar que la matriz no esté vacía.


Una solución más efectiva sería usar el método nextSpanTransition() :


 fun <T> hasSpan(spanned: Spanned, clazz: Class<T>): Boolean { val limit = spanned.length return spanned.nextSpanTransition(0, limit, clazz) < limit } 

Marcado de texto en varios recursos lingüísticos.


Tal tarea puede surgir cuando se requiere resaltar una palabra específica usando marcado en varios recursos de cadena. Por ejemplo, debemos resaltar la palabra "texto" en la versión en inglés y "texto" en español:


 <!-- 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> 

Si necesita algo simple, por ejemplo, para resaltar la palabra en negrita, puede usar las etiquetas html habituales ( <b> ). En la interfaz de usuario, solo necesita establecer el recurso de cadena en TextView:


 textView.setText(R.string.title) 

Pero si necesita algo más complejo, por ejemplo, cambiar la fuente, html ya no se puede usar. La solución es usar la <annotation> especial <annotation> . Esta etiqueta le permite definir cualquier par clave-valor en un archivo xml. Cuando extraemos una cadena de recursos, estas etiquetas se convierten automáticamente en Annotation de Annotation , organizados en el texto con las claves y valores correspondientes. Después de eso, puede analizar la lista de anotaciones en el texto y aplicar los intervalos necesarios.


Supongamos que necesitamos cambiar la fuente usando CustomTypefaceSpan .


Agregue una etiqueta y defina una clave de "fuente" y un valor; el tipo de fuente que queremos usar es "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> 

Extraiga la cadena de los recursos, encuentre las anotaciones con la tecla "fuente" y organice los tramos:


 //      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 


Se mencionó anteriormente que los tramos desde fuera del marco de Android no pueden implementar Parcelable y se transmiten a través de Intent. Pero esto no se aplica a las anotaciones que implementan Parcelable . Por lo tanto, puede pasar la cadena anotada a través de Intent y analizar exactamente de la misma manera organizando sus tramos.


Cómo se coloca el texto en un TextView


TextView puede mostrar no solo texto, sino también imágenes. También puede establecer varias sangrías delante del texto. Debajo del capó, esto funciona para que TextView cree una clase secundaria, Layout, que es responsable directamente de mostrar el texto. Esta es una clase abstracta que tiene tres implementaciones; por lo general, no tiene que trabajar directamente con ellas a menos que escriba su propio control:


  • BoringLayout se usa para textos simples, no admite saltos de línea, RTL y otras cosas, pero es el más liviano. TextView lo usa si el texto cumple con todas las restricciones.
  • StaticLayout se usa en TextView para otros casos.
  • DynamicLayout se usa para texto mutable en un EditText.

El diseño tiene muchos métodos que le permiten conocer los diversos parámetros del texto que se muestra: las coordenadas de las líneas, la línea base, las coordenadas del principio y el final del texto en la línea, etc. (se pueden encontrar más detalles en la documentación )


Tales métodos pueden ser muy útiles. Por ejemplo, algunos desarrolladores se enfrentan a la tarea de extraer parte del texto en rectángulos redondeados, y están tratando de encontrar su solución a través de tramos que no son aplicables para resolver este problema.



Pero los métodos de la clase Layout pueden venir al rescate. Aquí hay una solución de muestra:


Usando anotaciones, seleccionamos las palabras que deben estar encerradas en rectángulos.


Luego, cree 4 recursos dibujables para todos los casos de ajuste de texto, que deben encerrarse en rectángulos:



A continuación, encontramos las anotaciones que necesitamos en el texto, como se describió anteriormente. Ahora tenemos los índices del principio y el final de dicha anotación. A través de los métodos de diseño, puede averiguar el número de la línea en la que comienza el texto anotado y en el que termina:


 val startLine = layout.getLineForOffset(spanStartIndex) val endLine = layout.getLineForOffset(spanEndIndex) 

Luego, debes dibujar uno o más rectángulos. Considere el caso simple cuando la parte anotada del texto apareció en una línea, entonces solo necesitamos un rectángulo con cuatro esquinas redondeadas. Defina sus coordenadas y dibuje:


 ... 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 puede ver en este ejemplo, Layout almacena mucha información útil sobre el texto que se muestra, lo que puede ayudar en la implementación de varias tareas no estándar.


TextView Performance


TextView, como cualquier vista, pasa por tres fases cuando se muestra: onMeasure() , onLayout() y onDraw() . Al mismo tiempo, onMeasure() lleva la mayor parte del tiempo, a diferencia de los otros dos métodos: en este momento, se recrea la clase Layout y se calcula el tamaño del texto. Por lo tanto, cambiar el tamaño del texto (por ejemplo, cambiar la fuente) implica mucho trabajo. Cambiar el color del texto será más liviano porque solo requiere invocar onDraw() . Como se mencionó anteriormente, el sistema tiene una memoria caché global de palabras con tamaños calculados. Si la palabra ya está en el caché, volver a llamar a onMeasure() para que onMeasure() 11 y el 16% del tiempo que habría sido necesario para un cálculo completo.


Aceleración de texto


En 2015, los desarrolladores de Instagram aceleraron la visualización de comentarios en fotos usando el caché global. La idea era dibujar virtualmente el texto antes de mostrarlo en la pantalla, "calentando" la memoria caché del sistema. Cuando llegó el momento de mostrar el texto, el usuario lo vio mucho más rápido, ya que el texto ya estaba medido y estaba en el caché.


A partir de Android P (28) , los desarrolladores de Google han agregado a la API la capacidad de realizar la fase de medición del tamaño del texto por adelantado en el hilo de fondo - PrecomputedText (y el backport para la API que comienza con Android I (14) - PrecomputedTextCompat ). Usando la nueva API, el 90% del trabajo se realizará en el hilo de fondo.


Un ejemplo:


 // 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


Si necesita mostrar texto grande, no lo transfiera inmediatamente a TextView. De lo contrario, la aplicación puede dejar de funcionar sin problemas o congelarse por completo, ya que hará mucho trabajo en el hilo principal para mostrar un texto enorme que el usuario ni siquiera puede desplazarse hasta el final. La solución es dividir el texto en partes (por ejemplo, párrafos) y mostrar las partes individuales en RecyclerView. Para una aceleración aún mayor, puede calcular previamente el tamaño de los bloques de texto usando PrecomputedText.


Para facilitar la incorporación de PrecomputedText en RecyclerView, los desarrolladores de Google PrecomputedTextCompat.getTextFuture() métodos especiales PrecomputedTextCompat.getTextFuture() y 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 .


Conclusión


Android , , :


  • , TextView (, EditText)

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



Enlaces utiles


Artículos



Informes


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


All Articles