Encarts Android: gérer les peurs et se préparer pour Android Q

Android Q est la dixième version d'Android avec le niveau d'API 29. L'une des principales idées de la nouvelle version est le concept de bord à bord, lorsque les applications occupent tout l'écran, de bas en haut. Cela signifie que la barre d'état et la barre de navigation doivent être transparentes. Mais, s'ils sont transparents, il n'y a pas d'interface utilisateur système - elle chevauche les composants interactifs de l'application. Ce problème est résolu avec des encarts.

Les développeurs mobiles évitent les encarts, ils leur font peur. Mais dans Android Q, il ne sera pas possible de contourner les encarts - vous devrez les étudier et les appliquer. En fait, les encarts n'ont rien de compliqué: ils montrent quels éléments d'écran se croisent avec l'interface système et suggèrent comment déplacer l'élément afin qu'il n'entre pas en conflit avec l'interface utilisateur du système. Konstantin Tskhovrebov expliquera le fonctionnement des encarts et leur utilité.


Konstantin Tskhovrebov (terrakok) travaille à Redmadrobot . A été impliqué dans Android pendant 10 ans et a accumulé beaucoup d'expérience dans divers projets dans lesquels il n'y avait pas de place pour les encarts, ils ont toujours réussi à se déplacer d'une manière ou d'une autre. Konstantin racontera une longue histoire d'évitement du problème des encarts, d'étudier et de combattre Android. Il examinera les tâches typiques de son expérience dans lesquelles des encarts pourraient être appliqués et montrera comment arrêter d'avoir peur du clavier, reconnaître sa taille et répondre à l'apparence.

Remarque. L'article a été écrit sur la base d'un rapport de Konstantin à Saint AppsConf 2019 . Le rapport utilise des documents de plusieurs articles sur des encarts. Lien vers ces documents à la fin.

Tâches typiques


Barre d'état des couleurs. Dans de nombreux projets, le concepteur dessine une barre d'état couleur. Il est devenu à la mode lorsque Android 5 est venu avec le nouveau Material Design.



Comment peindre la barre d'état? Élémentaire - ajoutez colorPrimaryDarket la couleur est substituée.

 <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="colorPrimaryDark">@color/colorAccent</item>
    ...
</style>

Pour Android au-dessus de la cinquième version (API à partir de 21 et plus), vous pouvez définir des paramètres spéciaux dans la rubrique:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="android:statusBarColor">@color/colorAccent</item>
    ...
</style>

Barre d'état multicolore . Parfois, les concepteurs sur différents écrans dessinent la barre d'état de différentes couleurs.



Ce n'est pas grave, la façon la plus simple de travailler est avec différents sujets dans différentes activités .

La manière la plus intéressante est de changer les couleurs directement lors de l'exécution .

override fun onCreateView(...): View {
    requireActivity().window.statusBarColor = requireContext().getColor(R.color.colorPrimary)
    ...
}

Le paramètre est modifié via un drapeau spécial. Mais l'essentiel est de ne pas oublier de changer la couleur en arrière lorsque l'utilisateur quitte l'écran en arrière.

Barre d'état transparente. C'est plus difficile. Le plus souvent, la transparence est associée aux cartes, car c'est sur les cartes que la transparence est la plus visible. Dans ce cas, comme précédemment, nous définissons un paramètre spécial:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">    
    ...
    <item name="android:windowTranslucentStatus">true</item>
    ...
</style>

Ici, bien sûr, il existe une astuce bien connue - mettre en retrait plus haut, sinon la barre d'état sera superposée à l'icône et elle sera moche.


Mais sur d'autres écrans, tout se brise.



Comment résoudre le problème? La première méthode qui me vient à l'esprit est différentes activités: nous avons différents sujets, différents paramètres, ils fonctionnent différemment.

Travaillez avec le clavier. Nous évitons les encarts non seulement avec la barre d'état, mais aussi lorsque vous travaillez avec le clavier.



Personne n'aime l'option de gauche, mais pour la transformer en option de droite, il existe une solution simple.

<activity
    ...
    android:windowSoftInputMode="adjustResize">
    ...
</activity>

L'activité peut maintenant être redimensionnée lorsque le clavier apparaît. Cela fonctionne simplement et sévèrement. Mais n'oubliez pas une autre astuce - enveloppez tout ScrollView. Du coup le clavier occupera tout l'écran et il y aura une petite bande dessus?

Il est parfois plus difficile de changer la disposition lorsque le clavier apparaît. Par exemple, sinon organisez les boutons ou masquez le logo.



Nous avons entraîné tant de béquilles avec un clavier et une barre d'état transparente, maintenant il est difficile de nous arrêter. Accédez à StackOverflow et copiez le beau code.

boolean isOpened = false;

public void setListenerToRootView() {
    final View activityRootView = getWindow().getDecorView().findViewById(android.R.id.content);
    activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
    
            int heightDiff = activityRootView.getRootView().getHeight() - activityRootView.getHeight();      
            if (heightDiff > 100) { // 99% of the time the height diff will be due to a keyboard.       
                Toast.makeText(getApplicationContext(), "Gotcha!!! softKeyboardup", 0).show();

                if (isOpened == false) {
                   //Do two things, make the view top visible and the editText smaller
                }
                isOpened = true;
            } else if (isOpened == true) {
                Toast.makeText(getApplicationContext(), "softkeyborad Down!!!", 0).show();
                isOpened = false;
            }    
        }
    });
}

Ici, même la probabilité d'apparition du clavier est calculée. Le code fonctionne, dans l'un des projets que nous avons même utilisé, mais pendant longtemps.

De nombreuses béquilles dans les exemples sont associées à l'utilisation de différentes activités. Mais ils sont mauvais non seulement parce que ce sont des béquilles, mais aussi pour d'autres raisons: le problème du «démarrage à froid», l'asynchronie. J'ai décrit les problèmes plus en détail dans l'article « Permis de conduire une voiture, ou pourquoi les applications devraient être à activité unique ». En outre, la documentation de Google indique que l'approche recommandée est une application à activité unique.

Qu'avons-nous fait avant Android 10


Nous (chez Redmadrobot) développons des applications d'assez haute qualité, mais nous avons longtemps évité les encarts. Comment avons-nous réussi à les éviter sans grosses béquilles et en une seule activité?

Remarque. Captures d'écran et code tirés de mon projet pour animaux de compagnie GitFox .

Imaginez l'écran de l'application. Lorsque nous avons développé nos applications, nous n'avons jamais pensé qu'il pourrait y avoir une barre de navigation transparente ci-dessous. Y a-t-il une barre noire en dessous? Alors quoi, les utilisateurs y sont habitués.



En haut, nous avons initialement défini le paramètre selon lequel la barre d'état est noire avec transparence. À quoi cela ressemble-t-il en termes de mise en page et de code?



L'abstraction sur la figure: le bloc rouge est l'activité de l'application, le bleu est le fragment avec le bot de navigation (avec Tabs), et à l'intérieur les fragments verts avec le contenu sont commutés. On peut voir que la barre d'outils n'est pas sous la barre d'état. Comment y sommes-nous parvenus?

Android a un drapeau délicat fitSystemWindow. Si vous le définissez sur true, le conteneur s'ajoutera un remplissage pour que rien à l'intérieur ne tombe sous la barre d'état. Je pense que ce drapeau est la béquille officielle de Google pour ceux qui ont peur des encarts. Tout utiliser fonctionnera relativement bien sans encarts.

L'indicateur FitSystemWindow=”true”ajoute un remplissage au conteneur que vous avez spécifié. Mais la hiérarchie est importante: si l'un des parents définit ce drapeau sur "true", sa distribution ne sera pas prise en compte, car le conteneur a déjà appliqué l'indentation.

Le drapeau fonctionne, mais un autre problème apparaît. Imaginez un écran avec deux onglets. Lors du basculement, une transaction est lancée, ce qui provoque un replacefragment l'un sur l'autre, et tout se casse.



Le deuxième fragment a également un indicateur FitSystemWindow=”true”, et cela ne devrait pas se produire. Mais que s'est-il passé, pourquoi? La réponse est que c'est une béquille, et parfois cela ne fonctionne pas.

Mais nous avons trouvé une solution sur Stack Overflow : utilisez root pour pas des fragments FrameLayout, mais CoordinatorLayout. Il a été créé à d'autres fins, mais cela fonctionne ici.

Pourquoi ça marche?


Voyons à la source ce qui se passe CoordinatorLayout.

@Override
public void onAttachedToWindow() {   
    super.onAttachedToWindow();
    ...    
    if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {         
        // We're set to fitSystemWindows but we haven't had any insets yet...
        // We should request a new dispatch of window insets
        ViewCompat.requestApplyInsets(this);
    }     
    ...
}

Nous voyons un beau commentaire selon lequel des encarts sont nécessaires ici, mais ils ne le sont pas. Nous devons leur demander à nouveau lorsque nous demandons une fenêtre. Nous avons compris que les encarts fonctionnent d'une manière ou d'une autre à l'intérieur, mais nous ne voulons pas travailler avec eux et partir Coordinator.

Au printemps 2019, nous avons développé l'application, date à laquelle Google I / O vient de passer. Nous n'avons pas encore tout compris, nous avons donc continué à nous accrocher aux préjugés. Mais nous avons remarqué que la commutation des onglets en bas est en quelque sorte lente, car nous avons une disposition complexe, chargée avec l'interface utilisateur. Nous trouvons un moyen simple de résoudre ce problème - changez replacepour show/hideque chaque fois que vous ne recréez pas la mise en page.



Nous changeons, et encore une fois rien ne fonctionne - tout est cassé! Apparemment, il n'est tout simplement pas possible de disperser les béquilles, vous devez comprendre pourquoi elles ont fonctionné. Nous étudions le code et il s'avère que tout ViewGroup est également capable de travailler avec des encarts.


@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {    
    insets = super.dispatchApplyWindowInsets(insets);     
    if (!insets.isConsumed()) {         
        final int count = getChildCount();        
        for (int i = 0; i < count; i++) {
            insets = getChildAt(i).dispatchApplyWindowInsets(insets);             
            if (insets.isConsumed()) {          
                break;
            }
        }
    }
    return insets;
}

Il y a une telle logique à l'intérieur: si au moins quelqu'un a déjà traité des encarts, alors toutes les vues suivantes à l'intérieur du ViewGroup ne les recevront pas. Qu'est-ce que cela signifie pour nous? Permettez-moi de vous montrer un exemple de notre FrameLayout, qui change d'onglet.



À l'intérieur, il y a le premier fragment dont le drapeau est réglé fitSystemWindow=”true”. Cela signifie que le premier fragment traite les encarts. Après cela, nous appelons le HIDEpremier fragment et le SHOWsecond. Mais le premier reste dans la mise en page - sa vue est laissée, juste cachée.

Le conteneur passe par sa Vue: il prend le premier fragment, lui donne des encarts, et de lui fitSystemWindow=”true”- il les a pris et les a traités. Parfaitement, FrameLayout a regardé que les encarts étaient traités et ne les a pas donnés au deuxième fragment. Tout fonctionne comme il se doit. Mais cela ne nous convient pas, que devons-nous faire?

Nous écrivons notre propre ViewGroup


Nous venions de Java, et là OOP dans toute sa splendeur, nous avons donc décidé d'hériter. Nous avons écrit notre propre ViewGroup, à partir duquel nous avons redéfini la méthode dispatchApplyWindowInsets. Il fonctionne comme nous en avons besoin: il rend toujours aux enfants les encarts qui sont venus, que nous les ayons traités ou non.

class WindowInsetFrameLayout @JvmOverloads constructor(
    context: Context,      
    attrs: AttributeSet? = null,     
    defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleRes) {

    override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets { 
        for (child in (0 until childCount)) {
            getChildAt(child).dispatchApplyWindowInsets(insets)
        }
        return insets.consumeSystemWindowInsets()
    }
}

ViewGroup personnalisé fonctionne, le projet est aussi tout ce dont nous avons besoin. Mais il ne parvient toujours pas à une solution générale. Lorsque nous avons compris ce qu'ils nous ont dit sur Google IO, nous avons réalisé que nous ne serions plus en mesure d'agir comme ça.

Android 10


Android 10 nous a montré deux concepts d'interface utilisateur importants auxquels il est strictement recommandé d'adhérer: bord à bord et navigation gestuelle.

Côte à côte. Ce concept suggère que le contenu de l'application devrait occuper tout l'espace possible sur l'écran. Pour nous, en tant que développeurs, cela signifie que les applications doivent être placées sous les panneaux système de la barre d'état et de la barre de navigation.



Auparavant, nous pouvions en quelque sorte ignorer cela ou être placés uniquement sous la barre d'état.

Quant aux listes, elles doivent défiler non seulement jusqu'au dernier élément, mais aussi plus loin pour ne pas rester sous la barre de navigation.


Navigation gestuelle. Ce deuxième concept important est la navigation gestuelle . Il vous permet de contrôler l'application avec vos doigts depuis le bord de l'écran. Le mode gestuel n'est pas standard, tout semble différent, mais maintenant vous ne pouvez pas basculer entre deux barres de navigation différentes.

En ce moment, nous avons réalisé que tout n'est pas si simple. Il ne sera pas possible d'éviter davantage les encarts si nous voulons développer des applications de qualité. Il est temps d'étudier la documentation et de comprendre ce que sont les encarts.

Encarts Que devez-vous savoir à leur sujet?


Des encarts ont été créés pour effrayer les développeurs.
Ils le font parfaitement depuis le moment où il est apparu dans Android 5.

Bien sûr, tout n'est pas si effrayant. Le concept des encarts est simple - ils signalent une superposition d'interface utilisateur système sur l'écran de l'application. Il peut s'agir d'une barre de navigation, d'une barre d'état ou d'un clavier. Le clavier est également une interface utilisateur système standard, qui se superpose à l'application. Il n'est pas nécessaire d'essayer de le traiter avec des béquilles, seulement des encarts.

Le but des encarts est de résoudre les conflits . Par exemple, s'il y a un autre élément au-dessus de notre bouton, nous pouvons déplacer le bouton afin que l'utilisateur puisse continuer à utiliser l'application.

Pour gérer l'encart, utilisez Windowlnsetspour Android 10 et WindowInsetsCompatpour les autres versions.

Il existe 5 types différents d'encarts dans Android 10et un "bonus", qui n'est pas appelé encart, mais sinon. Nous traiterons tous les types, car la plupart ne connaissent qu'une seule chose: les insertions de fenêtres système.

Encarts de fenêtre système


Introduit dans Android 5.0 (API 21). Obtenu par la méthode getSystemWindowInsets().

Il s'agit du principal type d'encarts avec lequel vous devez apprendre à travailler, car les autres fonctionnent de la même manière. Ils sont nécessaires pour traiter la barre d'état, tout d'abord, puis la barre de navigation et le clavier. Par exemple, ils résolvent un problème lorsque la barre de navigation se trouve au-dessus de l'application. Comme dans l'image: le bouton est resté sous la barre de navigation, l'utilisateur ne peut pas cliquer dessus et est très mécontent.



Inserts d'éléments taraudables


Apparu uniquement dans Android 10. Obtenu par la méthode getTappableElementInsets().

Ces encarts ne sont utiles que pour gérer divers modes de barre de navigation . Comme le dit Chris Bane lui-même , vous pouvez oublier ce type d'encarts et contourner uniquement les encarts de fenêtre système. Mais si vous voulez que l'application soit 100% cool, pas 99,9% cool, vous devez l'utiliser.

Regarde l'image.



En haut, la couleur pourpre indique les encarts de la fenêtre système, qui se présenteront dans différents modes de la barre de navigation. On peut voir qu'à droite et à gauche ils sont égaux à la barre de navigation.

Rappelez-vous comment fonctionne la navigation gestuelle: dans le bon mode, nous ne cliquons jamais sur un nouveau panneau, mais faisons toujours glisser nos doigts de bas en haut. Cela signifie que les boutons ne peuvent pas être supprimés. Nous pouvons continuer à appuyer sur notre bouton FAB (Floating Action Button), personne n'interférera. Par conséquent, il TappableElementInsetssera vide car FAB n'est pas nécessaire de se déplacer. Mais si nous montons un peu plus haut, ça va.

La différence n'apparaît qu'avec la navigation gestuelle et la barre de navigation transparente (adaptation des couleurs). Ce n'est pas une situation très agréable.



Tout fonctionnera, mais ça a l'air désagréable. L'utilisateur peut être dérouté par la proximité des éléments. On peut expliquer que l'un est pour les gestes et l'autre pour le pressage, mais toujours pas beau. Par conséquent, élevez le FAB plus haut ou laissez-le à droite.

Encarts gestuels du système


Apparu uniquement dans Android 10. Obtenu par la méthode getSystemGestureInsets().

Ces encarts sont liés à la navigation gestuelle. Initialement, il a été supposé que l'interface utilisateur du système est dessinée au-dessus de l'application, mais les insertions de gestes système indiquent que ce n'est pas l'interface utilisateur qui est dessinée, mais le système lui-même gère les gestes. Ils décrivent où le système gérera les gestes par défaut.

Les zones de ces encarts sont approximativement celles marquées en jaune sur le schéma.



Mais je veux vous avertir - ils ne seront pas toujours comme ça. Nous ne saurons jamais quels nouveaux écrans Samsung et d'autres fabricants proposeront. Il existe déjà des appareils dans lesquels l'écran est de tous les côtés. Peut-être que les encarts ne seront pas du tout là où nous nous attendons. Par conséquent, vous devez travailler avec eux comme avec certaines abstractions: il existe de tels encarts dans les encarts de mouvement du système dans lesquels le système lui-même traite les gestes.

Ces encarts peuvent être remplacés. Par exemple, vous développez un éditeur de photos. Dans certains endroits, vous voulez gérer vous-même les gestes, même s'ils sont près du bord de l'écran. Indiquez au système que vous traiterez vous-même le point à l'écran dans le coin de la photo. Il peut être redéfini en disant au système: "Je traiterai toujours le carré autour du point moi-même."

Encarts gestuels système obligatoires


Apparu uniquement dans Android 10. Il s'agit d'un sous-type d'inserts de gestes système, mais ils ne peuvent pas être remplacés par l'application.

Nous utilisons la méthode getMandatorySystemGestureInsets()pour déterminer la zone où ils ne fonctionneront pas. Cela a été fait intentionnellement de sorte qu'il était impossible de développer une application «désespérée»: redéfinissez le geste de navigation de bas en haut, ce qui vous permet de quitter l'application vers l'écran principal. Ici, le système traite toujours les gestes lui-même .


Ce ne sera pas nécessairement sur le dessous de l'appareil et pas nécessairement de cette taille. Travaillez avec lui comme une abstraction.

Il s'agissait de types d'encarts relativement nouveaux. Mais il y a ceux qui sont apparus avant même Android 5 et ont été appelés différemment.

Inserts stables


Introduit avec Android 5.0 (API 21). Obtenu par la méthode getStableInsets().

Même les développeurs expérimentés ne peuvent pas toujours dire pourquoi ils sont nécessaires. Je vais vous dire un secret: ces encarts ne sont utiles que pour les applications plein écran: lecteurs vidéo, jeux. Par exemple, en mode lecture, toute interface utilisateur système est masquée, y compris la barre d'état, qui se déplace au-delà du bord de l'écran. Mais cela vaut la peine de toucher l'écran, car la barre d'état apparaîtra en haut.

Si vous placez un bouton RETOUR en haut de l'écran, alors qu'il traite correctement les insertions de la fenêtre système, puis avec chaque onglet, une interface utilisateur apparaît sur l'écran d'en haut, et le bouton saute vers le bas. Si vous ne touchez pas l'écran pendant un certain temps, la barre d'état disparaît, le bouton rebondit car les encarts de la fenêtre système ont disparu.

Pour de tels cas, les inserts stables sont parfaits. Ils disent que maintenant aucun élément n'est dessiné sur votre application, mais qu'ils peuvent le faire. Avec ces encarts, vous pouvez connaître à l'avance la valeur de la barre d'état dans ce mode, par exemple, et positionner le bouton où vous le souhaitez.

Remarque. La méthode getStableInsets()n'apparaissait qu'avec l'API 21. Auparavant, il y avait plusieurs méthodes pour chaque côté de l'écran. 

Nous avons examiné 5 types d'encarts, mais il y en a un autre qui ne s'applique pas directement aux encarts. Il aide à gérer les franges et les découpes.

Frange et décolleté


Les écrans ne sont plus carrés. Ils sont oblongs, avec une ou plusieurs découpes pour la caméra et une frange de tous les côtés.



Jusqu'à 28 API, nous n'avons pas pu les traiter. J'ai dû deviner à travers des encarts ce qui se passait là-bas. Mais avec l'API 28 et au-delà (de l'ancien Android), la classe est officiellement apparue DisplayCutout. C'est dans les mêmes encarts à partir desquels vous pouvez obtenir tous les autres types. La classe vous permet de connaître l'emplacement et la taille des artefacts.

En plus des informations de localisation, un ensemble d'indicateurs y est fourni pour le développeur WindowManager.LayoutParams. Ils vous permettent d'inclure différents comportements autour des découpes. Votre contenu peut être affiché autour d'eux, ou peut ne pas être affiché: en mode paysage et portrait de différentes manières.

Drapeauxwindow.attributes.layoutInDisplayCutoutMode =

  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT. En portrait - il y a, et en paysage - une barre noire par défaut.
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER. Pas du tout - une bande noire.
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - est toujours.

L'affichage peut être contrôlé, mais nous travaillons avec eux de la même manière qu'avec les autres encarts.

insets.displayCutout
    .boundingRects
    .forEach { rect -> ... }

Le rappel avec des encarts avec un tableau nous vient displayCutout. Ensuite, nous pouvons le parcourir, traiter toutes les découpes et les franges qui se trouvent dans l'application. Vous pouvez en savoir plus sur la façon de travailler avec eux dans l'article .

Compris avec 6 types d'encarts. Voyons maintenant comment cela fonctionne.

Comment ça fonctionne


Des encarts sont nécessaires, tout d'abord, lorsque quelque chose est dessiné au-dessus de l'application, par exemple, la barre de navigation.



Sans encarts, l'application n'aura pas de barre de navigation et de barre d'état transparentes. Ne soyez pas surpris que vous ne veniez pas SystemWindowInsets, cela arrive.

Inserts répartis


L'ensemble de la hiérarchie de l'interface utilisateur ressemble à un arbre avec une vue à ses extrémités. Les nœuds sont généralement un ViewGroup. Ils héritent également de View, de sorte que les encarts leur parviennent d'une manière spéciale dispatchApplyWindowInsets. À partir de la vue racine, le système envoie des encarts à travers l'arborescence. Voyons comment fonctionne View dans ce cas.



Le système fait appel à la méthode dispatchApplyWindowInsetsdans laquelle s'insèrent les encarts. Cette vue devrait renvoyer quelque chose. Que faire pour gérer ce comportement?

Par souci de simplicité, nous considérons uniquement les WindowInsets.

Inserts de manutention


Tout d'abord, la redéfinition de la méthode dispatchApplyWindowInsetset de l'implémentation à l'intérieur de sa propre logique semble logique : des encarts sont venus, nous avons tout implémenté à l'intérieur.

Si vous ouvrez la classe View et regardez le Javadoc écrit au-dessus de cette méthode, vous pouvez voir: "Ne remplacez pas cette méthode!"
Nous traitons les encarts par délégation ou héritage.
Utilisez la délégation . Google a donné la possibilité d'exposer son propre délégué, qui est responsable de la gestion des encarts. Vous pouvez l'installer via la méthode setOnApplyWindowInsetsListener(listener). Nous avons défini un rappel qui gère ces encarts.

Si, pour une raison quelconque, cela ne nous convient pas, vous pouvez hériter de View et remplacer une autre méthode onApplyWindowInsets(WindowInsets insets) . Nous y substituons nos propres encarts.

Google n'a pas choisi entre la délégation et l'héritage, mais a permis de tout faire ensemble. C'est formidable car nous n'avons pas à redéfinir toutes les barres d'outils ou ListView pour gérer les encarts. Nous pouvons prendre n'importe quelle vue prête à l'emploi, même une bibliothèque, et y définir le délégué, qui gérera les encarts sans redéfinition.

Que retournerons-nous?

Pourquoi retourner quelque chose?


Pour ce faire, vous devez comprendre le fonctionnement de la distribution des encarts, c'est-à-dire ViewGroup. Que fait-elle en elle. Examinons de plus près le comportement par défaut en utilisant l'exemple que j'ai montré plus tôt.

Les encarts système sont venus en haut et en bas, par exemple, 100 pixels chacun. Le ViewGroup les envoie à la première vue dont il dispose. Cette vue les a traités d'une manière ou d'une autre et renvoie des encarts: en haut, elle diminue à 0, mais en bas, elle part. En haut, elle a ajouté un rembourrage ou une marge, mais en bas, elle n'a pas touché et a signalé cela. Ensuite, le ViewGroup transmet des encarts à la deuxième vue.



La vue suivante traite et affiche les encarts. Le ViewGroup voit maintenant que les encarts sont traités à la fois au-dessus et en dessous - il ne reste plus rien, tous les paramètres sont nuls.

La troisième vue ne sait même pas qu'il y avait des encarts et quelque chose se passait. ViewGroup renverra les encarts à celui qui les a exposés.

Lorsque nous avons commencé à traiter ce sujet, nous avons noté l'idée par nous-mêmes - renvoyons toujours les mêmes encarts qui sont venus. Nous voulions éviter une telle situation lorsque certains View ne reconnaissaient même pas qu'il y avait des encarts. L'idée semblait logique. Mais il s'est avéré que non. Google n'a en vain pas ajouté de comportement aux encarts, comme dans l'exemple.

Cela est nécessaire pour que les encarts atteignent toujours toute la hiérarchie de la vue et que vous puissiez toujours travailler avec eux. Dans ce cas, il n'y aura pas de situation lorsque nous basculons deux fragments, l'un a traité et le second n'a pas encore été reçu. Dans la partie pratique, nous reviendrons sur ce point.

Entraine toi


La théorie est terminée. Voyons à quoi cela ressemble dans le code, car en théorie tout est toujours fluide. Nous utiliserons AndroidX.

Remarque. Dans GitFox, tout est déjà implémenté dans le code, prise en charge complète de tous les goodies du nouvel Android. Afin de ne pas vérifier les versions d'Android et de ne pas rechercher le type requis d'Insets, utilisez toujours la view.setOnApplyWindowInsetsListener { v, insets -> ... }version d'AndroidX - à la place ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> ... }.

Il y a un écran avec une barre de navigation sombre. Il ne chevauche aucun élément sur le dessus. Tout est comme d'habitude.


Mais il s'avère que nous devons maintenant le rendre transparent. Comment?

Activez la transparence


Le plus simple est d'indiquer au sujet que la barre d'état et la barre de navigation sont transparentes.

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
    <item name="android:windowTranslucentStatus">true</item>
    <item name="android:windowTranslucentNavigation">true</item>
</style>

À ce moment, tout commence à se chevaucher à la fois d'en haut et d'en bas.

Il est intéressant de noter qu'il existe deux indicateurs, et il y a toujours trois options. Si vous spécifiez la transparence pour la barre de navigation, la barre d'état deviendra transparente par elle-même - une telle limitation. Vous ne pouvez pas écrire la première ligne, mais j'aime toujours la clarté, alors j'écris 2 lignes pour que les abonnés puissent comprendre ce qui se passe, et ne pas creuser à l'intérieur.

Si vous choisissez cette méthode, la barre de navigation et la barre d'état deviennent noires avec transparence. Si l'application est blanche, vous ne pouvez ajouter des couleurs qu'à partir du code. Comment faire?

Activez la transparence avec la couleur


Il est bon que nous ayons une application d'activité unique, nous avons donc mis deux drapeaux dans une activité.

rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or  
    View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
    <!-- Set the navigation bar to 50% translucent white -->
    <item name="android:navigationBarColor">#80FFFFFF</item> 
</style>

Il y a beaucoup de drapeaux là-bas, mais ce sont ces deux-là qui aideront à rendre la transparence dans la barre de navigation et la barre d'état. La couleur peut être spécifiée via le thème.

C'est le comportement étrange d'Android: quelque chose à travers le thème et quelque chose à travers les drapeaux. Mais nous pouvons spécifier des paramètres à la fois dans le code et dans le sujet. Ce n'est pas si mauvais pour Android, juste sur les anciennes versions d'Android, le drapeau spécifié dans le sujet sera ignoré. navigationBarColorCela mettra en évidence que sur Android 5, ce n'est pas le cas, mais tout ira de pair. Dans GitFox, c'est à partir du code que j'ai défini la couleur de la barre de navigation.

Nous avons commis des fraudes - indiqué que la barre de navigation est blanche et transparente. Maintenant, l'application ressemble à ceci.


Quoi de plus simple que de manipuler des encarts?


En effet, élémentaire.

ViewCompat
    .setOnApplyWindowInsetsListener(bottomNav) { view, insets ->  
        view.updatePadding(bottom = insets.systemWindowInsetBottom) 
        insets
    }

Nous prenons la méthode setOnApplyWindowInsetsListeneret la transmettons bottomNav. Nous disons que lorsque les encarts viennent, installez le rembourrage inférieur qui est entré systemWindowInsetBottom. Nous le voulons en dessous, mais tous nos éléments cliquables sont au-dessus. Nous renvoyons entièrement les encarts afin que toutes les autres vues de notre hiérarchie les reçoivent également.

Tout a l'air génial.


Mais il ya un hic. Si dans la mise en page, nous avons indiqué une sorte de rembourrage dans la barre de navigation ci-dessous, alors ici, il a été supprimé - définissez des updatePaddingencarts égaux. Notre mise en page ne ressemble pas à ce que nous souhaiterions.

Pour enregistrer les valeurs de la mise en page, vous devez d'abord enregistrer ce qui se trouve dans le rembourrage inférieur. Plus tard, lorsque les encarts arrivent, ajoutez et définissez les valeurs résultantes.

val bottomNavBottomPadding = bottomNav.paddingBottom
ViewCompat
    .setOnApplyWindowInsetsListener(bottomNav) { view, insets ->                 
        view.updatePadding(
        bottom = bottomNavBottomPadding + insets.systemWindowInsetBottom
    )
    Insets
}

Il n'est pas pratique d'écrire de cette façon: partout dans le code où vous utilisez des encarts, vous devez enregistrer la valeur de la mise en page, puis les ajouter à l'interface utilisateur. Mais il y a Kotlin, et c'est merveilleux - vous pouvez écrire votre propre extension, qui fera tout cela pour nous.

Ajoutez Kotlin!


Nous nous en souvenons initialPaddinget le donnons au gestionnaire lorsque de nouveaux encarts arrivent (avec eux). Cela les aidera à s'additionner ou à construire une sorte de logique par le haut.

fun View.doOnApplyWindowInsets(block: (View, WindowInsetsCompat, Rect) -> WindowInsetsCompat) {    

    val initialPadding = recordInitialPaddingForView(this)

    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->        
        block(v, insets, initialPadding)
    }
    
    requestApplyInsetsWhenAttached()
}

private fun recordInitialPaddingForView(view: View) =
   Rect(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)

  .

bottomNav.doOnApplyWindowInsets { view, insets, padding ->   
    view.updatePadding(
        bottom = padding.bottom + insets.systemWindowInsetBottom
    ) 
    insets
}

Nous devons redéfinir la lambda. Il a non seulement des inserts, mais aussi des rembourrages. Nous pouvons les ajouter non seulement d'en bas, mais aussi d'en haut, si c'est une barre d'outils qui traite la barre d'état.

Quelque chose oublié!


Il y a un appel à une méthode incompréhensible.

fun View.doOnApplyWindowInsets(block: (View, WindowInsetsCompat, Rect) -> WindowInsetsCompat) {

    val initialPadding = recordInitialPaddingForView(this)
     
    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->        
        block(v, insets, initialPadding)
    }
      
    requestApplyInsetsWhenAttached() 
}

private fun recordInitialPaddingForView(view: View) =
      Rect(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)

Cela est dû au fait que vous ne pouvez pas simplement espérer que le système vous enverra des encarts. Si nous créons une vue à partir du code ou l'installons un InsetsListenerpeu plus tard, le système lui-même peut ne pas transmettre la dernière valeur.

Nous devons vérifier que lorsque la vue est à l'écran, nous avons dit au système: "Des encarts sont arrivés, nous voulons les traiter." Nous définissons l'écouteur et devons faire une demande requestsApplyInsets. "

fun View.requestApplyInsetsWhenAttached() {    
    if (isAttachedToWindow) {        
        requestApplyInsets()
    } else {
        addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {            
            override fun onViewAttachedToWindow(v: View) {
                v.removeOnAttachStateChangeListener(this)
                v.requestApplyInsets()           
            }

            override fun onViewDetachedFromWindow(v: View) = Unit
        })
    }
}

Si nous avons créé un code View et ne l'avons pas encore attaché à notre mise en page, nous devons alors nous abonner à l'heure à laquelle nous le faisons. C'est seulement alors que nous demandons des encarts.

Mais là encore, il y a Kotlin: ils ont écrit une simple extension et nous n'avons plus à y penser.

Refining RecyclerView


Parlons maintenant de la finalisation de RecyclerView. Le dernier élément tombe sous la barre de navigation et reste en dessous - laid. Il est également gênant de cliquer dessus. Si ce n'est pas un nouveau panneau, mais l'ancien, un grand, alors en général l'élément entier peut disparaître en dessous. Que faire?



La première idée consiste à ajouter un élément ci-dessous et à définir la hauteur pour s'adapter aux encarts. Mais si nous avons une application avec des centaines de listes, nous devrons en quelque sorte souscrire chaque liste à des encarts, y ajouter un nouvel élément et définir la hauteur. De plus, RecyclerView est asynchrone, on ne sait pas quand cela fonctionnera. Il y a beaucoup de problèmes.

Mais il y a un autre hack officiel. Étonnamment, cela fonctionne efficacement.

<androidx.recyclerview.widget.RecyclerView 
        android:id="@+id/recyclerView"        
    android:layout_width="match_parent"    
    android:layout_height="match_parent"             
    android:clipToPadding="false" />

recyclerView.doOnApplyWindowInsets { view, insets, padding ->    
    view.updatePadding(
        bottom = padding.bottom + insets.systemWindowInsetBottom
    )
}

Il y a un tel drapeau dans la mise en page clipToPadding. Par défaut, c'est toujours vrai. Cela suggère que vous n'avez pas besoin de dessiner des éléments qui apparaissent là où le rembourrage est exposé. Mais s'il est défini clipToPadding="false", vous pouvez dessiner.

Maintenant, si vous définissez le remplissage en bas, dans RecyclerView cela fonctionnera comme ceci: en bas, le remplissage est défini et l'élément est dessiné dessus jusqu'à ce que nous fassions défiler. Une fois arrivé à la fin, RecyclerView fera défiler les éléments jusqu'à la position dont nous avons besoin.

En définissant un tel indicateur, nous pouvons travailler avec RecyclerView comme avec une vue normale. Ne pensez pas qu'il y a des éléments qui défilent - définissez simplement le remplissage ci-dessous, comme, par exemple, dans la barre de navigation inférieure.

Maintenant, nous avons toujours renvoyé les encarts comme s'ils n'avaient pas été traités. Les encarts entiers nous sont parvenus, nous avons fait quelque chose avec eux, réglé les rembourrages et renvoyé tous les encarts entiers. Cela est nécessaire pour que tout ViewGroup transmette toujours ces encarts à tous les View. Ça marche.

Bogue d'application


Dans de nombreuses applications sur Google Play qui ont déjà traité des encarts, j'ai remarqué un petit bug. Maintenant, je vais parler de lui.

Il y a un écran avec navigation au sous-sol. À droite se trouve le même écran qui montre la hiérarchie. Le fragment vert affiche le contenu à l'écran, à l'intérieur il a un RecyclerView.



Qui a manipulé les encarts ici? Barre d'outils: il a appliqué un remplissage sur le dessus pour que son contenu se déplace sous la barre d'état. Par conséquent, la barre de navigation inférieure appliquait des encarts par le bas et montait au-dessus de la barre de navigation. Mais RecyclerView ne gère en aucun cas les encarts, il ne relève pas d'eux, il n'a pas besoin de traiter les encarts - tout est fait correctement.



Mais ici, il s'avère que nous voulons utiliser le fragment vert RecyclerView dans un autre endroit où la barre de navigation inférieure n'est plus là. À ce stade, RecyclerView commence déjà à gérer les encarts par le bas. Nous devons lui appliquer un remplissage pour faire défiler correctement sous la barre de navigation. Par conséquent, dans RecyclerView, nous ajoutons également le traitement des encarts.



Nous revenons à notre écran, où tout le monde traite les encarts. Rappelez-vous, personne ne signale qu'il les a traités?



Nous voyons cette situation: RecyclerView a traité les encarts par le bas, bien qu'il n'atteigne pas la barre de navigation - un espace est apparu en bas. J'ai remarqué cela dans les applications, et assez grand et populaire.



Si tel est le cas, nous nous souvenons que nous renvoyons toutes les encarts à traiter. Donc (il s'avère!) Il faut signaler que les encarts sont traités: la barre de navigation doit signaler que les encarts sont traités. Ils ne devraient pas accéder à RecyclerView.



Pour ce faire, installez la barre de navigation inférieure InsetsListener. Soustrayez à l'intérieur bottom + insetsqu'ils ne sont pas traités, retournez 0.

doOnApplyWindowInsets { view, insets, initialPadding ->    
    view.updatePadding(
        bottom = initialPadding.bottom + insets.systemWindowInsetBottom
    )
    insets.replaceSystemWindowInsets(
        Rect(
            insets.systemWindowInsetLeft,            
            insets.systemWindowInsetTop,            
            insets.systemWindowInsetRight,
            0
        )
    )
}

Nous retournons de nouveaux encarts, dans lesquels tous les anciens paramètres sont égaux, mais bottom + insetségaux à 0. Il semble que tout va bien. Nous commençons et encore un non-sens - RecyclerView gère toujours les encarts pour une raison quelconque.



Je n'ai pas immédiatement compris que le problème est que le conteneur pour ces trois vues est LinearLayout. À l'intérieur d'eux se trouve une barre d'outils, un extrait avec un RecyclerView et au bas de la barre de navigation inférieure. LinearLayout prend ses enfants en ordre et leur applique des encarts.

Il s'avère que la barre de navigation inférieure sera la dernière. Il a dit à quelqu'un qu'il avait traité tous les encarts, mais il était trop tard. Tous les encarts ont été traités de haut en bas, RecyclerView les a déjà reçus et cela ne nous sauve pas.



Que faire? LinearLayout ne fonctionne pas comme je le souhaite, il les transfère de haut en bas et je dois d'abord obtenir celui du bas.

Redéfinir tout


Le développeur Java a joué en moi - tout doit être redéfini ! Eh bien, dispatchApplyWindowInsetsredéfinissez maintenant , définissez yLinearLayout, qui ira toujours de bas en haut. Il enverra d'abord des encarts à la barre de navigation inférieure, puis à tout le monde.

@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {     
    insets = super.dispatchApplyWindowInsets(insets);     
    if (!insets.isConsumed()) {        
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            insets = getChildAt(i).dispatchApplyWindowInsets(insets);
            if (insets.isConsumed()) {
                break;
            }
        }
    }
    return insets;
}

Mais je me suis arrêté à temps, me souvenant du commentaire qu'il n'était pas nécessaire de redéfinir cette méthode.

Ici, un peu plus haut est le salut: ViewGroup recherche un délégué qui traite les encarts, puis appelle la super méthode sur View et active son propre traitement. Dans ce code, nous obtenons des encarts, et s'ils n'ont pas encore été traités, nous commençons la logique standard pour nos enfants.

J'ai écrit une simple extension. Il permet, en s'appliquant InsetsListenerà une seule vue, de dire à qui ces encarts passent.

fun View.addSystemBottomPadding(    
    targetView: View = this
) {
    doOnApplyWindowInsets { _, insets, initialPadding ->           
        targetView.updatePadding(
            bottom = initialPadding.bottom + insets.systemWindowInsetBottom
        )
        insets.replaceSystemWindowInsets(
            Rect(
                insets.systemWindowInsetLeft,
                insets.systemWindowInsetTop,     
                insets.systemWindowInsetRight,
                0
            )
        )
    }
}

Ici, il targetViewest égal par défaut à la même vue au-dessus de laquelle nous appliquons addSystemBottomPadding. Nous pouvons le redéfinir.

Sur mon LinearLayout, j'ai suspendu un tel gestionnaire, en passant targetView- c'est ma barre de navigation inférieure.

  • D'abord, il donnera des encarts à la barre de navigation inférieure. 
  • Cela traitera les encarts, retournera le fond zéro.
  • De plus, par défaut, ils iront de haut en bas: Barre d'outils, fragment avec RecyclerView.
  • Puis, peut-être, il enverra à nouveau des encarts à la barre de navigation inférieure. Mais ce n'est plus important, tout fonctionnera si bien.

J'ai réalisé exactement ce que je voulais: tous les encarts sont traités dans l'ordre dans lequel ils sont nécessaires.



Important


Quelques points importants à garder à l'esprit.

Le clavier est l'interface utilisateur du système au-dessus de votre application. Pas besoin de la traiter d'une manière spéciale. Si vous regardez le clavier Google, ce n'est pas seulement une mise en page avec des boutons sur laquelle vous pouvez cliquer. Il existe des centaines de modes pour ce clavier: recherche de gifs et de mèmes, entrée vocale, redimensionnement de 10 pixels en hauteur aux tailles d'écran. Ne pensez pas au clavier, mais abonnez-vous aux encarts.

Le tiroir de navigation ne prend toujours pas en charge la navigation gestuelle. À Google IO, ils ont promis que tout fonctionnerait hors de la boîte. Mais dans Android 10, Navigation Drawer ne prend toujours pas en charge cela. Si vous passez à Android 10 et activez la navigation par gestes, le tiroir de navigation tombera. Maintenant, vous devez cliquer sur le menu hamburger pour le faire apparaître, ou une combinaison de circonstances aléatoires vous permet de l'étirer.

Dans la version pré-alpha d'Android, Navigation Drawer fonctionne, mais je n'ai pas osé mettre à jour - c'est pré-alpha. Par conséquent, même si vous installez la dernière version de GitFox à partir du référentiel, il y a un tiroir de navigation, mais il ne peut pas être extrait. Dès que le support officiel sera disponible, je mettrai à jour et tout fonctionnera bien.

Liste de contrôle de préparation pour Android 10


Définissez la transparence de la barre de navigation et de la barre d'état depuis le début du projet . Google recommande fortement de maintenir une barre de navigation transparente. Pour nous, c'est une partie pratique importante. Si le projet fonctionne, mais que vous ne l'avez pas activé, prenez le temps de prendre en charge Android 10. Activez-les d'abord dans le sujet, comme translucide, et corrigez la mise en page où il s'est cassé.

Ajoutez des extensions à Kotlin - c'est plus facile avec elles.

Ajoutez à toute la barre d'outils au-dessus du rembourrage. Les barres d'outils sont toujours en haut, et c'est ce que vous devez faire.

Pour tous les RecyclerViews, ajoutez un rembourrage par le bas et la possibilité de faire défiler clipToPadding="false".

Pensez à tous les boutons sur les bords de l'écran (FAB) . Les FAB ne seront probablement pas là où vous vous attendez.

Ne remplacez pas ou ne faites pas vos propres implémentations pour tous les LinearLayout et autres cas similaires. Faites l'astuce, comme dans GitFox, ou prenez mon extension prête à l'emploi pour vous aider.

Vérifiez les gestes sur les bords de l'écran pour une vue personnalisée . Le tiroir de navigation ne le prend pas en charge. Mais ce n'est pas si difficile de le supporter avec vos mains, de redéfinir les encarts pour les gestes à l'écran avec le tiroir de navigation. Vous avez peut-être des éditeurs d'images où les gestes fonctionnent.

Le défilement dans ViewPager ne fonctionne pas du bord, mais uniquement du centre vers la droite et la gauche . Google dit que c'est normal. Si vous tirez le ViewPager depuis le bord, il est perçu comme appuyant sur le bouton "Retour".

Dans un nouveau projet, intégrez immédiatement toute la transparence. Vous pouvez dire aux concepteurs que vous n'avez pas de barre de navigation ni de barre d'état. Le carré entier est le contenu de votre application. Et les développeurs sauront déjà où et quoi soulever.

Liens utiles:

  • @rmr_spb dans un télégramme - enregistrements de mitaps internes de Redmadrobot SPb.
  • Tout le code source dans cet article et encore plus dans GitFox . Il y a une branche Android 10 distincte, où vous pouvez voir en validant comment je suis arrivé à tout cela et comment j'ai pris en charge le nouvel Android.
  • Bibliothèque Insetter de Chris Bane. Il contient 5-6 extensions que j'ai montrées. Pour l'utiliser dans votre projet, contactez la bibliothèque, il est probable qu'elle passe à Android Jetpack. Je les ai développés dans mon projet et, il me semble, ils se sont améliorés.
  • Article de Chris Bane.
  • FunCorn, , .
  • «Why would I want to fitsSystemWindows?» .

AppConf, youtube- . , - . AppsConf , . , , stay tuned!

All Articles