Comment Discord a rendu la navigation plus simple sur Android
Anagram • Publié le 24 July 2020
traductionsQuand Discord a commencé en tant qu'application de bureau, la façon la plus rapide d'être disponible sur Android et iOS était d'impliquer la copie des paternes de navigation existants de l'application de bureau.
Voilà une comparaison entre notre application du bureau et notre ancienne application Android qui a été utilisée comme un tiroir de navigation.
Les utilisateurs mobiles ont changé entre les caractéristiques majeures en utilisant un tiroir de navigation, qui permettait aux applications mobiles de quasiment répliquer l'interface de l'application de bureau. Alors que l'ancienne interface de navigation avait bien servi au début de Discord, nous avons réalisé au fil du temps que la navigation de l'application mobile pour accéder aux caractéristiques majeures, comme le "commutateur rapide" (Quick Switcher), est devenue non intuitive et difficile à découvrir pour les nouveaux utilisateurs. Nous avions besoin d'une interface qui montrait plus facilement les caractéristiques primaires de découverte pour les nouveaux utilisateurs pendant que les caractéristiques secondaires leur étaient toujours disponibles.
Pour rendre ces caractéristiques plus compréhensibles, nous nous sommes récemment lancés dans la création de nouveaux onglets pour produits dans notre application Android ! Nous avons trouvé ce projet exaltant, parce qu'en plus de rendre l'application plus simple en termes de navigation, il a présenté une série de challenges techniques qui nous ont amenés à construire une navigation personnalisable en bas de l'interface, et un remplacement des panneaux initiaux pour le tiroir de navigation d'Android.
Pourquoi des onglets ?
Avant que nous nous plongions dans des détails techniques, revoyons pourquoi nous avons construit les Onglets incluant les onglets inférieurs et la navigation des panneaux. Nous les avons construits pour améliorer l'expérience mobile de Discord en 3 façons :
Débloquer de la surface pour les futurs produits. Nous sommes en train d'explorer des idées de produits, comme les caractéristiques pour aider les utilisateurs à découvrir et à rejoindre les communautés avec intérêt, ce que nous aimerions avoir dans les Onglets. Tandis que les tiroirs ont optimisé les caractéristiques actuelles de l'application, ils ont fourni trop de contraintes pour le futur top level des caractéristiques de l'application mobile.
Navigation mobile familière. Les applications Android et iOS utilisent généralement la navigation de l'onglet inférieur afin de rendre les caractéristiques majeures plus compréhensibles.
Avantages UX. Les conversations sur Discord arrivent à plusieurs endroits, et les Onglets aident les utilisateurs à naviguer à travers ces conversations. Avec les nouveaux gestes horizontaux de panneau dans les Onglets, les utilisateurs peuvent maintenant accéder aux serveurs, canaux, et message privés plus facilement !
Et en plus, les mouvements de panneaux permettent à présent aux utilisateurs de jeter un coup d'œil sur leur activité dans la liste des serveurs.
Ouvrant les panneaux latéraux et jetant un coup d'œil à la liste de serveurs avec les mouvements horizontaux.
Les onglets mettent surtout en avant des caractéristiques puissantes comme le "commutateur rapide" qui aide également les utilisateurs à trouver les conversations plus rapidement.
Dans cet enregistrement d'écran, nous naviguons vers un message non lu dans le #coffee-meetups avec un "commutateur rapide" !
Les Onglets aident surtout les utilisateurs pour les mentions non lues et les requêtes d'amis inconnues grâce aux badges de notifications sur l'onglet principal et sur l'onglet des amis.
Le badge de notification est ici pour indiquer que l'utilisateur a une mention non lue et une requête d'ami inconnue.
Contrairement aux tiroirs standards d'Android ayant des conflits avec les mouvements de retour sur Android 10, nos panneaux de l'application mobile sont plus faciles à faire glisser car ils supportent les glissements horizontaux de presque toute la zone de l'écran.
L'enregistrement d'écran sur le côté gauche montre que les mouvements de retour sur Android 10 ont des conflits lorsque l'on fait glisser les tiroirs. L'enregistrement d'écran à droite montre que les mouvements font en sorte que l'ouverture du panneau latéral soit plus simple.
D'un design de perspective, nous voulions que l'application n'ait qu'une composante d'une navigation primaire : les onglets inférieurs. Garder le tiroir avec les onglets inférieurs violait la politique du Design Matériel qui a suggéré de ne pas utiliser les tiroirs et les onglets inférieurs ensemble. Ils étaient tous les deux des composantes de navigation primaire qui ont rendu la hiérarchie des informations ambigüe quand ils sont utilisés ensemble. Parce que le tiroir recouvrait la liste des salons au-dessus des onglets inférieurs, les tiroirs n'indiquaient pas clairement comment les utilisateurs auraient dû avoir la caractéristique du Chat depuis un onglet non principal. En remplaçant les tiroirs des panneaux, nous avons repositionné la liste des canaux pour faire une interface correcte qui représente la hiérarchie de "Accueil -> Canaux -> Chat". Avec ce changement, les onglets inférieurs vont plus clairement représenter la hiérarchie des navigations primaires, pendant que les panneaux représenteront la hiérarchie de la navigation secondaire au sein de l'onglet principal.
Construire la navigation des onglets inférieurs
Quand nous avons créé les onglets de Discord pour Android, nous avons regardé le BottomNavigationView d'Android le long du composant de navigation AndroidX. Sur le papier, ces deux structures semblaient être des solutions qui auraient pu faire des constructions d'onglets beaucoup plus simples. En réalité, nous avons découvert que ces deux structures avaient des limitations qui nous empêchaient de livrer une interface agréable et réactive à nos utilisateurs.
Pour les onglets inférieurs, nous avions pour but de créer une expérience visuelle agréable, en montrant les avatars et leur indicateur de présence dans l'onglet des paramètres de l'utilisateur. Cela nécessitait que nos onglets inférieurs prennent en charge les vues personnalisées en plus des icônes standards. Dès le départ, l'API limitée "BottomNavigationView", qui utilisait des icônes standards d'une ressource de menu Android, ne pouvait pas prendre en charge les comportements d'icônes complexes que nous espérions offrir. Même si le BottomNavigationView supportait l'ajout de vues enfants customisables par programmation, cela n'a pas offert d'API par défaut pour des comportements d'icônes complexes comme ajuster les valeurs alpha pour indiquer le stade de sélection (par exemple pour l'avatar de l'utilisateur). Cela n'a également pas supporté automatiquement le fait de montrer la vue des enfants customisables dans les plans d'Android Studio duquel nous dépendions afin de nous aider à développer rapidement l'interface. Au lieu d'utiliser le BottomNavigationView, nous avons construit une solution personnalisable simple avec un LinearLayout où l'enfant ImageViews et les vues personnalisables ont été représentés sur chaque onglet. Notre solution personnalisable a supporté les comportements des icônes complexes et les avant-premières des plans d'Android Studio.
Pour la navigation des onglets, nous avons initialement essayé la composante de navigation AndroidX parce que nous aimions la simplicité de leur Navigateur et leur Graphique de Navigation d'API. Malheureusement, quand nous avons commencé à construire la navigation des onglets, la composante de la navigation d'AndroidX a eu une limite majeure : la composante de navigation de base a créé un nouveau fragment sur chaque sélection d'onglet, naviguant entre les onglets lentement. Pour créer une sélection rapide d'onglets, nous avons recherché toutes les façons de garder les fragments en mémoires (par exemple en créant la composante de navigateur personnalisable et en utilisant les méthodes d'extension de l'échantillon de composant d'architecture Android), mais nous n'avons pas trouvé de solution satisfaisante au final.
Le Navigateur personnalisable présentait trop de risques pour nous car les solutions proposées impliquaient de complètement remplacer la méthode parent, FragmentNavigator#navigate(), sans qu'elle ne soit jamais appelée.
Cela signifiait que les mises à jour du FragmentNavigator
et d'autres parties du composant de navigation pouvaient facilement interrompre notre implémentation.
Pour la seconde solution, les méthodes d'extension de l'échantillon appliquées spécifiquement au BottomNavigationView
d'Android, et les méthodes géraient le cycle de vie des fragments d'onglets. Parce que nous avons eu à utiliser une vue personnalisable pour nos onglets inférieurs, réutiliser la logique dans ces méthodes d'extension aurait nécessité l'écriture d'une solution personnalisable dans tous les cas.
Puisque qu'aucune de ces deux solutions ci-dessus n'a facilité la création d'onglet réactifs, nous avons décidé d'y aller avec une solution simple d'onglets personnalisables sans utiliser de composante de Navigation : garder tous les fragments d'onglets dans la mémoire à l'intérieur d'un hébergeur de fragments. Pour activer la sélection d'onglets rapide, nous avons simplement basculé la visibilité des fragments de l'onglet sans avoir besoin de créer un nouveau fragment à chaque fois que l'utilisateur sélectionne un onglet.
L'enregistrement de l'écran de gauche montre la sélection des onglets avec la composante de Navigation, et l'enregistrement de l'écran de droite montre la sélection d'onglets via le basculement de visibilité pour les fragments gardés en mémoire. La composante de Navigation de l'onglet est plus lente car elle doit créer chaque fragment d'onglet.
Pour nous aider à éviter les requêtes de réseau gaspillées, nous avons fait en sorte que chaque fragment d'onglet prenne en compte l'état de sélection de l'onglet. L'onglet Mentions récentes, par exemple, utilise cet état pour récupérer uniquement les mentions du réseau lorsqu'il s'agit de l'onglet sélectionné.
Ouvrir la Porte pour de Meilleurs UX avec des Panneaux
Dans une des premières versions de Discord avec Onglets, nous avons essayé de combiner l'interface des tiroirs et l'interface des onglets inférieurs ensemble. Nous savions que nous ne pouvions pas les assembler ensemble car cela créait deux composantes de navigations primaires simultanément, mais nous avons apprécié les animations par défaut de la disposition des tiroirs, et leur facilité d'utilisation. Pour remplacer le DrawerLayout
, nous avons eu pour but de construire une vue personnalisable appelée OverlappingPanelsLayout
avec des animations tout aussi soignées et des API faciles d'utilisation. Nous avons initialement expérimenté les animations basées sur la physique. Bien qu'elles supportent les animations rapides pour les mouvements rapides, elles ont fait des animations lentes pour un mouvement qui est en moyenne de vélocité lente. Nous ne pouvions pas améliorer la vitesse minimum car cela aurait rendu les panneaux trop compliqués à ouvrir. Au lieu d'utiliser des animations basées sur la physique, nous avons éventuellement décidé d'adopter les interpolations simples et les durées fixées recommandés par la politique des Design de Matériel. Suivre ces politiques a rendu l'ouverture et la fermeture des panneaux plus réactifs et rapides.
Pour faire en sorte que le OverlappingPanelsLayout
de l'API soit facile à utiliser, nous avons fait de la vue personnalisée un FrameLayout qui attendait exactement trois vues enfants. Les vues enfants représentent les panneaux de gauche, centre et droite. Dans notre code, nous avons apporté les vues enfants en les nichant dans le OverlappingPanelsLayout
via l'XML.
En plus de supporter les animations soignées et l'utilisation simple des API, construire une vue personnalisable nous a permis de créer une manipulation de clic flexible et de défilement. Contrairement au DrawerLayout
qui permettait uniquement de faire des mouvements de glisser-ouvrir depuis le bord de l'écran, le OverlappingPanelsLayout
a offert de multiples mouvements pour facilement ouvrir les panneaux latéraux : défilements horizontaux sur la majeure partie de l'écran et également des clics sur le panneau central fermé.
Cet enregistrement d'écran démontre le clic sur le panneau central fermé pour l'ouvrir.
Avec ses caractéristiques et son API flexible, le OverlappingPanelsLayout
a évolué en un excellent remplacement pour le DrawerLayout
. Avant d'atteindre cet état final, cependant, nous avons d'abord dû surmonter de nombreux défis techniques tels que la création d'une détection de mouvement de panneau personnalisée, l'autorisation de défilement horizontaux dans les vues enfants et le maintien des états des panneaux lors des rotations de l'appareil.
Construire la Détection de Mouvement de Panneau Personnalisable
D'abord, nous avons essayé de supporter le fait de tirer le panneau central. Cet article de StackOverflow nous a donné une idée de la façon de déplacer le panneau central en réponse aux mouvements de glissement. Cependant, copier le code dans la méthode onTouchEvent
du OverlappingPanelsLayout
ne fonctionnait pas à l'origine. Les vues enfants du OverlappingPanelsLayout
géraient leurs propres événements tactiles tels que les clics et les défilements verticaux, de sorte que le OverlappingPanelsLayout [ViewGroup](https://developer.android.com/reference/android/view/ViewGroup)
ne gérait pas initialement ces événements tactiles. Heureusement, les recherches Google nous ont conduits à la documentation du développeur Android pour la gestion des événements tactiles dans ViewGroups.
Sur la base des leçons tirées de ces documents pour développeurs Android, nous avons décidé de gérer les mouvements de glissement dans le OverlappingPanelsLayout
à l'aide des API onInterceptTouchEvent et onTouchEvent d'Android. onInterceptTouchEvent
décide quand le OverlappingPanelsLayout ViewGroup
doit gérer l'événement tactile et quand il doit autoriser les vues enfants à gérer les événements tactiles. OverlappingPanelsLayout gère la plupart des défilements horizontaux tout en permettant à ses vues enfants de gérer les défilements verticaux (par exemple, pour faire défiler verticalement la liste de discussion ou la liste des canaux). Cet exemple de code de notre classe OverlappingPanelsLayout montre comment nous détectons les mouvements.
Premièrement, nous détectons les défilements horizontaux en vérifiant que la différence entre les valeurs x est plus grande que la distance entre les valeurs y. Ensuite, nous interceptons ces MotionEvents de défilement horizontal, afin que onTouchEvent
puisse les gérer. onTouchEvent
gère les événements ACTION_MOVE en faisant glisser le panneau, et gère les événements ACTION_UP en alignant le panneau central sur un état fermé ou ouvert. En utilisant la vitesse de mouvement mesurée par le VelocityTracker, snapOpenOrClose
aligne le panneau central sur le bord le plus proche pour les mouvements lents ou lance le panneau central dans la direction du geste si la vitesse dépasse la vitesse de projection minimale.
Permettre les Défilements Horizontaux dans les Vues Enfants
Avant que nous nous lançions dans les onglets inférieurs et l'interface des panneaux pour le public, nous avons demandé aux membres des serveurs de Testeurs de Discord nous aider à trouver les bugs. Les testeurs de Discord ont rapidement identifié un bug où les utilisateurs ne pouvaient plus faire défiler horizontalement le sélecteur des catégories d'émojis dans l'entrée du chat avec l'interface des panneaux.
Nous voulions que les panneaux continuent à gérer tous les mouvements de balayage horizontal comme comportement par défaut. Par conséquent, notre correction de bug pour le bug du sélecteur de catégorie émoji devait impliquer une nouvelle API qui permettrait à l'application d'enregistrer sélectivement les régions de mouvements enfants dans lesquelles les panneaux ne devraient pas gérer les balayages. Pour désactiver les mouvements du panneau sur les vues enfants sélectionnées, nous avons créé une classe appelée PanelsChildGestureRegionObserver
qui a aidé à détecter les régions de mouvements enfants et à informer le OverlappingPanelsLayout
de ces régions de mouvements.
Sélectivement en train de défiler horizontalement dans le sélecteur des catégories d'émoji au sein des panneaux.
Dans cet exemple de code pour enregistrer les régions de mouvements enfants, le ChatInputFragmentest
un enfant du OverlappingPanelsLayout
. Le fragment parent contenant le OverlappingPanelsLayout
, PanelsFragment
, écoute les mises à jour de la région de mouvement et les transmet au OverlappingPanelsLayout
via setChildGestureRegions
. OverlappingPanelsLayout
ne gère alors pas les défilements horizontaux dans ces régions. En gardant OverlappingPanelsLayout ignorant quelles fonctionnalités ont des régions de mouvements enfants, cette API facilite la désactivation des gestes du panneau sur plusieurs vues enfants, y compris d'autres cas d'utilisation comme le bouton push-to-talk de Discord dans le panneau central qui nécessite d'être maintenu enfoncé sans déplacer le panneau.
Conservation des États de Panneau lors des Rotations de Périphériques
Comme d'autres applications Android modernes, l'application Discord sur Android a pour but de maintenir le statut de vue à travers les rotations de l'appareil. Nous avons initialement pensé que la composante d'architecture ViewModel de AndroidX pourrait totalement régler le problème pour maintenir le statut des panneaux à travers les rotations. Lorsque le fragment parent de la vue du OverlappingPanelsLayout
a repris après une rotation de périphérique, le fragment a récupéré l'état du panneau de pré-rotation à partir de son modèle de vue correspondant et a essayé de mettre à jour l'état sur la vue du OverlappingPanelsLayout recréée
. Malheureusement, cette solution initialement simple a provoqué le blocage de l'application après la rotation des appareils. Le modèle de vue stockait correctement l'état lors des rotations de périphériques, mais notre application essayait d'appliquer la mise à jour de l'état au OverlappingPanelsLayout
trop tôt. Nous avons constaté que lorsque le fragment a tenté d'appliquer cette mise à jour après la rotation, la vue enfant, le OverlappingPanelsLayout
, n'avait pas encore terminé sa mise en page dans le processus de dessin de vue. Le OverlappingPanelsLayout
a attribué ses références de panneau à partir des vues enfants dans onLayout
, de sorte qu'il ne pouvait pas appliquer les changements d'état du panneau avant onLayout
. Pour résoudre la contrainte posée par la passe de mise en page se produisant après la mise à jour de l'état du panneau, nous avons capturé la mise à jour de pré-mise en page en tant que lambda Kotlin appelé pendingUpdate
. Nous avons ensuite appelé le pendingUpdate
dans onLayout
après avoir défini les références du panneau enfant. Nous avons envisagé de créer une file d'attente de mises à jour à venir. Cependant, nous avons réalisé que seule la dernière mise à jour de l'état comptait dans notre cas d'utilisation des panneaux, nous nous sommes donc contentés de la solution simple consistant à enregistrer une seule mise à jour en attente plutôt qu'une file d'attente de mises à jour. Nous avons partagé un exemple de code pour appliquer les mises à jour de vue en attente ici.
Voilà un exemple du panneau gauche restant ouvert à travers les rotations de l'appareil.
Améliorer Continuellement les Applications et Ravir les Utilisateurs
Alors que de plus en plus de développeurs d'applications mettent à jour les applications basées sur la navigation dans les tiroirs pour gérer la navigation par gestes "retour" du système Android 10, nous espérons que les leçons de Discord tirées de la création des onglets et des panneaux inférieurs vous aideront tous à créer une interface utilisateur agréable et facile à naviguer ! Nous avons ouvert notre bibliothèque de panneaux dans notre nouveau référentiel GitHub de Panneaux Superposés, et nous avons inclus un exemple d'application pour vous aider à démarrer. N'hésitez pas à essayer les panneaux qui se chevauchent dans vos applications et dites-nous comment cela se passe !
Les équipes de produits et d'ingénierie de Discord travaillent constamment à rendre nos applications utiles et agréables pour tous nos clients pris en charge : Android, iOS, Desktop et Web. Si vous souhaitez nous aider à rassembler les gens sur Discord en développant les fonctionnalités du produit ou en améliorant l'infrastructure, faites-le nous savoir en postulant à nos offres d'emploi ici !
Traduction de l'article « How Discord Made Android In-App Navigation Easier » écrit par Donald Chen le 16 juillet 2020, ingénieur d'applications Android, par Anagram.