Inserciones de Android: lidiar con los miedos y prepararse para Android Q

Android Q es la décima versión de Android con API nivel 29. Una de las ideas principales de la nueva versión es el concepto de borde a borde, cuando las aplicaciones ocupan toda la pantalla, de abajo hacia arriba. Esto significa que la barra de estado y la barra de navegación deben ser transparentes. Pero, si son transparentes, entonces no hay una interfaz de usuario del sistema: se superpone a los componentes interactivos de la aplicación. Este problema se resuelve con inserciones.

Los desarrolladores móviles evitan las inserciones, les causan miedo. Pero en Android Q no será posible evitar las inserciones: tendrá que estudiarlas y aplicarlas. De hecho, las inserciones no tienen nada de complicado: muestran qué elementos de la pantalla se cruzan con la interfaz del sistema y sugieren cómo mover el elemento para que no entre en conflicto con la interfaz de usuario del sistema. Konstantin Tskhovrebov contará sobre cómo funcionan los insertos y cómo son útiles.


Konstantin Tskhovrebov (terrakok) funciona en Redmadrobot . Ha estado involucrado en Android durante 10 años y ha acumulado mucha experiencia en varios proyectos en los que no había lugar para las inserciones, siempre lograron moverse de alguna manera. Konstantin contará sobre una larga historia de evitar el problema de las inserciones, sobre estudiar y luchar contra Android. Considerará las tareas típicas de su experiencia en las que se podrían aplicar inserciones, y mostrará cómo dejar de temer al teclado, reconocer su tamaño y responder a la apariencia.

Nota. El artículo fue escrito en base a un informe de Konstantin en Saint AppsConf 2019 . El informe utilizó materiales de varios artículos sobre inserciones. Enlace a estos materiales al final.

Tareas típicas


Barra de estado de color. En muchos proyectos, el diseñador dibuja una barra de estado de color. Se puso de moda cuando llegó Android 5 junto con el nuevo Material Design.



¿Cómo pintar la barra de estado? Elemental: agrega colorPrimaryDarky el color se sustituye.

 <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="colorPrimaryDark">@color/colorAccent</item>
    ...
</style>

Para Android por encima de la quinta versión (API de 21 y superior), puede establecer parámetros especiales en el tema:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="android:statusBarColor">@color/colorAccent</item>
    ...
</style>

Barra de estado multicolor . A veces, los diseñadores en diferentes pantallas dibujan la barra de estado en diferentes colores.



Está bien, la forma más fácil de trabajar es con diferentes temas en diferentes actividades .

La forma más interesante es cambiar los colores directamente en tiempo de ejecución .

override fun onCreateView(...): View {
    requireActivity().window.statusBarColor = requireContext().getColor(R.color.colorPrimary)
    ...
}

El parámetro se cambia a través de una bandera especial. Pero lo principal es no olvidar cambiar el color cuando el usuario vuelve a salir de la pantalla.

Barra de estado transparente. Esto es mas dificil. La mayoría de las veces, la transparencia está asociada con los mapas, porque es en los mapas donde se ve mejor la transparencia. En este caso, como antes, establecemos un parámetro especial:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">    
    ...
    <item name="android:windowTranslucentStatus">true</item>
    ...
</style>

Aquí, por supuesto, hay un truco bien conocido: sangrar más alto, de lo contrario, la barra de estado se superpondrá en el ícono y será fea.


Pero en otras pantallas todo se rompe.



¿Como resolver el problema? El primer método que viene a la mente son las diferentes actividades: tenemos diferentes temas, diferentes parámetros, funcionan de manera diferente.

Trabaja con el teclado. Evitamos las inserciones no solo con la barra de estado, sino también cuando trabajamos con el teclado.



A nadie le gusta la opción de la izquierda, pero para convertirla en una opción de la derecha, hay una solución simple.

<activity
    ...
    android:windowSoftInputMode="adjustResize">
    ...
</activity>

La actividad ahora puede cambiar de tamaño cuando aparece el teclado. Funciona de manera simple y severa. Pero no olvides otro truco: envuelve todo ScrollView. De repente, el teclado ocupará toda la pantalla y habrá una pequeña franja en la parte superior.

Hay momentos más difíciles cuando queremos cambiar el diseño cuando aparece el teclado. Por ejemplo, organice los botones u oculte el logotipo.



Entrenamos tantas muletas con un teclado y una barra de estado transparente, ahora es difícil detenernos. Vaya a StackOverflow y copie el hermoso código.

boolean isOpened = false;

public void setListenerToRootView() {
    final View activityRootView = getWindow().getDecorView().findViewById(android.R.id.content);
    activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
    
            int heightDiff = activityRootView.getRootView().getHeight() - activityRootView.getHeight();      
            if (heightDiff > 100) { // 99% of the time the height diff will be due to a keyboard.       
                Toast.makeText(getApplicationContext(), "Gotcha!!! softKeyboardup", 0).show();

                if (isOpened == false) {
                   //Do two things, make the view top visible and the editText smaller
                }
                isOpened = true;
            } else if (isOpened == true) {
                Toast.makeText(getApplicationContext(), "softkeyborad Down!!!", 0).show();
                isOpened = false;
            }    
        }
    });
}

Incluso calculó la probabilidad de que aparezca un teclado. El código funciona, incluso en uno de los proyectos lo usamos, pero durante mucho tiempo.

Muchas muletas en los ejemplos están asociadas con el uso de diferentes actividades. Pero son malos no solo porque son muletas, sino también por otras razones: el problema del "arranque en frío", la asincronía. Describí los problemas con más detalle en el artículo " Licencia para conducir un automóvil, o por qué las aplicaciones deberían ser de actividad única ". Además, la documentación de Google indica que el enfoque recomendado es una aplicación de Actividad única.

¿Qué hicimos antes de Android 10?


Nosotros (en Redmadrobot) estamos desarrollando aplicaciones de bastante alta calidad, pero hemos evitado las inserciones durante mucho tiempo. ¿Cómo logramos evitarlos sin muletas grandes y en una sola actividad?

Nota. Capturas de pantalla y código tomados de mi proyecto de mascota GitFox .

Imagina la pantalla de la aplicación. Cuando desarrollamos nuestras aplicaciones, nunca pensamos que podría haber una barra de navegación transparente a continuación. ¿Hay una barra negra debajo? Y qué, los usuarios están acostumbrados.



En la parte superior, inicialmente establecemos el parámetro de que la barra de estado es negra con transparencia. ¿Cómo se ve en términos de diseño y código?



La abstracción en la figura: el bloque rojo es la actividad de la aplicación, el azul es el fragmento con el bot de navegación (con pestañas), y dentro de él se cambian los fragmentos verdes con el contenido. Se puede ver que la barra de herramientas no está debajo de la barra de estado. ¿Cómo logramos esto?

Android tiene una bandera difícil fitSystemWindow. Si lo establece en verdadero, el contenedor agregará relleno para sí mismo para que nada dentro de él caiga bajo la barra de estado. Creo que esta bandera es la muleta oficial de Google para aquellos que tienen miedo a las inserciones. Usar todo funcionará relativamente bien sin inserciones.

La bandera FitSystemWindow=”true”agrega relleno al contenedor que especificó. Pero la jerarquía es importante: si uno de los padres establece este indicador en "verdadero", entonces su distribución no se tendrá en cuenta, porque el contenedor ya ha aplicado la sangría.

La bandera funciona, pero aparece otro problema. Imagine una pantalla con dos pestañas. Al cambiar, se inicia una transacción, que causa un replacefragmento uno sobre otro, y todo se rompe.



El segundo fragmento también tiene un conjunto de banderas FitSystemWindow=”true”, y esto no debería suceder. Pero qué pasó, ¿por qué? La respuesta es que esto es una muleta, y a veces no funciona.

Pero encontramos una solución en Stack Overflow : use root para no fragmentos FrameLayout, sino CoordinatorLayout. Fue creado para otros fines, pero funciona aquí.

Por que funciona


Veamos en la fuente lo que está sucediendo en CoordinatorLayout.

@Override
public void onAttachedToWindow() {   
    super.onAttachedToWindow();
    ...    
    if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {         
        // We're set to fitSystemWindows but we haven't had any insets yet...
        // We should request a new dispatch of window insets
        ViewCompat.requestApplyInsets(this);
    }     
    ...
}

Vemos un hermoso comentario de que aquí se necesitan inserciones, pero no lo son. Necesitamos volver a preguntarles cuando solicitamos una ventana. Descubrimos que las inserciones de alguna manera funcionan dentro, pero no queremos trabajar con ellas e irnos Coordinator.

En la primavera de 2019, desarrollamos la aplicación, momento en el que Google I / O acaba de pasar. Todavía no hemos resuelto todo, así que seguimos aferrándonos a los prejuicios. Pero notamos que cambiar las pestañas en la parte inferior es de alguna manera lento, porque tenemos un diseño complejo, cargado con la interfaz de usuario. Encontramos una manera simple de resolver esto: cambie replacepara show/hideque cada vez que no vuelva a crear el diseño.



Cambiamos, y de nuevo nada funciona, ¡todo se ha roto! Aparentemente no es posible esparcir muletas, debes entender por qué funcionaron. Estudiamos el código y resulta que cualquier ViewGroup también puede trabajar con inserciones.


@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {    
    insets = super.dispatchApplyWindowInsets(insets);     
    if (!insets.isConsumed()) {         
        final int count = getChildCount();        
        for (int i = 0; i < count; i++) {
            insets = getChildAt(i).dispatchApplyWindowInsets(insets);             
            if (insets.isConsumed()) {          
                break;
            }
        }
    }
    return insets;
}

Existe una lógica de este tipo: si al menos alguien ya ha procesado inserciones, todas las vistas posteriores dentro del ViewGroup no las recibirán. ¿Qué significa esto para nosotros? Déjame mostrarte un ejemplo de nuestro FrameLayout, que cambia las pestañas.



Dentro hay el primer fragmento que tiene la bandera establecida fitSystemWindow=”true”. Esto significa que el primer fragmento procesa las inserciones. Después de eso, llamamos al HIDEprimer fragmento y al SHOWsegundo. Pero el primero permanece en el diseño: su vista se deja, solo oculta.

El contenedor pasa por su Vista: toma el primer fragmento, le da insertos, y de él fitSystemWindow=”true”, los tomó y los procesó. Perfectamente, FrameLayout parecía que los insertos fueron procesados, y no se lo dio al segundo fragmento. Todo funciona como deberia. Pero esto no nos conviene, ¿qué debemos hacer?

Escribimos nuestro propio ViewGroup


Venimos de Java, y hay POO en todo su esplendor, así que decidimos heredar. Escribimos nuestro propio ViewGroup, desde el cual redefinimos el método dispatchApplyWindowInsets. Funciona de la manera que lo necesitamos: siempre devuelve a los niños las inserciones que vinieron, independientemente de si las procesamos o no.

class WindowInsetFrameLayout @JvmOverloads constructor(
    context: Context,      
    attrs: AttributeSet? = null,     
    defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleRes) {

    override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets { 
        for (child in (0 until childCount)) {
            getChildAt(child).dispatchApplyWindowInsets(insets)
        }
        return insets.consumeSystemWindowInsets()
    }
}

Custom ViewGroup funciona, el proyecto también es todo lo que necesitamos. Pero aún no llega a una solución general. Cuando descubrimos lo que nos dijeron en Google IO, nos dimos cuenta de que ya no podremos actuar así.

Android 10


Android 10 nos mostró dos conceptos importantes de IU a los que se recomienda cumplir estrictamente: borde a borde y navegación gestual.

Borde a borde. Este concepto sugiere que el contenido de la aplicación debería ocupar todo el espacio posible en la pantalla. Para nosotros, como desarrolladores, esto significa que las aplicaciones deben colocarse bajo los paneles del sistema de la barra de estado y la barra de navegación.



Anteriormente, de alguna manera podíamos ignorar esto o colocarnos solo debajo de la Barra de estado.

En cuanto a las listas, deben desplazarse no solo hasta el último elemento, sino también más allá para no permanecer debajo de la barra de navegación.


Navegación gestual. Este segundo concepto importante es la navegación por gestos . Le permite controlar la aplicación con los dedos desde el borde de la pantalla. El modo de gestos no es estándar, todo parece diferente, pero ahora no puede cambiar entre dos barras de navegación diferentes.

En este momento, nos dimos cuenta de que no todo es tan simple. No será posible evitar más inserciones si queremos desarrollar aplicaciones de calidad. Es hora de estudiar la documentación y comprender qué son los insertos.

Inserciones ¿Qué necesitas saber sobre ellos?


Las inserciones fueron creadas para asustar a los desarrolladores.
Lo han estado haciendo perfectamente desde el momento en que apareció en Android 5.

Por supuesto, no todo es tan aterrador. El concepto de insertos es simple: informan una superposición de IU del sistema en la pantalla de la aplicación. Puede ser una barra de navegación, una barra de estado o un teclado. El teclado también es una interfaz de usuario del sistema normal, que se superpone en la parte superior de la aplicación. No es necesario tratar de procesarlo con muletas, solo insertos.

El objetivo de las inserciones es resolver conflictos . Por ejemplo, si hay otro elemento sobre nuestro botón, podemos mover el botón para que el usuario pueda continuar usando la aplicación.

Para manejar la inserción, use Windowlnsetspara Android 10 y WindowInsetsCompatpara otras versiones.

Hay 5 tipos diferentes de inserciones en Android 10y un "bono", que se llama no insertado, sino de otro modo. Nos ocuparemos de todos los tipos, porque la mayoría solo sabe una cosa: Inserciones de ventanas del sistema.

Sistema de ventanas


Introducido en Android 5.0 (API 21). Obtenido por el método getSystemWindowInsets().

Este es el tipo principal de inserciones con las que necesita aprender a trabajar, porque el resto funciona de la misma manera. Son necesarios para procesar la barra de estado, en primer lugar, y luego la barra de navegación y el teclado. Por ejemplo, resuelven un problema cuando la barra de navegación está por encima de la aplicación. Como en la imagen: el botón permaneció debajo de la barra de navegación, el usuario no puede hacer clic en él y está muy descontento.



Inserciones de elementos que se pueden tocar


Apareció solo en Android 10. Obtenido por el método getTappableElementInsets().

Estas inserciones solo son útiles para manejar varios modos de barra de navegación . Como dice el propio Chris Bane , puede olvidarse de este tipo de inserciones y moverse solo por los recuadros de ventana del sistema. Pero si desea que la aplicación sea 100% fresca, no 99.9% fresca, debe usarla.

Mira la foto.



En la parte superior, el color carmesí indica los recuadros de la ventana del sistema, que vendrán en diferentes modos de la barra de navegación. Se puede ver que a la derecha y a la izquierda son iguales a la barra de navegación.

Recuerde cómo funciona la navegación por gestos: en el modo correcto, nunca hacemos clic en un nuevo panel, sino que siempre arrastramos los dedos de abajo hacia arriba. Esto significa que los botones no se pueden quitar. Podemos continuar presionando nuestro botón FAB (botón de acción flotante), nadie interferirá. Por lo tanto, TappableElementInsetsquedará vacío porque no es necesario mover FAB. Pero si nos movemos un poco más arriba, está bien.

La diferencia aparece solo con la navegación por gestos y la barra de navegación transparente (adaptación de color). Esta no es una situación muy agradable.



Todo funcionará, pero se ve desagradable. El usuario puede confundirse por la proximidad de los elementos. Se puede explicar que uno es para gestos y el otro para presionar, pero aún no es hermoso. Por lo tanto, eleve el FAB más alto o déjelo a la derecha.

Inserciones gestuales del sistema


Apareció solo en Android 10. Obtenido por el método getSystemGestureInsets().

Estas inserciones están relacionadas con la navegación por gestos. Inicialmente, se suponía que la IU del sistema se dibuja sobre la aplicación, pero las Inserciones de gestos del sistema dicen que no se dibuja la IU, sino que el sistema mismo maneja los gestos. Describen dónde el sistema manejará los gestos por defecto.

Las áreas de estas inserciones son aproximadamente las marcadas en amarillo en el diagrama.



Pero quiero advertirte: no siempre serán así. Nunca sabremos qué pantallas nuevas se le ocurrirán a Samsung y otros fabricantes. Ya hay dispositivos en los que la pantalla está en todos los lados. Quizás las inserciones no estarán donde esperamos en absoluto. Por lo tanto, debe trabajar con ellos como con alguna abstracción: existen inserciones en los recuadros de gestos del sistema en los que el sistema mismo procesa los gestos.

Estas inserciones pueden ser anuladas. Por ejemplo, está desarrollando un editor de fotos. En algunos lugares, usted mismo quiere manejar los gestos, incluso si están cerca del borde de la pantalla. Indique al sistema que procesará el punto en la pantalla en la esquina de la foto usted mismo. Se puede redefinir diciéndole al sistema: "Yo mismo siempre procesaré el cuadrado alrededor del punto".

Inserciones obligatorias de gestos del sistema


Apareció solo en Android 10. Este es un subtipo de Inserciones de gestos del sistema, pero la aplicación no puede anularlas.

Usamos el método getMandatorySystemGestureInsets()para determinar el área donde no funcionarán. Esto se hizo intencionalmente para que fuera imposible desarrollar una aplicación "desesperada": redefinir el gesto de navegación de abajo hacia arriba, lo que le permite salir de la aplicación a la pantalla principal. Aquí, el sistema siempre procesa los gestos en sí .


No necesariamente estará en la parte inferior del dispositivo y no necesariamente de este tamaño. Trabaja con él como una abstracción.

Estos eran relativamente nuevos tipos de insertos. Pero hay aquellos que aparecieron incluso antes de Android 5 y se llamaron de manera diferente.

Insertos estables


Introducido con Android 5.0 (API 21). Obtenido por el método getStableInsets().

Incluso los desarrolladores experimentados no siempre pueden decir por qué son necesarios. Te diré un secreto: estas inserciones solo son útiles para aplicaciones de pantalla completa: reproductores de video, juegos. Por ejemplo, en el modo de reproducción, cualquier interfaz de usuario del sistema está oculta, incluida la barra de estado, que se mueve más allá del borde de la pantalla. Pero vale la pena tocar la pantalla ya que la barra de estado aparece en la parte superior.

Si coloca cualquier botón "ATRÁS" en la parte superior de la pantalla, mientras procesa correctamente los recuadros de la ventana del sistema, entonces con cada pestaña aparece una IU en la parte superior de la pantalla, y el botón salta hacia abajo. Si no toca la pantalla durante un tiempo, la barra de estado desaparece, el botón se activa porque los recuadros de la ventana del sistema han desaparecido.

Para tales casos, los recuadros estables son perfectos. Dicen que ahora no se dibuja ningún elemento en su aplicación, pero puede hacerlo. Con estas inserciones, puede conocer de antemano el valor de la barra de estado en este modo, por ejemplo, y colocar el botón donde desee.

Nota. El método getStableInsets()apareció solo con API 21. Anteriormente, había varios métodos para cada lado de la pantalla. 

Examinamos 5 tipos de insertos, pero hay uno más que no se aplica a los insertos directamente. Ayuda a lidiar con flequillo y recortes.

Flequillo y escotes


Las pantallas ya no son cuadradas. Son oblongos, con uno o más recortes para la cámara y golpes en todos los lados.



Hasta 28 API no pudimos procesarlos. Tuve que adivinar a través de las inserciones lo que estaba sucediendo allí. Pero con la API 28 y más allá (desde el Android anterior), la clase apareció oficialmente DisplayCutout. Está en las mismas inserciones de donde puede obtener todos los otros tipos. La clase le permite averiguar la ubicación y el tamaño de los artefactos.

Además de la información de ubicación, se proporciona un conjunto de indicadores y para el desarrollador WindowManager.LayoutParams. Le permiten incluir diferentes comportamientos en torno a los recortes. Su contenido puede mostrarse a su alrededor, o puede no mostrarse: en modo horizontal y vertical de diferentes maneras.

Banderaswindow.attributes.layoutInDisplayCutoutMode =

  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT. En vertical, hay, y en horizontal, una barra negra por defecto.
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER. Para nada, una franja negra.
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - siempre lo es.

La pantalla se puede controlar, pero trabajamos con ellos de la misma manera que con cualquier otro recuadro.

insets.displayCutout
    .boundingRects
    .forEach { rect -> ... }

La devolución de llamada con inserciones con una matriz nos llega displayCutout. Luego podemos ejecutarlo, procesar todos los recortes y explosiones que se encuentran en la aplicación. Puede obtener más información sobre cómo trabajar con ellos en el artículo .

Entendido con 6 tipos de inserciones. Ahora hablemos de cómo funciona.

Cómo funciona


Se necesitan inserciones, en primer lugar, cuando se dibuja algo en la parte superior de la aplicación, por ejemplo, la barra de navegación.



Sin inserciones, la aplicación no tendrá una barra de navegación y una barra de estado transparentes. No te sorprendas de no venir SystemWindowInsets, esto sucede.

Separar inserciones


Toda la jerarquía de la interfaz de usuario se parece a un árbol con una vista en sus extremos. Los nodos suelen ser un ViewGroup. También heredan de View, por lo que las inserciones les llegan de una manera especial dispatchApplyWindowInsets. A partir de la Vista raíz, el sistema envía inserciones en todo el árbol. Veamos cómo funciona Ver en este caso.



El sistema recurre al método dispatchApplyWindowInsetspor el cual entran las inserciones. Volver esta vista debería devolver algo. ¿Qué hacer para manejar este comportamiento?

Por simplicidad, solo consideramos WindowInsets.

Manejo de insertos


En primer lugar, la redefinición del método dispatchApplyWindowInsetsy la implementación dentro de su propia lógica parece lógica : llegaron las inserciones, implementamos todo dentro.

Si abre la clase Ver y mira el Javadoc escrito sobre este método, puede ver: "¡No anule este método!"
Manejamos Insets a través de delegación o herencia.
Usar delegación . Google brindó la oportunidad de exponer a su propio delegado, quien es responsable del manejo de las inserciones. Puede instalarlo a través del método setOnApplyWindowInsetsListener(listener). Establecemos una devolución de llamada que maneja estas inserciones.

Si por alguna razón esto no nos conviene, puede heredar de Ver y anular otro método onApplyWindowInsets(WindowInsets insets) . Sustituimos nuestras propias inserciones.

Google no eligió entre delegación y herencia, pero permitió hacer todo junto. Esto es genial porque no tenemos que redefinir todas las barras de herramientas o ListView para manejar las inserciones. Podemos tomar cualquier Vista ya preparada, incluso una de biblioteca, y establecer el delegado allí, que manejará las inserciones sin redefinir.

¿Qué devolveremos?

¿Por qué devolver algo?


Para hacer esto, debe comprender cómo funciona la distribución de inserciones, es decir, ViewGroup. ¿Qué está haciendo ella dentro de sí misma? Echemos un vistazo más de cerca al comportamiento predeterminado usando el ejemplo que mostré anteriormente.

Las inserciones del sistema llegaron en la parte superior e inferior, por ejemplo, 100 píxeles cada una. ViewGroup los envía a la primera vista que tiene. Esta vista los manejó de alguna manera y devuelve inserciones: en la parte superior disminuye a 0, pero en la parte inferior se va. En la parte superior, agregó relleno o margen, pero en la parte inferior no tocó e informó sobre esto. A continuación, ViewGroup pasa inserciones a la segunda vista.



La siguiente Vista procesa y renderiza inserciones. Ahora ViewGroup ve que las inserciones se procesan tanto arriba como abajo: no queda nada, todos los parámetros son cero.

La tercera Vista ni siquiera sabe que hubo algunas inserciones y que algo estaba sucediendo. ViewGroup devolverá las inserciones a quien las expuso.

Cuando comenzamos a tratar este tema, escribimos la idea por nosotros mismos: siempre devolvemos las mismas inserciones que vinieron. Queríamos evitar tal situación cuando alguna Vista ni siquiera reconocía que había inserciones. La idea parecía lógica. Pero resultó que no. Google no ha agregado en vano el comportamiento a las inserciones, como en el ejemplo.

Esto es necesario para que las inserciones siempre lleguen a la jerarquía completa de la Vista y siempre pueda trabajar con ellas. En este caso, no habrá situación cuando cambiemos dos fragmentos, uno ha procesado y el segundo aún no ha recibido. En la sección de práctica, volveremos a este punto.

Práctica


La teoría está hecha. Veamos cómo se ve en el código, porque en teoría todo siempre es fluido. Usaremos AndroidX.

Nota. En GitFox, todo ya está implementado en el código, soporte completo para todos los extras del nuevo Android. Para no verificar las versiones de Android y no buscar el tipo requerido de Insets, use siempre la view.setOnApplyWindowInsetsListener { v, insets -> ... }versión de AndroidX ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> ... }.

Hay una pantalla con una barra de navegación oscura. No se superpone a ningún elemento en la parte superior. Todo es como estamos acostumbrados.


Pero resulta que ahora tenemos que hacerlo transparente. ¿Cómo?

Activar transparencia


Lo más simple es indicar en el tema que la barra de estado y la barra de navegación son transparentes.

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
    <item name="android:windowTranslucentStatus">true</item>
    <item name="android:windowTranslucentNavigation">true</item>
</style>

En este momento, todo comienza a superponerse tanto desde arriba como desde abajo.

Curiosamente, hay dos banderas, y siempre hay tres opciones: si especifica la transparencia para la barra de navegación, la barra de estado se volverá transparente por sí misma, tal limitación. No puedes escribir la primera línea, pero siempre me encanta la claridad, así que escribo 2 líneas para que los seguidores puedan entender lo que está sucediendo y no profundizar.

Si elige este método, la barra de navegación y la barra de estado se volverán negras con transparencia. Si la aplicación es blanca, puede agregar colores solo desde el código. ¿Cómo hacerlo?

Activa la transparencia con color


Es bueno que tengamos una aplicación de Actividad única, por lo que ponemos dos banderas en una actividad.

rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or  
    View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
    <!-- Set the navigation bar to 50% translucent white -->
    <item name="android:navigationBarColor">#80FFFFFF</item> 
</style>

Hay muchas banderas allí, pero son estas dos las que ayudarán a hacer transparencia en la Barra de navegación y la Barra de estado. El color se puede especificar a través del tema.

Este es el comportamiento extraño de Android: algo a través del tema y algo a través de las banderas. Pero podemos especificar parámetros tanto en el código como en el tema. Esto no es tan malo para Android, solo en versiones anteriores de Android se ignorará la bandera especificada en el tema. navigationBarColorDestacará que en Android 5 esto no es así, pero todo se unirá. En GitFox, fue desde el código que configuré el color de la barra de navegación.

Realizamos fraudes: indicamos que la barra de navegación es blanca con transparencia. Ahora la aplicación se ve así.


¿Qué podría ser más fácil que manipular insertos?


De hecho, primaria.

ViewCompat
    .setOnApplyWindowInsetsListener(bottomNav) { view, insets ->  
        view.updatePadding(bottom = insets.systemWindowInsetBottom) 
        insets
    }

Tomamos el método setOnApplyWindowInsetsListenery se lo pasamos bottomNav. Decimos que cuando vienen las inserciones, instale el relleno inferior que entró systemWindowInsetBottom. Lo queremos debajo, pero todos nuestros elementos en los que se puede hacer clic están en la parte superior. Devolvemos las inserciones completas para que todas las demás Vistas de nuestra jerarquía también las reciban.

Todo se ve genial.


Pero hay una trampa. Si en el diseño indicamos algún tipo de relleno en la barra de navegación a continuación, entonces aquí se eliminó: establezca las updatePaddinginserciones iguales. Nuestro diseño no se ve como nos gustaría.

Para guardar los valores del diseño, primero debe guardar lo que está en el relleno inferior. Más tarde, cuando lleguen las inserciones, agregue y establezca los valores resultantes.

val bottomNavBottomPadding = bottomNav.paddingBottom
ViewCompat
    .setOnApplyWindowInsetsListener(bottomNav) { view, insets ->                 
        view.updatePadding(
        bottom = bottomNavBottomPadding + insets.systemWindowInsetBottom
    )
    Insets
}

Es un inconveniente escribir de esta manera: en todas partes del código donde usa inserciones, debe guardar el valor del diseño y luego agregarlo a la interfaz de usuario. Pero está Kotlin, y esto es maravilloso: puede escribir su propia extensión, que hará todo esto por nosotros.

Añadir Kotlin!


Lo recordamos initialPaddingy se lo damos al manejador cuando aparecen nuevos insertos (junto con ellos). Esto los ayudará de alguna manera a agregar o construir algún tipo de lógica desde arriba.

fun View.doOnApplyWindowInsets(block: (View, WindowInsetsCompat, Rect) -> WindowInsetsCompat) {    

    val initialPadding = recordInitialPaddingForView(this)

    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->        
        block(v, insets, initialPadding)
    }
    
    requestApplyInsetsWhenAttached()
}

private fun recordInitialPaddingForView(view: View) =
   Rect(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)

  .

bottomNav.doOnApplyWindowInsets { view, insets, padding ->   
    view.updatePadding(
        bottom = padding.bottom + insets.systemWindowInsetBottom
    ) 
    insets
}

Tenemos que redefinir la lambda. No solo tiene inserciones, sino también acolchados. Podemos agregarlos no solo desde abajo, sino también desde arriba, si se trata de una barra de herramientas que procesa la barra de estado.

Algo olvidado!


Hay un llamado a un método incomprensible.

fun View.doOnApplyWindowInsets(block: (View, WindowInsetsCompat, Rect) -> WindowInsetsCompat) {

    val initialPadding = recordInitialPaddingForView(this)
     
    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->        
        block(v, insets, initialPadding)
    }
      
    requestApplyInsetsWhenAttached() 
}

private fun recordInitialPaddingForView(view: View) =
      Rect(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)

Esto se debe al hecho de que no puedes esperar que el sistema te envíe inserciones. Si creamos una Vista desde el código o la instalamos un InsetsListenerpoco más tarde, el sistema en sí mismo puede no pasar el último valor.

Debemos verificar que cuando la Vista esté en la pantalla, le hayamos dicho al sistema: "Los recuadros han llegado, queremos procesarlos". Configuramos el oyente y debemos hacer una solicitud requestsApplyInsets".

fun View.requestApplyInsetsWhenAttached() {    
    if (isAttachedToWindow) {        
        requestApplyInsets()
    } else {
        addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {            
            override fun onViewAttachedToWindow(v: View) {
                v.removeOnAttachStateChangeListener(this)
                v.requestApplyInsets()           
            }

            override fun onViewDetachedFromWindow(v: View) = Unit
        })
    }
}

Si creamos un código de Vista y aún no lo hemos adjuntado a nuestro diseño, entonces debemos suscribirnos al momento en que lo hacemos. Solo entonces solicitamos inserciones.

Pero, de nuevo, está Kotlin: escribieron una extensión simple y ya no tenemos que pensar en eso.

Refinar RecyclerView


Ahora hablemos de finalizar RecyclerView. El último elemento se encuentra debajo de la barra de navegación y permanece debajo, feo. También es inconveniente hacer clic en él. Si este no es un panel nuevo, sino el antiguo, uno grande, entonces, en general, todo el elemento puede desaparecer debajo de él. ¿Qué hacer?



La primera idea es agregar un elemento a continuación y establecer la altura para encajar en las inserciones. Pero si tenemos una aplicación con cientos de listas, entonces de alguna manera tendremos que suscribir cada lista a inserciones, agregar un nuevo elemento allí y establecer la altura. Además, RecyclerView es asíncrono, no se sabe cuándo funcionará. Hay muchos problemas.

Pero hay otro truco oficial. Sorprendentemente, funciona de manera eficiente.

<androidx.recyclerview.widget.RecyclerView 
        android:id="@+id/recyclerView"        
    android:layout_width="match_parent"    
    android:layout_height="match_parent"             
    android:clipToPadding="false" />

recyclerView.doOnApplyWindowInsets { view, insets, padding ->    
    view.updatePadding(
        bottom = padding.bottom + insets.systemWindowInsetBottom
    )
}

Hay una bandera en el diseño clipToPadding. Por defecto, siempre es cierto. Esto sugiere que no necesita dibujar elementos que aparecen donde el relleno está expuesto. Pero si está configurado clipToPadding="false", entonces puedes dibujar.

Ahora, si configura el relleno en la parte inferior, en RecyclerView funcionará así: el relleno se configura en la parte inferior y el elemento se dibuja encima de él hasta que nos desplazamos. Una vez alcanzado el final, RecyclerView desplazará los elementos a la posición que necesitamos.

Al establecer uno de estos indicadores, podemos trabajar con RecyclerView como con una vista normal. No piense que hay elementos que se desplazan, simplemente configure el relleno a continuación, como, por ejemplo, en la barra de navegación inferior.

Ahora siempre devolvíamos las inserciones como si no hubieran sido procesadas. Los insertos enteros vinieron a nosotros, hicimos algo con ellos, colocamos el relleno y devolvimos todos los insertos enteros nuevamente. Esto es necesario para que cualquier ViewGroup siempre pase estas inserciones a toda la Vista. Funciona.

Error de aplicación


En muchas aplicaciones en Google Play que ya han procesado inserciones, noté un pequeño error. Ahora les contaré sobre él.

Hay una pantalla con navegación en el sótano. A la derecha está la misma pantalla que muestra la jerarquía. El fragmento verde muestra el contenido en la pantalla, dentro tiene un RecyclerView.



¿Quién manejó las inserciones aquí? Barra de herramientas: aplicó relleno en la parte superior para que su contenido se mueva debajo de la barra de estado. En consecuencia, la barra de navegación inferior aplicó inserciones desde abajo y se elevó por encima de la barra de navegación. Pero RecyclerView no maneja las inserciones de ninguna manera, no cae debajo de ellas, no necesita procesar inserciones: todo se hace correctamente.



Pero aquí resulta que queremos usar el fragmento verde RecyclerView en otro lugar donde la barra de navegación inferior ya no está allí. En este punto, RecyclerView ya está comenzando a manejar inserciones desde abajo. Necesitamos aplicarle relleno para desplazarnos correctamente desde debajo de la barra de navegación. Por lo tanto, en RecyclerView, también agregamos procesamiento de inserciones.



Volvemos a nuestra pantalla, donde todos procesan las inserciones. ¿Recuerdas que nadie informa que los procesó?



Vemos esta situación: RecyclerView procesó las inserciones desde abajo, aunque no llega a la barra de navegación, apareció un espacio en la parte inferior. Me di cuenta de esto en las aplicaciones, y bastante grande y popular.



Si este es el caso, recordamos que devolvemos todas las inserciones para procesar. Entonces (¡resulta!) Es necesario informar que las inserciones se procesan: la barra de navegación debe informar que las inserciones se procesaron. No deberían llegar a RecyclerView.



Para hacer esto, instale la barra de navegación inferior InsetsListener. Resta dentro de bottom + insetsque no se procesan, devuelve 0.

doOnApplyWindowInsets { view, insets, initialPadding ->    
    view.updatePadding(
        bottom = initialPadding.bottom + insets.systemWindowInsetBottom
    )
    insets.replaceSystemWindowInsets(
        Rect(
            insets.systemWindowInsetLeft,            
            insets.systemWindowInsetTop,            
            insets.systemWindowInsetRight,
            0
        )
    )
}

Devolvemos nuevas inserciones, en las que todos los parámetros antiguos son iguales, pero bottom + insetsiguales a 0. Parece que todo está bien. Comenzamos y nuevamente tonterías: RecyclerView todavía maneja inserciones por alguna razón.



No entendí de inmediato que el problema es que el contenedor de estas tres Vistas es LinearLayout. Dentro de ellos hay una barra de herramientas, un fragmento con RecyclerView y en la parte inferior de la barra de navegación inferior. LinearLayout toma a sus hijos en orden y les aplica inserciones.

Resulta que la barra de navegación inferior será la última. Le dijo a alguien que procesó todas las inserciones, pero ya era demasiado tarde. Todos los insertos se procesaron de arriba a abajo, RecyclerView ya los recibió, y esto no nos salva.



¿Qué hacer? LinearLayout no funciona como yo quiero, los transfiere de arriba a abajo, y primero necesito obtener el inferior.

Redefinir todo


El desarrollador de Java jugó en mí, ¡ todo necesita ser redefinido ! Bueno, ahora dispatchApplyWindowInsetsredefinir, establecer yLinearLayout, que siempre irá de abajo hacia arriba. Primero enviará inserciones a la barra de navegación inferior y luego a todos los demás.

@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {     
    insets = super.dispatchApplyWindowInsets(insets);     
    if (!insets.isConsumed()) {        
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            insets = getChildAt(i).dispatchApplyWindowInsets(insets);
            if (insets.isConsumed()) {
                break;
            }
        }
    }
    return insets;
}

Pero me detuve a tiempo, recordando el comentario de que no había necesidad de redefinir este método.

Aquí, un poco más alto es la salvación: ViewGroup busca un delegado que procese las inserciones, luego llama al súper método en View y habilita su propio procesamiento. En este código, obtenemos inserciones, y si aún no se han procesado, comenzamos la lógica estándar para nuestros hijos.

Escribí una extensión simple. Permite, aplicando InsetsListenera una Vista, decir a quién pasan estas inserciones.

fun View.addSystemBottomPadding(    
    targetView: View = this
) {
    doOnApplyWindowInsets { _, insets, initialPadding ->           
        targetView.updatePadding(
            bottom = initialPadding.bottom + insets.systemWindowInsetBottom
        )
        insets.replaceSystemWindowInsets(
            Rect(
                insets.systemWindowInsetLeft,
                insets.systemWindowInsetTop,     
                insets.systemWindowInsetRight,
                0
            )
        )
    }
}

Aquí targetViewes igual por defecto a la misma Vista sobre la cual aplicamos addSystemBottomPadding. Podemos redefinirlo.

En mi diseño lineal colgué un controlador de este tipo, pasando como targetView: esta es mi barra de navegación inferior.

  • Primero le dará inserciones a la barra de navegación inferior. 
  • Eso procesará las inserciones, devolverá el fondo cero.
  • Además, de manera predeterminada, irán de arriba a abajo: barra de herramientas, fragmento con RecyclerView.
  • Entonces, tal vez, enviará nuevamente insertos Barra de navegación inferior. Pero esto ya no es importante, todo funcionará muy bien.

Logré exactamente lo que quería: todas las inserciones se procesan en el orden en que se necesitan.



Importante


Algunas cosas importantes a tener en cuenta.

El teclado es la interfaz de usuario del sistema en la parte superior de su aplicación. No hay necesidad de tratarla de una manera especial. Si observa el teclado de Google, no es solo un diseño con botones en los que puede hacer clic. Hay cientos de modos para este teclado: búsqueda de gifs y memes, entrada de voz, cambio de tamaño de 10 píxeles de altura a tamaños de pantalla. No pienses en el teclado, sino suscríbete a las inserciones.

El cajón de navegación aún no admite la navegación por gestos. En Google IO, prometieron que todo saldría de la caja. Pero en Android 10, Navigation Drawer todavía no es compatible con esto. Si actualiza a Android 10 y habilita la navegación con gestos, el cajón de navegación se caerá. Ahora debe hacer clic en el menú de hamburguesas para que aparezca, o una combinación de circunstancias aleatorias le permite estirarlo.

En la versión pre-alfa de Android, Navigation Drawer funciona, pero no me atreví a actualizarlo, esto es pre-alfa. Por lo tanto, incluso si instala la última versión de GitFox desde el repositorio, hay un cajón de navegación allí, pero no se puede extraer. Tan pronto como salga el soporte oficial, actualizaré y todo funcionará bien.

Lista de verificación de preparación de Android 10


Establezca la transparencia de la barra de navegación y la barra de estado desde el comienzo del proyecto . Google recomienda encarecidamente mantener una barra de navegación transparente. Para nosotros, esta es una parte práctica importante. Si el proyecto funciona, pero no lo activó, tómese el tiempo para admitir Android 10. Actívelo primero en el tema, como translúcido, y corrija el diseño donde se rompió.

Agregue extensiones a Kotlin : es más fácil con ellas.

Añadir a toda la barra de herramientas en la parte superior del relleno. Las barras de herramientas siempre están en la parte superior, y eso es lo que debe hacer.

Para todas las RecyclerViews, agregue relleno desde la parte inferior y la capacidad de desplazarse clipToPadding="false".

Piense en todos los botones en los bordes de la pantalla (FAB) . Lo más probable es que las FAB no estén donde esperabas.

No anule ni realice sus propias implementaciones para todos LinearLayout y otros casos similares. Haz el truco, como en GitFox, o toma mi extensión ya preparada para ayudar.

Verifique los gestos en los bordes de la pantalla para una vista personalizada . Navigation Drawer no es compatible con esto. Pero no es tan difícil apoyarlo con las manos, redefinir las inserciones para los gestos en la pantalla con el cajón de navegación. Tal vez tienes editores de imágenes donde funcionan los gestos.

Desplazarse en ViewPager no funciona desde el borde, sino solo desde el centro hacia la derecha y hacia la izquierda . Google dice que esto es normal. Si tira de ViewPager desde el borde, se percibe que presiona el botón "Atrás".

En un nuevo proyecto, incluya de inmediato toda la transparencia.. Puede decirle a los diseñadores que no tiene una barra de navegación y una barra de estado. Todo el cuadrado es el contenido de su aplicación. Y los desarrolladores ya descubrirán dónde y qué recaudar.

Enlaces útiles:

  • @rmr_spb en un telegrama: registros de mitaps internos de Redmadrobot SPb.
  • Todo el código fuente en este artículo y aún más en GitFox . Hay una rama separada de Android 10, donde puedes ver confirmando cómo llegué a todo esto y cómo admití el nuevo Android.
  • La biblioteca de Chris Bane Insetter . Contiene 5-6 extensiones que mostré. Para usar en su proyecto, comuníquese con la biblioteca, lo más probable es que se traslade a Android Jetpack. Los desarrollé en mi proyecto y, me parece, mejoraron.
  • El artículo de Chris Bane.
  • FunCorn, , .
  • «Why would I want to fitsSystemWindows?» .

AppConf, youtube- . , - . AppsConf , . , , stay tuned!

All Articles