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.
Completa los siguientes pasos:
- Abre y ejecuta la app en Android Studio.
- 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.
- Ejecuta la app. La lista que ves agrupa las canciones en las selecciones de Recomendaciones y Álbumes.
- Haz clic en Recomendaciones y selecciona una canción de la lista.
- 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:
- Barra de navegación: Abre
res/values-29/styles.xml
y establecenavigationBarColor
encolor/transparent
. - Barra de estado: De forma similar, establece
statusBarColor
encolor/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:
- Abre la clase
MainActivity.kt
y busca el métodoonCreate()
. Obtén una instancia delfragmentContainer
. - Establece lo siguiente en
content.systemUiVisibility
:
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
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:
- Ejecuta la app y selecciona una canción para reproducir de modo que navegues a la pantalla del reproductor.
- 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:
- Navega a la configuración del sistema, cambia al modo de navegación de tres botones y regresa a la app.
- 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. - 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:
- Obtén una instancia de
playerLayout
a partir de la instancia del objeto deview
. - Agrega un objeto
OnApplyWindowInsetsListener
a laplayerView
. - 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
}
- 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.
- Para corregir esto, cambia a
FragmentContainerView
, que controla este problema automáticamente. Abreactivity_main.xml
y cambiaFrameLayout
aFragmentContainerView
.
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"/>
- 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:
- 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
- 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
}
- 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:
- Abre
fragment_nowplaying.xml
. Cambia a la Vista de diseño y selecciona laSeekBar
en la parte inferior:
- Cambia a la Vista de código.
- Para mover la
SeekBar
a la parte superior deplayerLayout
, cambia el valorlayout_constraintTop_toBottomOf
de la barra de búsqueda aparent
. - A fin de limitar otros elementos de la
playerView
a la parte inferior de laSeekBar
, cambialayout_constraintTop_toTopOf
del elemento superior a@+id/seekBar
enmedia_button
,title
yposition
.
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>
- 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:
- Crea un nuevo paquete llamado
view
. - A fin de llamar a esta API, crea una clase nueva llamada
MySeekBar
y extiendeAppCompatSeekBar
.
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) {
}
- Crea un nuevo método llamado
updateGestureExclusion()
.
Revisa la siguiente muestra de código de MySeekBar.kt
:
private fun updateGestureExclusion() {
}
- 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
}
- 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()
}
}
- Llama a
systemGestureExclusionRects()
con las listas degestureExclusionRects
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
}
- Llama al método
updateGestureExclusion()
desdeonDraw()
oonLayout()
. AnulaonDraw()
y agrega una llamada aupdateGestureExclusion
.
Revisa la siguiente muestra de código de MySeekBar.kt
:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
updateGestureExclusion()
}
- Debes actualizar las referencias a
SeekBar
. Para comenzar, abrefragment_nowplaying.xml
. - Cambia
SeekBar
porcom.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" />
- A fin de actualizar las referencias a
SeekBar
enNowPlayingFragment.kt
, abreNowPlayingFragment.kt
y cambia el tipo depositionSeekBar
aMySeekBar
. Para que coincida el tipo de variable, cambia la información genérica deSeekBar
por la llamada defindViewById
aMySeekBar
.
Revisa la siguiente muestra de código de NowPlayingFragment.kt
:
val positionSeekBar: MySeekBar = view.findViewById<MySeekBar>(
R.id.seekBar
).apply { progress = 0 }
- Ejecuta la app e interactúa con la
SeekBar
. Si todavía tienes conflictos de gestos, puedes experimentar y modificar los límites deMySeekBar
. 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
- WindowInsets: Objetos de escucha y diseños
- Navegación por gestos: Implementación de borde a borde
- Navegación por gestos: Manejo de superposiciones visuales
- Navegación por gestos: Manejo de conflictos de gestos
- Cómo garantizar la compatibilidad con la navegación por gestos