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.
Procédez comme suit :
- Ouvrez et créez l'application dans Android Studio.
- 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.
- Exécutez l'application. Vous accédez à une liste de titres classés dans deux catégories : Recommandations et Albums.
- Cliquez sur Recommandations, puis sélectionnez un titre dans la liste.
- 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 :
- Barre de navigation : ouvrez
res/values-29/styles.xml
et définisseznavigationBarColor
surcolor/transparent
. - Barre d'état : de la même manière, définissez
statusBarColor
surcolor/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 :
- Ouvrez la classe
MainActivity.kt
et recherchez la méthodeonCreate()
. Obtenez une instance defragmentContainer
. - Définissez les indicateurs suivants sur
content.systemUiVisibility
:
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
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 :
- Exécutez l'application, puis sélectionnez un titre à lire afin d'accéder à l'écran du lecteur.
- Vérifiez que les commandes du lecteur sont passées derrière la barre de navigation, ce qui les rend difficilement accessibles :
- Accédez aux paramètres système, réactivez le mode de navigation à trois boutons, puis revenez dans l'application.
- 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. - 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 :
- Obtenez une instance de
playerLayout
via l'instance d'objetview
. - Ajoutez un
OnApplyWindowInsetsListener
auplayerView
. - É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
}
- 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é.
- Pour résoudre automatiquement ce problème, utilisez
FragmentContainerView
. Ouvrezactivity_main.xml
et remplacezFrameLayout
parFragmentContainerView
.
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"/>
- 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 :
- 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
- 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
}
- 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 :
- Ouvrez
fragment_nowplaying.xml
. Passez à la vue Conception, puis sélectionnez laSeekBar
tout en bas de l'écran :
- Passez à la vue Code.
- Pour déplacer la
SeekBar
en haut deplayerLayout
, définissez le paramètrelayout_constraintTop_toBottomOf
de la barre de recherche surparent
. - Pour que les autres éléments de la
playerView
restent en bas de laSeekBar
, remplacez la valeur "parent" du paramètrelayout_constraintTop_toTopOf
par@+id/seekBar
pourmedia_button
,title
etposition
.
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>
- 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 :
- Créez un package nommé
view
. - Pour appeler cette API, créez une classe intitulée
MySeekBar
et étendezAppCompatSeekBar
.
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) {
}
- Créez une méthode appelée
updateGestureExclusion()
.
Examinez l'exemple de code suivant, issu du fichier MySeekBar.kt
:
private fun updateGestureExclusion() {
}
- 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
}
- 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()
}
}
- Appelez
systemGestureExclusionRects()
avec les listesgestureExclusionRects
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
}
- Appelez la méthode
updateGestureExclusion()
deonDraw()
ouonLayout()
. RemplacezonDraw()
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()
}
- Vous devez mettre à jour les références
SeekBar
. Commencez par ouvrirfragment_nowplaying.xml
. - Remplacez
SeekBar
parcom.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" />
- Pour mettre à jour les références
SeekBar
dansNowPlayingFragment.kt
, ouvrezNowPlayingFragment.kt
, puis remplacez le type depositionSeekBar
parMySeekBar
. Pour faire correspondre le type de variable, remplacez les éléments génériquesSeekBar
de l'appelfindViewById
parMySeekBar
.
Examinez l'exemple de code suivant, issu du fichier NowPlayingFragment.kt
:
val positionSeekBar: MySeekBar = view.findViewById<MySeekBar>(
R.id.seekBar
).apply { progress = 0 }
- Exécutez l'application, puis interagissez avec la
SeekBar
. Si les conflits de gestes persistent, vous pouvez essayer de modifier les limites du curseur dansMySeekBar
. 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
- WindowInsets — Listeners to layouts (Encarts Windows : des écouteurs pour les mises en page)
- Gesture Navigation: going edge-to-edge (Navigation par gestes : offrir une expérience bord à bord)
- Gesture Navigation: handling visual overlaps (Navigation par gestes : gérer le chevauchement visuel)
- Gesture Navigation: handling gesture conflicts (Navigation par gestes : gérer les conflits de gestes)
- Ensure compatibility with gesture navigation (Assurer la compatibilité avec la navigation par gestes)