Edge-to-edge en Android: hacerlo bien

El pasado Google I / O 2019 trajo muchas innovaciones sensacionales, muchas de las cuales afectarán a la industria del desarrollo móvil en los próximos años. No fue menos interesante seguir las tendencias emergentes. Primero, las teclas de control mecánico pasaron a la historia, las pantallas de los teléfonos inteligentes se hicieron más grandes y los marcos laterales se volvieron más discretos. Los gestos reemplazaron los botones del sistema en pantalla, dejando más y más espacio para el consumo de contenido. Las aplicaciones se muestran en toda la superficie visible de la pantalla, desde el marco inferior al superior, sin restringirse a los límites condicionales de la barra de estado y el panel de navegación. Estamos al borde de una era de borde a borde.



¿Qué es de borde a borde? Entendido literalmente, esto significa que su aplicación debe mostrarse en toda la superficie visible de la pantalla, desde la parte inferior hasta el marco superior, sin restringirse a la barra de estado y los botones de navegación inferiores.


Borde a borde en el ejemplo del shell del sistema Android.

Cuando se trata de Android, una idea simple está lejos de ser siempre fácil de implementar. En este artículo, hablaremos sobre cómo maximizar el uso de todo el espacio disponible en la pantalla de cualquier dispositivo, independientemente del fabricante, la versión del sistema y la variedad de configuraciones que los fabricantes de dispositivos del Reino Medio (y no solo) adoran complacer a los usuarios. El código presentado en el artículo fue probado personalmente en más de 30 dispositivos, y en 231 dispositivos diferentes por 100 mil usuarios de nuestras aplicaciones.

El problema de crear una interfaz de borde a borde no es nuevo en sí mismo y era relevante mucho antes de I / O 2019. Seguramente, cada uno de ustedes recordará cómo googleó por primera vez algo de la categoría: "barra de estado transparente de Android" o "gradiente de barra de estado de Android" " .

Los criterios principales para que la aplicación coincida con el título de borde a borde son:

  • barra de estado transparente;
  • Barra de navegación transparente.

Lea más sobre ellos en material.io .


La aplicación Deezer no se preocupa por el cumplimiento de borde a borde

Es importante tener en cuenta que no estamos hablando de eliminarlos por completo, como en el " modo de pantalla completa ". Dejamos al usuario la oportunidad de ver información importante del sistema y utilizar la navegación familiar.

Un requisito igualmente importante para una solución es la escalabilidad y la extensibilidad. Hay varios otros:

  • Cambie correctamente la pantalla sobre el teclado sin romper el soporte para los indicadores de ajuste de tamaño de actividad;
  • Evite superponer la barra de estado y la barra de navegación en los elementos de la interfaz de usuario de la aplicación, mientras muestra el fondo correspondiente debajo de ellos;
  • Trabaja en todos los dispositivos con versiones actuales de Android y tiene un aspecto idéntico.

Poco de teoría


De manera inesperada, puede tomar mucho tiempo encontrar una solución para una tarea aparentemente simple, que no será fácil de explicar para el gerente del proyecto. Y cuando QA todavía encuentra el teléfono inteligente desafortunado, en el que su pantalla no se ve "según los cánones" ...
En nuestro proyecto, nos equivocamos varias veces. Solo un mes después, después de pasar por una larga serie de prueba y error, resolvimos el problema de una vez por todas.

En primer lugar, debe comprender cómo Android dibuja los paneles del sistema. Comenzando con Android 5.0, se proporcionó una API conveniente para trabajar con sangrías del sistema a lo largo de los bordes horizontales de la pantalla. Se llaman WindowInsets, y en la imagen a continuación están coloreados en rojo:


Además, los desarrolladores del equipo de Android han agregado escuchas que le permiten suscribirse a los cambios en estas sangrías, por ejemplo, cuando aparece el teclado. Estrictamente hablando, WindowInsets son los márgenes de su archivo de diseño desde los bordes de la pantalla. Al cambiar el tamaño de su actividad (modo de pantalla dividida, apariencia del teclado), el recuadro también cambiará. Por lo tanto, para admitir borde a borde, debemos asegurarnos de que estas sangrías no estén allí. Una pantalla con WindowInsets nulos se verá así:


Implementación


En nuestra implementación, operaremos activamente en Windows y sus banderas.
Todos los ejemplos se escribirán en Kotlin, pero puede implementarlos fácilmente en Java, utilizando utilidades en lugar de funciones de extensión.

Lo primero que debe hacer con el elemento de diseño raíz es establecer explícitamente el indicador:

android:fitsSystemWindows="true" 

Esto es necesario para garantizar que la Vista raíz se dibuje debajo de los elementos del sistema, así como para las medidas correctas de Inset al suscribirse a su cambio.
Ahora pasamos a lo más importante: ¡eliminamos los bordes de la pantalla! Sin embargo, esto debe hacerse con mucho cuidado. Y aquí está el por qué:

  1. Poniendo a cero el recuadro inferior, corremos el riesgo de perder la reacción de la ventana a la apariencia del teclado: hay muchos consejos en StackOverflow para restablecer el recuadro superior, pero los inferiores son delicadamente silenciosos. Debido a esto, la barra de navegación no resulta ser completamente transparente. Al restablecer el recuadro inferior, el indicador de ajuste de tamaño deja de funcionar.

    Solución: cada vez que cambie el recuadro, determine si la altura más baja del teclado está contenido en él y solo reinícielo de lo contrario.
  2. Cuando reinicia el recuadro, las partes visibles de la vista aparecerán debajo de la barra de estado y la barra de navegación. Según el concepto de diseño de materiales (y sentido común), no deben ubicarse elementos activos en las áreas del sistema. Es decir, en esta área no debe haber botones, campos para ingresar texto, casillas de verificación, etc.

    Solución: agregaremos un oyente al oyente, de modo que cuando cambie WindowInsets, traduzca las sangrías del sistema a la Actividad y responda a ellas internamente configurando los rellenos y márgenes correctos para la Vista.


Este comportamiento no debe permitirse (la barra de herramientas se arrastra en la barra de estado).

La función removeSystemInsets () se ve así:

 fun removeSystemInsets(view: View, listener: OnSystemInsetsChangedListener) { ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets -> val desiredBottomInset = calculateDesiredBottomInset( view, insets.systemWindowInsetTop, insets.systemWindowInsetBottom, listener ) ViewCompat.onApplyWindowInsets( view, insets.replaceSystemWindowInsets(0, 0, 0, desiredBottomInset) ) } } 

La función CalculateDesiredBottomInset () calcula el recuadro inferior con o sin el teclado, dependiendo de la configuración actual del dispositivo.

 fun calculateDesiredBottomInset( view: View, topInset: Int, bottomInset: Int, listener: OnSystemInsetsChangedListener ): Int { val hasKeyboard = isKeyboardAppeared(view, bottomInset) val desiredBottomInset = if (hasKeyboard) bottomInset else 0 listener(topInset, if (hasKeyboard) 0 else bottomInset) return desiredBottomInset } 

Use el método isKeyboardAppeared () para verificar la altura del teclado . Confiamos en la hipótesis de que el teclado no puede ocupar menos de un cuarto de la altura de la pantalla. Si lo desea, puede modificar la lógica de verificación a su gusto.

 private fun View.isKeyboardAppeared(bottomInset: Int) = bottomInset / resourdisplayMetrics.heightPixels.toDouble() > .25 

El método removeSystemInsets () usa un escucha. En realidad, estos son solo typealias para una expresión lambda. Su código completo:

 typealias OnSystemBarsSizeChangedListener = (statusBarSize: Int, navigationBarSize: Int) -> Unit 

El siguiente paso es establecer la transparencia en las barras del sistema:

 window.statusBarColor = Color.TRANSPARENT window.navigationBarColor = Color.TRANSPARENT 

Una vez compilado todo lo anterior, obtenemos el siguiente método:

 fun Activity.setWindowTransparency( listener: OnSystemInsetsChangedListener = { _, _ -> } ) { InsetUtil.removeSystemInsets(window.decorView, listener) window.navigationBarColor = Color.TRANSPARENT window.statusBarColor = Color.TRANSPARENT } 

Ahora, para habilitar el modo de borde a borde de la Actividad deseada, solo necesita llamar a la siguiente función en el método onCreate () :

 setWindowTransparency { statusBarSize, navigationBarSize -> //  } 

Por lo tanto, en menos de 30 líneas de código, hemos logrado un efecto de "borde a borde", sin violar ningún principio de UX y sin privar al usuario de los controles habituales del sistema. Tal implementación puede parecer simple y trivial para alguien, pero es eso lo que garantiza el funcionamiento confiable de su aplicación en cualquier dispositivo.
Puede lograr el efecto de "borde a borde" de aproximadamente cien formas diferentes (el número de consejos en StackOverflow es una clara confirmación de esto), pero muchos de ellos conducen a un comportamiento incorrecto en diferentes versiones de Android, o no tienen en cuenta parámetros como la necesidad de mostrar por mucho tiempo listas, o rompa el tamaño de la pantalla al mostrar el teclado.




Volar en la pomada


La solución descrita en este artículo es adecuada para todos los dispositivos actuales. Real significa dispositivos en Android Lollipop (5.0) y superior. Para ellos, la solución anterior funcionará perfectamente. Pero para las versiones anteriores de Android, necesitará su propia implementación, ya que no se sabía nada sobre WindowInsets en esos días.

La buena noticia es que en Android KitKat (4.4), la transparencia de los paneles del sistema todavía es compatible. Pero las versiones anteriores no admiten tal belleza en absoluto, ni siquiera puedes intentarlo.

Centrémonos en el cambio de Insets en Android 4.4. Esto se puede hacer en el método fitSystemWindows () . Por lo tanto, el elemento principal en su diseño debe ser un contenedor con un método anulado fitSystemWindows que contenga exactamente la misma implementación que nuestro oyente en el ejemplo para las versiones actuales de Android.

 class KitkatTransparentSystemBarsFrame @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = -1 ) : FrameLayout(context, attrs, defStyleAttr), KitkatTransparentSystemBarsContainer { override var onSystemInsetsChangedListener: OnSystemInsetsChangedListener = { _, _ -> } override fun fitSystemWindows(insets: Rect?): Boolean { insets ?: return false val desiredBottomInset = InsetUtil.calculateDesiredBottomInset( this, insets.top, insets.bottom, onSystemInsetsChangedListener ) return super.fitSystemWindows(Rect(0, 0, 0, desiredBottomInset)) } 

En dispositivos con Android 4.4, solo la transparencia parcial funciona mediante la configuración de indicadores translúcidos:

 window.addFlags( WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION ) 

Estas banderas hacen que las barras del sistema sean translúcidas, agregando un ligero gradiente que, desafortunadamente, no se puede eliminar. Sin embargo, el gradiente se puede convertir en una barra de color translúcida usando esta biblioteca: https://github.com/jgilfelt/SystemBarTint . Ella nos ha rescatado más de una vez en el pasado. Los últimos cambios se realizaron en la biblioteca hace 5 años, por lo que solo revelará su encanto a las verdaderas ciudades retrógradas.

Todo el proceso de marcado para Kitkat se verá así:

 fun Activity.setWindowTransparencyKitkat( rootView: KitkatTransparentSystemBarsContainer, listener: OnSystemBarsSizeChangedListener = { _, _ -> } ) { rootView.onSystemBarsSizeChangedListener = listener window.addFlags( WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION ) } 

Con esto en mente, escribimos un método universal que puede hacer que las barras del sistema sean transparentes (o al menos translúcidas), independientemente de qué aplicación se ejecute en un dispositivo con qué versión de Android:

 when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> setWindowTransparency(::updateMargins) Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT -> setWindowTransparencyKitkat(root_container, ::updateMargins) else -> { /*do nothing*/ } } 

Debajo del spoiler a continuación, puede ver cómo se ve la muestra presentada en el artículo en algunos de los dispositivos problemáticos:

Capturas de pantalla
Huawei Honor 8, Android 7.0



Xiaomi Redmi Note 4, Android 6.1



HTC Desire Dual Sim, Android 4.4.2



Samsung J3, Android 7.0



Meizu M3s, Android 5.1



Asus Zenfone 3 Max, Android 6.0



Umi Rome, Android 5.0



Nexus 5X, Android 8.0



Samsung Galaxy S8, Android 9.0





En conclusión, quiero decir que la solución incluso a una tarea aparentemente tan simple como establecer la transparencia para los elementos de la interfaz de usuario del sistema puede arrastrarlo a lo largo de toda la variedad de trampas y, en última instancia, no conducir al resultado deseado, sino que causará errores desagradables. Qué bueno que tienes este artículo ahora.

Puede encontrar una lista completa del programa y una muestra de trabajo en nuestro repositorio de git .

El material está inspirado en el informe de Chris Banes "Convertirse en un instalador de ventanas maestro".


Expreso mi gratitud al estudio de Surf y Evgeny Saturov por su ayuda en la preparación del material.

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


All Articles