Navigation par gestes et expérience bord à bord

Les appareils Android version 10 ou ultérieure disposent d'un nouveau mode de navigation par gestes qui permet à votre application d'utiliser tout l'écran et offre une expérience visuelle plus immersive. Ainsi, lorsque l'utilisateur balaie l'écran de bas en haut, il accède à l'écran d'accueil Android. Et lorsqu'il balaie l'écran à partir du bord gauche ou droit, il accède à l'écran précédent.

Grâce à ces deux gestes, votre application peut occuper l'espace disponible en bas de l'écran. Par contre, si elle utilise des gestes spécifiques ou des commandes dans les zones réservées aux gestes système, cela peut créer des conflits avec ceux définis au niveau du système.

Dans cet atelier de programmation, vous allez apprendre à utiliser des encarts afin d'éviter les conflits de gestes. Vous verrez également comment ajouter des commandes (par exemple, des poignées de déplacement) dans les zones de gestes avec l'API Gesture Exclusion.

Points abordés

  • Utiliser des écouteurs d'encarts dans les vues
  • Utiliser l'API Gesture Exclusion
  • Comprendre le comportement du mode immersif avec les gestes activés

Le but de cet atelier de programmation est de rendre votre application compatible avec les gestes système. Les concepts et les blocs de codes non pertinents ne sont pas abordés. Ils vous sont fournis afin que vous puissiez simplement les copier et les coller.

Objectifs de l'atelier

Le lecteur de musique UAMP (Universal Android Music Player) est une application exemple pour Android développée en Kotlin. Vous allez le configurer pour la navigation par gestes.

  • Éloigner les commandes des zones de gestes au moyen d'encarts
  • Désactiver le geste retour pour les commandes en conflit à l'aide de l'API Gesture Exclusion
  • Explorer le changement de comportement du mode immersif avec la navigation par gestes en utilisant vos builds

Prérequis

  • Un appareil ou un émulateur exécutant Android version 10 ou ultérieure
  • Android Studio

Le lecteur de musique UAMP (Universal Android Music Player) est une application exemple pour Android développée en Kotlin. Compatible avec diverses plates-formes, dont Wear, TV et Auto, cette application propose entre autres des fonctionnalités de lecture en arrière-plan, de gestion de la priorité audio et d'intégration avec l'Assistant.

Figure 1 : Un parcours utilisateur dans UAMP

L'utilisateur d'UAMP peut parcourir les titres et les albums dans un catalogue chargé depuis un serveur distant. Lorsqu'il appuie sur une chanson, elle est diffusée via son casque ou son enceinte connectés. L'application n'est pas conçue pour fonctionner avec les gestes système. Par conséquent, cela explique les problèmes initialement rencontrés sur un appareil exécutant Android version 10 ou ultérieure.

Pour obtenir l'application exemple, clonez le dépôt sur GitHub et passez à la branche starter :

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


Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.

Télécharger le fichier ZIP

Procédez comme suit :

  1. Ouvrez et créez l'application dans Android Studio.
  2. Créez un appareil virtuel, puis sélectionnez Niveau d'API 29. Vous pouvez également associer un appareil réel exécutant le niveau d'API 29 ou ultérieur.
  3. Exécutez l'application. Vous accédez à une liste de titres classés dans deux catégories : Recommandations et Albums.
  4. Cliquez sur Recommandations, puis sélectionnez un titre dans la liste.
  5. L'application lit le titre.

Activer la navigation par gestes

Si vous utilisez une nouvelle instance d'émulateur avec le niveau d'API 29, la navigation par gestes peut ne pas être activée par défaut. Pour l'activer, sélectionnez Paramètres système > Système > Navigation système > Navigation par gestes.

Exécuter l'application avec la navigation par gestes

Si vous exécutez l'application alors que la navigation par gestes est activée et que vous lancez la lecture d'un titre, vous remarquerez que les commandes du lecteur s'affichent tout près des zones réservées aux gestes accueil et retour.

Qu'est-ce que l'expérience bord à bord ?

Les applications exécutées sur Android version 10 ou ultérieure peuvent s'afficher sur un écran bord à bord, que des gestes ou des boutons de navigation aient été activés ou non. Pour offrir cette expérience bord à bord, vos applications doivent passer derrière les barres transparentes d'état et de navigation.

Faire passer l'application derrière la barre de navigation

Pour que le contenu de votre application soit visible derrière la barre de navigation, vous devez d'abord rendre transparent l'arrière-plan de la barre de navigation, puis faire de même pour la barre d'état. Votre application pourra ainsi s'afficher sur toute la hauteur de l'écran.

Pour changer la couleur des barres d'état et de navigation, procédez comme suit :

  1. Barre de navigation : ouvrez res/values-29/styles.xml et définissez navigationBarColor sur color/transparent.
  2. Barre d'état : de la même manière, définissez statusBarColor sur color/transparent.

Examinez l'exemple de code suivant, issu du fichier 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>

Indicateurs de visibilité de l'UI du système

Vous devez également configurer les indicateurs de visibilité de l'UI du système de sorte que l'application s'affiche au-dessous des barres système. Les API systemUiVisibility de la classe View permettent de définir différents indicateurs. Procédez comme suit :

  1. Ouvrez la classe MainActivity.kt et recherchez la méthode onCreate(). Obtenez une instance de fragmentContainer.
  2. Définissez les indicateurs suivants sur content.systemUiVisibility :

Examinez l'exemple de code suivant, issu du fichier 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

Définir ces indicateurs ensemble permet de demander au système d'afficher l'application en plein écran, comme si les barres d'état et de navigation n'existaient pas. Procédez comme suit :

  1. Exécutez l'application, puis sélectionnez un titre à lire afin d'accéder à l'écran du lecteur.
  2. Vérifiez que les commandes du lecteur sont passées derrière la barre de navigation, ce qui les rend difficilement accessibles :

  1. Accédez aux paramètres système, réactivez le mode de navigation à trois boutons, puis revenez dans l'application.
  2. Vérifiez que les commandes sont encore plus difficiles à utiliser avec la barre de navigation à trois boutons : la barre cache l'élément SeekBar et recouvre presque entièrement la commande Lecture/Pause.
  3. Explorez l'application et testez-la un peu. Réactivez ensuite la navigation par gestes dans les paramètres système :

L'application s'affiche maintenant bord à bord, mais il reste à résoudre des problèmes d'ergonomie (des commandes sont en conflit et se chevauchent).

WindowInsets permet d'indiquer à l'application où l'UI du système recouvre le contenu ainsi que les zones de l'écran où les gestes système sont prioritaires sur ceux de l'application. Les encarts sont représentés par les classes WindowInsets et WindowInsetsCompat dans Jetpack. Nous vous recommandons vivement de vous servir de WindowInsetsCompat pour que les utilisateurs bénéficient de la même expérience quel que soit le niveau d'API.

Encarts système et encarts système obligatoires

Les API d'encart suivants correspondent aux types d'encarts les plus souvent utilisés :

  • Encarts de fenêtre système : ils indiquent où l'UI du système recouvre l'application. Nous verrons ci-dessous comment vous pouvez les utiliser pour éloigner vos commandes des barres système.
  • Encarts de geste système : ils renvoient toutes les zones de gestes. Les commandes de balayage de l'application qui se trouvent dans ces zones peuvent déclencher malencontreusement des gestes système.
  • Encarts de geste obligatoires : ce sous-ensemble d'encarts de geste système ne peut pas être ignoré. Ils indiquent les zones de l'écran où les gestes système seront toujours prioritaires sur ceux de l'application.

Éloigner les commandes de l'application à l'aide d'encarts

Maintenant que vous en savez davantage sur les API d'encart, vous pouvez résoudre les problèmes liés aux commandes de l'application en procédant comme suit :

  1. Obtenez une instance de playerLayout via l'instance d'objet view.
  2. Ajoutez un OnApplyWindowInsetsListener au playerView.
  3. Éloignez la vue de la zone de gestes : déterminez la valeur de l'encart système correspondant au bas de l'écran et augmentez d'autant la marge intérieure de la vue. Pour modifier la marge intérieure de la vue en fonction de la [valeur associée à la marge inférieure de l'application], ajoutez la [valeur correspondant au bas de l'encart système].

Examinez l'exemple de code suivant, issu du fichier NowPlayingFragment.kt :

playerView = view.findViewById(R.id.playerLayout)
playerView.setOnApplyWindowInsetsListener { view, insets ->
   view.updatePadding(
      bottom = insets.systemWindowInsetBottom + view.paddingBottom
   )
   insets
}
  1. Exécutez l'application, puis sélectionnez un titre. Aucun changement n'est visible au niveau des commandes du lecteur. Si vous ajoutez un point d'arrêt et que vous exécutez l'application en mode débogage, l'écouteur n'est pas appelé.
  2. Pour résoudre automatiquement ce problème, utilisez FragmentContainerView. Ouvrez activity_main.xml et remplacez FrameLayout par FragmentContainerView.

Examinez l'exemple de code suivant, issu du fichier 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. Exécutez de nouveau l'application, puis accédez à l'écran du lecteur. Les commandes de lecteur en bas de l'écran sont décalées par rapport à la zone de gestes inférieure.

Maintenant, les commandes de l'application fonctionnent avec la navigation par gestes. Par contre, elles se décalent plus que prévu. Vous devez résoudre ce problème.

Conserver les marges actuelles

Lorsque vous passez à d'autres applications ou accédez à l'écran d'accueil, puis revenez dans l'application encore ouverte, les commandes du lecteur se décalent de plus en plus vers le haut.

En effet, l'application déclenche requestApplyInsets() chaque fois que l'activité démarre. Même sans cet appel, WindowInsets peut se déclencher plusieurs fois au cours du cycle de vie d'une vue.

L'InsetListener actuel de la playerView fonctionne parfaitement la première fois que vous ajoutez la valeur correspondant au bas de l'encart à la marge inférieure de l'application déclarée dans activity_main.xml. Cependant, cette valeur continue d'être ajoutée lors de chaque appel ultérieur, alors que la marge inférieure de la vue a déjà été mise à jour.

Pour résoudre ce problème, procédez comme suit :

  1. Enregistrez la valeur initiale de la marge intérieure de la vue. Créez une valeur et stockez la valeur initiale de la marge intérieure de la vue playerView juste avant le code de l'écouteur.

Examinez l'exemple de code suivant, issu du fichier NowPlayingFragment.kt :

   val initialPadding = playerView.paddingBottom
  1. Utilisez cette valeur initiale pour mettre à jour la marge inférieure de la vue et éviter ainsi d'appliquer la marge inférieure actuelle de l'application.

Examinez l'exemple de code suivant, issu du fichier NowPlayingFragment.kt :

   playerView.setOnApplyWindowInsetsListener { view, insets ->
            view.updatePadding(bottom = insets.systemWindowInsetBottom + initialPadding)
            insets
        }
  1. Exécutez à nouveau l'application. Naviguez entre les applications et accédez à l'écran d'accueil. Lorsque vous revenez dans l'application, les commandes du lecteur s'affichent au bon endroit, juste au-dessus de la zone de gestes.

Modifier visuellement les commandes de l'application

La barre de recherche du lecteur est trop proche de la zone de gestes en bas de l'écran. Par conséquent, en balayant l'écran vers la gauche ou vers la droite, l'utilisateur risque de déclencher par inadvertance le geste accueil. Augmenter davantage la marge intérieure peut résoudre le problème, mais cela risque aussi de décaler le lecteur plus haut que prévu.

Si les encarts permettent de régler les conflits de gestes, il est parfois possible d'éviter ce type de complications en effectuant de petites modifications visuelles. Pour changer l'apparence des commandes du lecteur afin d'éviter les conflits de gestes, procédez comme suit :

  1. Ouvrez fragment_nowplaying.xml. Passez à la vue Conception, puis sélectionnez la SeekBar tout en bas de l'écran :

  1. Passez à la vue Code.
  2. Pour déplacer la SeekBar en haut de playerLayout, définissez le paramètre layout_constraintTop_toBottomOf de la barre de recherche sur parent.
  3. Pour que les autres éléments de la playerView restent en bas de la SeekBar, remplacez la valeur "parent" du paramètre layout_constraintTop_toTopOf par @+id/seekBar pour media_button, title et position.

Examinez l'exemple de code suivant, issu du fichier 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. Exécutez l'application, puis interagissez avec le lecteur et la barre de recherche.

Ces modifications visuelles minimes améliorent considérablement l'application.

Les conflits de gestes entre les commandes du lecteur et la zone du geste accueil sont maintenant résolus. Toutefois, la zone du geste retour peut également créer des conflits avec les commandes de l'application. Sur la capture d'écran suivante, vous pouvez voir que la barre de recherche du lecteur s'étend jusqu'aux zones réservées à ce geste, à gauche et à droite :

SeekBar gère automatiquement les conflits de gestes. Cependant, vous pouvez avoir besoin d'autres composants de l'UI qui risquent de déclencher ce type de conflits. Dans ce cas, Gesture Exclusion API vous permet de désactiver partiellement le geste retour.

Utiliser l'API Gesture Exclusion

Pour créer une zone d'exclusion des gestes, appelez setSystemGestureExclusionRects() sur la vue avec une liste d'objets rect. Ces objets rect sont mappés aux coordonnées des zones rectangulaires exclues. L'appel doit être ajouté dans les méthodes onLayout() ou onDraw() de la vue. Pour ce faire, procédez comme suit :

  1. Créez un package nommé view.
  2. Pour appeler cette API, créez une classe intitulée MySeekBar et étendez AppCompatSeekBar.

Examinez l'exemple de code suivant, issu du fichier 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. Créez une méthode appelée updateGestureExclusion().

Examinez l'exemple de code suivant, issu du fichier MySeekBar.kt :

private fun updateGestureExclusion() {

}
  1. Ajoutez une condition afin d'ignorer cet appel pour le niveau d'API version 28 ou antérieure.

Examinez l'exemple de code suivant, issu du fichier MySeekBar.kt :

private fun updateGestureExclusion() {
        // Skip this call if we're not running on Android 10+
        if (Build.VERSION.SDK_INT < 29) return
}
  1. Comme l'API Gesture Exclusion fixe une limite de 200 dp, excluez seulement le curseur de la barre de recherche. Obtenez une copie des limites de la barre de recherche et ajoutez chaque objet à une liste modifiable.

Examinez l'exemple de code suivant, issu du fichier 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. Appelez systemGestureExclusionRects() avec les listes gestureExclusionRects que vous avez créées.

Examinez l'exemple de code suivant, issu du fichier 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. Appelez la méthode updateGestureExclusion() de onDraw() ou onLayout(). Remplacez onDraw() et ajoutez un appel à updateGestureExclusion.

Examinez l'exemple de code suivant, issu du fichier MySeekBar.kt :

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    updateGestureExclusion()
}
  1. Vous devez mettre à jour les références SeekBar. Commencez par ouvrir fragment_nowplaying.xml.
  2. Remplacez SeekBar par com.example.android.uamp.view.MySeekBar.

Examinez l'exemple de code suivant, issu du fichier 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. Pour mettre à jour les références SeekBar dans NowPlayingFragment.kt, ouvrez NowPlayingFragment.kt, puis remplacez le type de positionSeekBar par MySeekBar. Pour faire correspondre le type de variable, remplacez les éléments génériques SeekBar de l'appel findViewById par MySeekBar.

Examinez l'exemple de code suivant, issu du fichier NowPlayingFragment.kt :

val positionSeekBar: MySeekBar = view.findViewById<MySeekBar>(
     R.id.seekBar
).apply { progress = 0 }
  1. Exécutez l'application, puis interagissez avec la SeekBar. Si les conflits de gestes persistent, vous pouvez essayer de modifier les limites du curseur dans MySeekBar. Attention : la zone d'exclusion des gestes ne doit pas être trop grande. Sinon, cela risque de limiter les autres appels d'exclusion de gestes et de créer une expérience incohérente pour l'utilisateur.

Félicitations ! Vous savez à présent éviter et résoudre les conflits liés aux gestes système.

Votre application occupe tout l'écran maintenant que vous en avez étendu l'affichage de bord à bord et éloigné ses commandes des zones de gestes à l'aide d'encarts. Vous avez également appris à désactiver le geste retour système sur les commandes de l'application.

Vous connaissez désormais les principales étapes nécessaires pour rendre vos applications compatibles avec les gestes système.

Autres ressources

Documents de référence