Navegación por gestos y la experiencia de borde a borde

En Android 10 o versiones posteriores, la navegación por gestos está disponible como un modo nuevo. Esto permitirá que tu app use la pantalla completa y brinde una experiencia de pantalla más envolvente. Si el usuario desliza el dedo hacia arriba desde el borde inferior de la pantalla, se lo dirige a la pantalla principal de Android. Cuando desliza el dedo hacia el centro desde los bordes izquierdo o derecho, esta acción lo lleva a la pantalla anterior.

Mediante estos dos gestos, la app puede aprovechar el espacio real de la pantalla en la parte inferior. Sin embargo, si tu app usa gestos o tiene controles en las áreas de gestos del sistema, es posible que esto provoque algún conflicto con los gestos de todo el sistema.

El objetivo de este codelab es enseñarte a usar inserciones a fin de evitar conflictos en relación con los gestos. Además, con este codelab aprenderás a usar la API de Gesture Exclusion para los controles, como los de arrastre, que necesitan ubicarse en las áreas de gestos.

Qué aprenderás

  • Cómo usar los objetos de escucha de inserciones en las vistas
  • Cómo usar la API de Gesture Exclusion
  • Cómo se comporta el modo envolvente cuando se activan los gestos

El objetivo de este codelab es hacer que tu app resulte compatible con los gestos del sistema. Se pasarán por alto los conceptos irrelevantes y los bloques de código, que se proporcionarán para que puedas copiarlos y pegarlos.

Qué compilarás

El Universal Android Music Player (UAMP) es un ejemplo de app de reproducción de música para Android escrita en Kotlin. Configurarás el UAMP para la navegación por gestos.

  • Usa las inserciones a fin de alejar los controles de las áreas de gestos
  • Usa la API de Gesture Exclusion a fin de inhabilitar el gesto de retroceder en los controles que presenten algún conflicto
  • Usa tus compilaciones para explorar los cambios de comportamiento del modo envolvente con Gesture Navigation

Requisitos

  • Un dispositivo o emulador que ejecute Android 10 o una versión posterior
  • Android Studio

El Universal Android Music Player (UAMP) es un ejemplo de app de reproducción de música para Android escrita en Kotlin. Es compatible con funciones como la reproducción en segundo plano, el control del foco de audio, la integración con Asistente, y con varias plataformas como Wear, TV y Auto.

Figura 1: Un flujo en UAMP

UAMP carga un catálogo musical desde un servidor remoto y le permite al usuario explorar álbumes y canciones. El usuario presiona una canción, que se reproduce mediante bocinas o auriculares conectados. La app no se diseñó para funcionar con los gestos del sistema. Por lo tanto, cuando ejecutes UAMP en un dispositivo con Android 10 o una versión posterior, al principio encontrarás algunos problemas.

Para obtener la app de ejemplo, clona el repositorio desde GitHub y cambia a la rama starter:

$  git clone https://github.com/googlecodelabs/android-gestural-navigation/


También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Descarga el archivo ZIP

Completa los siguientes pasos:

  1. Abre y ejecuta la app en Android Studio.
  2. Crea un dispositivo virtual nuevo y selecciona el nivel de API 29. También puedes conectar un dispositivo real que ejecute ese nivel de API o uno posterior.
  3. Ejecuta la app. La lista que ves agrupa las canciones en las selecciones de Recomendaciones y Álbumes.
  4. Haz clic en Recomendaciones y selecciona una canción de la lista.
  5. La app comenzará a reproducir la canción.

Cómo habilitar la navegación por gestos

Si ejecutas una instancia nueva del emulador con el nivel de API 29, es posible que la navegación por gestos no esté activada de forma predeterminada. Para habilitarla, selecciona System settings > System > System Navigation > Gesture Navigation.

Ejecución de la app con la navegación por gestos

Si ejecutas la app con la navegación por gestos habilitada y comienzas la reproducción de una canción, es posible que notes que los controles del reproductor están muy cerca de las áreas de gestos para ir a la página principal o el de retroceder.

¿En qué consiste el borde a borde?

Las apps que se ejecutan en Android 10 o una versión posterior pueden ofrecer una experiencia de pantalla de borde a borde, independientemente de si se habilitaron los gestos o botones para la navegación. Para ofrecer una experiencia de borde a borde, tus apps deben dibujar detrás de las barras de estado y navegación transparentes.

Cómo dibujar detrás de la barra de navegación

A fin de que tu app renderice contenido debajo de la barra de navegación, primero debes hacer que el fondo de esa barra sea transparente. Luego, debes establecer en transparente la barra de estado. Esto permite que tu app muestre contenido en todo el alto de la pantalla.

A fin de cambiar el color de las barras de navegación y estado, realiza los siguientes pasos:

  1. Barra de navegación: Abre res/values-29/styles.xml y establece navigationBarColor en color/transparent.
  2. Barra de estado: De forma similar, establece statusBarColor en color/transparent.

Revisa la siguiente muestra de código de res/values-29/styles.xml:

<!-- change navigation bar color -->
<item name="android:navigationBarColor">
    @android:color/transparent
</item>

<!-- change status bar color -->
<item name="android:statusBarColor">
    @android:color/transparent
</item>

Marcas de visibilidad de la IU del sistema

Para indicarle al sistema que organice el contenido de la app debajo de las barras del sistema, deberás establecer las marcas de visibilidad de la IU del sistema. Las API de systemUiVisibility en la clase de View permite establecer varias marcas. Completa los pasos siguientes:

  1. Abre la clase MainActivity.kt y busca el método onCreate(). Obtén una instancia del fragmentContainer.
  2. Establece lo siguiente en content.systemUiVisibility:

Revisa la siguiente muestra de código de MainActivity.kt:

  val content: FrameLayout = findViewById(R.id.fragmentContainer)
  content.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
            View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
            View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

Cuando estableces estas marcas, le indicas al sistema que quieres que tu app se muestre en pantalla completa, tal como si no estuvieran las barras de navegación y estado. Completa los pasos siguientes:

  1. Ejecuta la app y selecciona una canción para reproducir de modo que navegues a la pantalla del reproductor.
  2. Verifica que los controles del reproductor se dibujen debajo de la barra de navegación, lo que hará que resulte difícil acceder a ellos:

  1. Navega a la configuración del sistema, cambia al modo de navegación de tres botones y regresa a la app.
  2. Verifica que sea todavía más difícil usar los controles con la barra de navegación de tres puntos: observa que la SeekBar se encuentre escondida detrás de la barra de navegación y que los controles de Reproducir y pausar estén ocultos por la barra de navegación.
  3. Explora y experimenta un poco. Cuando termines, navega hasta la configuración del sistema y vuelve a cambiar la navegación por gestos:

Ahora la app dibuja de borde a borde, pero surgen problemas relativos a la usabilidad y a controles de app en conflicto y superpuestos, los cuales deben resolverse.

Mediante WindowInsets, se indica a la app los casos en que la IU del sistema aparece sobre tu contenido, así como los lugares de la pantalla en que los gestos del sistema tienen prioridad por sobre aquellos integrados en la app. Las inserciones se representan con las clases WindowInsets y WindowInsetsCompat en Jetpack. Te recomendamos que uses WindowInsetsCompat a fin de lograr un comportamiento coherente en todos los niveles de API.

Las inserciones del sistema y aquellas que son obligatorias

Las siguientes API de inserción son los tipos de inserción de uso general:

  • Inserciones de ventanas del sistema: Estas te indican el lugar en el que la IU del sistema se muestra sobre tu app. Analizamos la forma en que puedes usar las inserciones del sistema a fin de alejar los controles de las barras del sistema.
  • Inserciones de gestos del sistema: Estas muestran todas las áreas de gestos. Los controles deslizantes integrados en la app que se encuentren en estas áreas pueden activar accidentalmente los gestos del sistema.
  • Inserciones de gestos obligatorios: Estas son un subgrupo de las inserciones de gestos del sistema y no es posible anularlas. Te indican las áreas de la pantalla en las que el comportamiento de los gestos del sistema tendrán prioridad por sobre aquellos integrados en la app.

Usa las inserciones para mover los controles de la app

Ahora que sabes más sobre las API de inserciones, puedes corregir los controles de la app como se describe en los siguientes pasos:

  1. Obtén una instancia de playerLayout a partir de la instancia del objeto de view.
  2. Agrega un objeto OnApplyWindowInsetsListener a la playerView.
  3. Aleja la vista del área de gestos: Encuentra el valor de la inserción del sistema para la parte inferior y aumenta el padding de la vista en esa cantidad. A fin de actualizar el padding de la vista en consecuencia, suma el [valor asociado con el padding de la parte inferior de la app] con [el valor asociado a aquel de la inserción del sistema para la parte inferior].

Revisa la siguiente muestra de código de NowPlayingFragment.kt:

playerView = view.findViewById(R.id.playerLayout)
playerView.setOnApplyWindowInsetsListener { view, insets ->
   view.updatePadding(
      bottom = insets.systemWindowInsetBottom + view.paddingBottom
   )
   insets
}
  1. Ejecuta la app y selecciona una canción. Observa que, al parecer, nada cambia en los controles del reproductor. Si agregas un punto de interrupción y ejecutas la app en modo de depuración, verás que no se llama al objeto de escucha.
  2. Para corregir esto, cambia a FragmentContainerView, que controla este problema automáticamente. Abre activity_main.xml y cambia FrameLayout a FragmentContainerView.

Revisa la siguiente muestra de código de activity_main.xml:

<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/fragmentContainer"
    tools:context="com.example.android.uamp.MainActivity"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
  1. Ejecuta la app una vez más y navega a la pantalla del reproductor. Los controles de la parte inferior del reproductor se alejaron del área de gestos inferior.

Los controles de la app ahora funcionan con la navegación por gestos, pero estos controles se mueven más de lo esperado. Debes resolver esto.

Cómo mantener el padding y los márgenes actuales

Si cambias a otras apps o si vas a la pantalla principal y luego vuelves a la app sin cerrarla, puedes observar que los controles del reproductor se mueven hacia arriba cada vez.

Esto se debe a que la app activa requestApplyInsets() cada vez que la actividad comienza. Incluso sin esta llamada, WindowInsets puede despacharse varias veces en cualquier momento durante el ciclo de vida de una vista.

El objeto InsetListener actual de la playerView funciona a la perfección la primera vez que agregas el valor de la inserción de la parte inferior al valor de padding inferior de la app declarado en activity_main.xml. Sin embargo, las llamadas subsiguientes siguen agregando el valor de la inserción de la parte inferior al padding inferior de la vista ya actualizada.

Para solucionar esto, realiza los siguientes pasos:

  1. Graba el valor de padding de la vista inicial. Crea una nueva val y almacena el valor de padding de la vista inicial de playerView, justo antes del código del objeto de escucha.

Revisa la siguiente muestra de código de NowPlayingFragment.kt:

   val initialPadding = playerView.paddingBottom
  1. Usa este valor inicial con el fin de actualizar el padding de la parte inferior de la vista, lo cual te permite que evites el uso del valor actual de padding de la parte inferior de la app.

Revisa la siguiente muestra de código de NowPlayingFragment.kt:

   playerView.setOnApplyWindowInsetsListener { view, insets ->
            view.updatePadding(bottom = insets.systemWindowInsetBottom + initialPadding)
            insets
        }
  1. Vuelve a ejecutar la app. Navega entre apps y ve a la pantalla principal. Cuando vuelvas a la app, los controles del reproductor se ubicarán justo sobre el área de gestos.

Cómo rediseñar los controles de las apps

La barra de búsqueda del reproductor está demasiado cerca del área de gestos inferior, lo que significa que el usuario podría activar sin querer el gesto para ir a la pantalla principal cuando complete un deslizamiento horizontal. Si aumentas aún más el padding, esto podría solucionar el problema, pero quizás también mueva el reproductor a un lugar más arriba del deseado.

El uso de las inserciones te permitirá corregir problemas de gestos, pero, a veces, mediante pequeños cambios de diseño, puedes evitar los conflictos de gestos por completo. Si deseas rediseñar los controles del reproductor a los efectos de evitar conflictos de gestos, realiza los siguientes pasos:

  1. Abre fragment_nowplaying.xml. Cambia a la Vista de diseño y selecciona la SeekBar en la parte inferior:

  1. Cambia a la Vista de código.
  2. Para mover la SeekBar a la parte superior de playerLayout, cambia el valor layout_constraintTop_toBottomOf de la barra de búsqueda a parent.
  3. A fin de limitar otros elementos de la playerView a la parte inferior de la SeekBar, cambia layout_constraintTop_toTopOf del elemento superior a @+id/seekBar en media_button, title y position.

Revisa la siguiente muestra de código de fragment_nowplaying.xml:

<androidx.constraintlayout.widget.ConstraintLayout
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:padding="8dp"
   android:layout_gravity="bottom"
   android:background="@drawable/media_overlay_background"
   android:id="@+id/playerLayout">

   <ImageButton
       android:id="@+id/media_button"
       android:layout_width="@dimen/exo_media_button_width"
       android:layout_height="@dimen/exo_media_button_height"
       android:background="?attr/selectableItemBackground"
       android:scaleType="centerInside"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:srcCompat="@drawable/ic_play_arrow_black_24dp"
       tools:ignore="ContentDescription" />

   <TextView
       android:id="@+id/title"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginTop="8dp"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Title"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:layout_constraintLeft_toRightOf="@id/media_button"
       app:layout_constraintRight_toLeftOf="@id/position"
       tools:text="Song Title" />

   <TextView
       android:id="@+id/subtitle"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
       app:layout_constraintTop_toBottomOf="@+id/title"
       app:layout_constraintLeft_toRightOf="@id/media_button"
       app:layout_constraintRight_toLeftOf="@id/position"
       tools:text="Artist" />

   <TextView
       android:id="@+id/position"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginTop="8dp"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Title"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:layout_constraintRight_toRightOf="parent"
       tools:text="0:00" />

   <TextView
       android:id="@+id/duration"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
       app:layout_constraintTop_toBottomOf="@id/position"
       app:layout_constraintRight_toRightOf="parent"
       tools:text="0:00" />

   <SeekBar
       android:id="@+id/seekBar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. Ejecuta la app e interactúa con el reproductor y la barra de búsqueda.

Estos cambios mínimos de diseño mejoran de forma significativa la app.

Se corrigieron los problemas con los controles del reproductor para conflictos de gestos en el área de gestos de la pantalla principal. El área del gesto de retroceder también podría generar conflictos con los controles de la app. La siguiente captura de pantalla muestra que la barra deslizante del reproductor ahora se ubica tanto en las áreas izquierda y derecha de los gestos:

SeekBar automáticamente controla los conflictos de gestos. También es posible que debas usar otros componentes de la IU que activan conflictos de gestos. En estos casos, puedes usar la Gesture Exclusion API a fin de deshabilitar de forma parcial el gesto de retroceder.

Cómo usar la API de Gesture Exclusion

Si deseas crear un área de exclusión, llama a setSystemGestureExclusionRects() en tu vista con una lista de objetos rect. Estos objetos rect se asignan a las coordenadas de las áreas excluidas de un rectángulo. Esta llamada debe realizarse en los métodos onLayout() o onDraw() de la vista. Para hacerlo, sigue estos pasos:

  1. Crea un nuevo paquete llamado view.
  2. A fin de llamar a esta API, crea una clase nueva llamada MySeekBar y extiende AppCompatSeekBar.

Revisa la siguiente muestra de código de MySeekBar.kt:

class MySeekBar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = android.R.attr.seekBarStyle
) : androidx.appcompat.widget.AppCompatSeekBar(context, attrs, defStyle) {

}
  1. Crea un nuevo método llamado updateGestureExclusion().

Revisa la siguiente muestra de código de MySeekBar.kt:

private fun updateGestureExclusion() {

}
  1. Agrega una validación para omitir esta llamada en el nivel de API 28 o uno anterior.

Revisa la siguiente muestra de código de MySeekBar.kt:

private fun updateGestureExclusion() {
        // Skip this call if we're not running on Android 10+
        if (Build.VERSION.SDK_INT < 29) return
}
  1. Dado que la API de Gesture Exclusion tiene un límite de 200 dp, solo excluye la perilla de la barra de búsqueda. Obtén una copia de los límites de esa barra y agrega cada objeto a una lista mutable.

Revisa la siguiente muestra de código de MySeekBar.kt:

private val gestureExclusionRects = mutableListOf<Rect>()

private fun updateGestureExclusion() {
    // Skip this call if we're not running on Android 10+
    if (Build.VERSION.SDK_INT < 29) return

    thumb?.also { t ->
        gestureExclusionRects += t.copyBounds()
    }
}
  1. Llama a systemGestureExclusionRects() con las listas de gestureExclusionRects que creaste.

Revisa la siguiente muestra de código de MySeekBar.kt:

private val gestureExclusionRects = mutableListOf<Rect>()

private fun updateGestureExclusion() {
    // Skip this call if we're not running on Android 10+
    if (Build.VERSION.SDK_INT < 29) return

    thumb?.also { t ->
        gestureExclusionRects += t.copyBounds()
    }
    // Finally pass our updated list of rectangles to the system
    systemGestureExclusionRects = gestureExclusionRects
}
  1. Llama al método updateGestureExclusion() desde onDraw() o onLayout(). Anula onDraw() y agrega una llamada a updateGestureExclusion.

Revisa la siguiente muestra de código de MySeekBar.kt:

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    updateGestureExclusion()
}
  1. Debes actualizar las referencias a SeekBar. Para comenzar, abre fragment_nowplaying.xml.
  2. Cambia SeekBar por com.example.android.uamp.view.MySeekBar.

Revisa la siguiente muestra de código de fragment_nowplaying.xml:

<com.example.android.uamp.view.MySeekBar
    android:id="@+id/seekBar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="parent" />
  1. A fin de actualizar las referencias a SeekBar en NowPlayingFragment.kt, abre NowPlayingFragment.kt y cambia el tipo de positionSeekBar a MySeekBar. Para que coincida el tipo de variable, cambia la información genérica de SeekBar por la llamada de findViewById a MySeekBar.

Revisa la siguiente muestra de código de NowPlayingFragment.kt:

val positionSeekBar: MySeekBar = view.findViewById<MySeekBar>(
     R.id.seekBar
).apply { progress = 0 }
  1. Ejecuta la app e interactúa con la SeekBar. Si todavía tienes conflictos de gestos, puedes experimentar y modificar los límites de MySeekBar. Ten cuidado de no crear un área de exclusión de gestos más grande de lo necesario, ya que esto limita otras posibles llamadas a exclusiones de gestos y crea un comportamiento inconsistente para el usuario.

¡Felicitaciones! Aprendiste a evitar y resolver conflictos con los gestos del sistema.

Lograste que tu app use la pantalla completa cuando se usa la extensión de borde a borde y empleaste las inserciones para alejar los controles de la app de las áreas de gestos. También aprendiste a inhabilitar el gesto de retroceder del sistema en los controles de la app.

Ahora conoces los pasos principales que se requieren para que tu app funcione con los gestos del sistema.

Materiales adicionales

Documentos de referencia