Intermédiaire

Transformez vos layouts en ConstraintLayout – Partie 1

Introduction au ConstraintLayout

Le problème avec les layouts actuels

 

Si vous faites du mobile depuis un moment, vous avez forcément dû au moins une fois vous retrouver dans la situation suivante :

 

Planning de la semaine. On annonce la nouvelle grande fonctionnalité de l’application, celle qui est censée booster les notes sur le store et augmenter la fréquentation sur l’application. Aussi, la présentation a été extrêmement soignée : réflexion poussée sur l’ergonomie de l’écran, et surtout sur le design pour garantir l’effet “Waouh” sur l’utilisateur.

Puis vient l’implémentation. Et vient le moment où vous, développeur, devez adapter au mieux la superbe maquette bichonnée par votre designer. Et pour peu que celui-ci ait des côtés artistiques prononcés, vous commencez à vous faire une épilation du crâne à grand renfort d’arrachage de cheveux !

 

Alors pourquoi ? Pour concevoir ses layouts sur Android, on utilise certains types de ViewGroup qui reviennent régulièrement, par exemple le FrameLayout, le LinearLayout ou encore le RelativeLayout. On vous a toujours dit que le RelativeLayout est le meilleur du lot pour composer un layout avec beaucoup d’éléments éparpillés, notamment pour une raison : il permet d’éviter d’imbriquer ses ViewGroup, en les plaçant de façon relative. L’imbrication des ViewGroup est une mauvaise chose car plus la hiérarchie de vos widgets augmente, plus le calcul pour les faire apparaître à l’écran sera long !

Sauf que… le RelativeLayout est limité. Toutes ses propriétés ne marchent pas comme on le voudrait (les problèmes avec l’attribut layout_alignParentBottom parleront peut-être à certains d’entre vous), il ne permet pas de répartir dynamiquement les proportions de ses widgets, ou encore de centrer efficacement des widgets les uns par rapport aux autres. Tout cela, le LinearLayout le gère bien mieux (grâce aux layout_weight notamment, et au fait que l’ajout d’éléments au LinearLayout modifie dynamiquement l’alignement des éléments déjà présents).

Résultat : on se retrouve à intégrer du LinearLayout dans notre RelativeLayout. Voir, on passe totalement sur du LinearLayout, avec plusieurs niveaux de hiérarchie (à un certain stade, c’est plus rentable car le LinearLayout est moins coûteux en calculs). Et la performance en pâtit inexorablement. Ainsi que la lisibilité du code.

 

Et c’est sur ce constat que vient une nouvelle solution.

 

Le ConstraintLayout : un RelativeLayout boosté aux hormones

 

Le ConstraintLayout est un nouveau type de ViewGroup présenté en mai 2016 par Google à la Google I/O.

 

Son objectif est de vous permettre de créer vos layouts avec plus de facilité, ainsi que de vous fournir des outils pour placer au maximum vos widgets sur une hiérarchie plate.

 

Dans les grandes lignes, le ConstraintLayout reprend le fonctionnement du RelativeLayout. Il permet de placer ses widgets de façon relative aux autres. Ici, cela se fait à l’aide de “contraintes” de positionnement, similaires aux règles du RelativeLayout (layout_alignTop, layout_toRightOf etc.).

Sauf que ça ne s’arrête pas là. Car en effet, il existe d’autres contraintes que celles héritées du RelativeLayout. Certaines sont là pour calculer un ratio, placer un élément sur un pourcentage précis de l’écran ou même pouvoir créer des chaînes de widgets qui permettront, par exemple, d’utiliser les poids comme on le ferait avec un LinearLayout. En cela, le ConstraintLayout permet de répondre à plusieurs problématiques qui nécessitaient jusqu’ici l’usage de ViewGroup imbriqués.

 

Pour mieux comprendre ces fonctionnalités, et pouvoir situer dans quels cas elles sont utiles, je vous propose de suivre un cas concret, en transformant un RelativeLayout complexe en ConstraintLayout.

Dans ce premier article, je vous propose de voir comment garder un comportement similaire d’un RelativeLayout vers un ConstraintLayout, tout en apportant quelques améliorations au passage.

Ensuite, dans le prochain article, nous verrons comment optimiser le code et réduire le nombre de ViewGroup imbriqués.

 

Avant de commencer

Point sur l’installation

 

Afin de pouvoir utiliser le ConstraintLayout dans un projet, il est nécessaire d’ajouter une dépendance car le ConstraintLayout est détaché du coeur de l’API, comme le RecyclerView ou les CardView par exemple.

La dépendance Gradle est la suivante :

compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4'

 

Comme vous pouvez le remarquer, j’utilise la version bêta 4 du ConstraintLayout pour la rédaction de cet article. Il n’est pas impossible que cette version soit rendue obsolète et que certaines fonctionnalités ou certains nommages évoluent avec le temps.

 

Point sur le vocabulaire

 

Vous aurez peut-être remarqué que depuis le début de l’article, j’utilise des termes précis pour définir les éléments d’un écran. Comme ces termes peuvent être ambigus, je vous en donne la définition ici tels qu’ils seront utilisés dans l’article :

 

  • ViewGroup : les classes qui héritent de ViewGroup (RelativeLayout, LinearLayout, ConstraintLayout etc.)
  • Widget : les éléments graphiques que l’on peut placer dans un layout et qui héritent de View (ImageView, TextView etc… ainsi que les ViewGroup eux-mêmes).
  • Layout : correspond au fichier XML de layout “inflaté” par vos Activity, Fragment ou encore RecyclerView. J’utiliserai ce terme pour parler de l’ensemble de l’écran.

 

Point sur l’éditeur de layout

 

Enfin, avant de commencer, parlons rapidement de l’éditeur de layouts, car celui-ci est fortement lié au ConstraintLayout.

L’article traite surtout du passage au ConstraintLayout en terme de code, c’est pourquoi je n’entrerai pas dans les détails du fonctionnement de l’éditeur. Si le sujet vous intéresse, je vous suggère de consulter la très bonne documentation fournie par Google sur l’utilisation de l’éditeur avec le ConstraintLayout.

 

Je vous recommande toutefois de créer vos vues grâce à l’éditeur XML pour l’instant.

En effet, même s’il est effectivement plus pratique qu’avant, l’éditeur souffre de problèmes similaires à son ancienne mouture. Notamment, l’éditeur a la fâcheuse tendance à modifier votre code à son gré lorsque vous souhaitez utiliser les deux (éditeur graphique et XML). Un point agaçant, par exemple, est la tendance de l’éditeur à placer dans la preview des valeurs absolues sur les widgets qui n’ont pas encore de contraintes.

Vous pourriez bien entendu ne passer que par l’éditeur graphique, sauf que celui-ci manque encore de plusieurs options pratiques (les styles par exemple) ainsi que de l’absence totale de gestion du databinding. Ce qui fait que vous ferez régulièrement des allers-retours entre l’éditeur graphique et le XML.

Enfin, un dernier souci est que le panneau de preview permet à présent l’édition graphique, apportant les inconvénients que nous avons pu voir précédemment à chaque fois que l’on veut prévisualiser son layout. Sans compter que pour les layouts possédant beaucoup de widgets, l’éditeur a tendance à consommer beaucoup de ressources et à ralentir l’IDE.

 

Il n’est pas à douter que l’éditeur s’améliorera au fil du temps, mais pour le moment il est plus fiable de n’utiliser que le XML.

Pour mieux illustrer les fonctionnalités cependant, j’y ferai référence durant l’article avec notamment la preview de l’application en mode normal (telle que sur l’écran) et en mode blueprint (mode ajouté avec l’éditeur, et qui permet de mieux voir les widgets et leurs contraintes sur l’écran).

 

Passons maintenant aux choses sérieuses !

 

Transformation d’un RelativeLayout complexe vers un ConstraintLayout

 

Drawing

 

Notre exemple : une application de streaming musical

 

Prenons la situation suivante : vous travaillez en tant que développeur mobile sur une application d’écoute de musique en ligne (du même genre que Deezer ou Spotify). Vous pouvez écouter tout ce que vous voulez, cependant votre compte gratuit vous limite à une certaine quantité de musique. Les utilisateurs se plaignent depuis un moment qu’ils ne peuvent pas consulter la durée totale de musique écoutée pour savoir s’ils approchent de la limite.

On vous a donc demandé de réaliser une page où, après connexion de l’utilisateur, celui-ci peut voir sa consommation musicale.

 

Voici comment la fonctionnalité a été définie :

 

Drawing

 

L’écran se divise en trois parties principales :

 

  • Le bloc d’authentification permet à un utilisateur de s’authentifier afin d’afficher ses informations dans les blocs suivants. Les boutons d’authentification permettent respectivement de réinitialiser l’écran (suppression des informations et remise à zéro des champs de texte), ou de valider l’authentification.
  • Le bloc de consommation affiche à l’utilisateur où en est sa consommation, à l’aide d’un curseur situé entre 0 et la durée maximale d’écoute permise par son compte.
  • Le bloc de dernière musique enfin, affiche à l’utilisateur la dernière musique qu’il a écoutée.

 

On ajoutera le header de la page, une image prenant toute la largeur de l’écran, ainsi que le titre de la fonctionnalité.

 

Bien entendu, on veut que la fonctionnalité que vous allez coder respecte au mieux cette maquette, et soit la plus performante possible car beaucoup d’utilisateurs de votre application n’ont pas de devices puissants.

 

Vous faites donc un premier essai avec le RelativeLayout, puisque c’est celui qui semble le plus adéquat ici. Mais le résultat n’est pas satisfaisant : la maquette contient des placements par rapport à un pourcentage de l’écran qui ne sont que partiellement respectés, le ratio des images pose problème et il est difficile d’éviter l’intégration de LinearLayout pour gérer l’alignement de certains widgets. Vous avez fait votre possible pour optimiser le layout mais là, vous touchez à la limite.

 

Voyons ensemble comment on peut améliorer ce code, point par point, grâce au ConstraintLayout.

 

Le code du projet est entièrement disponible sur le GitHub de Soat.
Je vous conseille de télécharger le projet afin de pouvoir suivre avec plus d’aisance la progression de l’article. Néanmoins, je rappellerai le code concerné dans chaque partie.

 

Les contraintes de positionnement : la force du Relativelayout

 

Commençons par le commencement, et parlons des contraintes principales : celles issues du RelativeLayout.
Elles permettent d’aligner ou de positionner un widget par rapport à un autre. Ces contraintes sont des attributs de widgets qui prennent la forme suivante dans le XML :

 

layout_constraint(X)_to(Y)of

 

Avec X le côté du widget où l’on place la contrainte, et Y le côté du widget de destination.

Par exemple, si je veux aligner la droite de mon widget à la gauche d’un autre widget, j’utiliserais la contrainte suivante :

 

layout_constraintLeft_toRightOf

 

Sur l’éditeur de layout, cela donnerait ça (avec une marge de 56dp) :

 

Drawing

 

Vous aurez peut-être remarqué que cette contrainte fonctionne exactement de la même manière que l’attribut “layout_toRightOf” du RelativeLayout, et c’est voulu. Toutes ces contraintes de positionnement ont un équivalent direct à des attributs du RelativeLayout :

 

RelativeLayout vers -> ConstraintLayout
layout_alignLeft -> layout_constraintLeft_toLeftOf
layout_alignStart -> layout_constraintStart_toStartOf
layout_alignRight -> layout_constraintRight_toRightOf
layout_alignEnd -> layout_constraintEnd_toEndOf
layout_alignTop -> layout_constraintTop_toTopOf
layout_alignBottom -> layout_constraintBottom_toBottomOf
layout_alignBaseline -> layout_constraintBaseline_toBaselineOf
layout_alignParentLeft -> layout_constraintLeft_toLeftOf
layout_alignParentStart -> layout_constraintStart_toStartOf
layout_alignParentRight -> layout_constraintRight_toRightOf
layout_alignParentEnd -> layout_constraintEnd_toEndOf
layout_alignParentTop -> layout_constraintTop_toTopOf
layout_alignParentBottom -> layout_constraintBottom_toBottomOf
layout_toLeftOf -> layout_constraintRight_toLeftOf
layout_toStartOf -> layout_constraintEnd_toStartOf
layout_toRightOf -> layout_constraintLeft_toRightOf
layout_toEndOf -> layout_constraintStart_toEndOf
layout_above -> layout_constraintBottom_toTopOf
layout_below -> layout_constraintTop_toBottomOf

 

Grâce à ce tableau, on voit pourquoi ces contraintes ont été nommées ainsi au lieu de conserver les mêmes noms que pour le RelativeLayout : bien que la syntaxe soit verbeuse, elle est homogène et plus claire. C’est d’ailleurs l’une des critiques qui revient souvent sur le RelativeLayout, ses attributs ne sont pas intuitifs. Ici, la syntaxe est tellement simple à comprendre qu’elle devient rapide à mémoriser.

 

Petite astuce par ailleurs : sur Android Studio, il vous suffit de taper les premières lettres des côtés concernés précédées d’un “c” pour qu’il vous trouve la bonne contrainte (si on reprend l’exemple précédent cela donnerait “ctb” pour “layout_constraintTop_toBottomOf”).

 

Mise en application : le bloc d’authentification

 

Drawing

 

Prenons la partie authentification dans notre application. Ses widgets sont placés grâce aux attributs du RelativeLayout. Voyons à quoi ressemble le code :

 

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <!--Cet ImageView sert de background pour les widgets suivants.
    Cela évite d'avoir à regrouper ces widgets dans un layout juste pour leur donner un background.
    Sa hauteur est définie par ses règles d'alignement : il prend l'espace entre le bas du header
    et le bas du TextView de conseil. -->
    <ImageView
        android:id="@+id/auth_background_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_alignBottom="@+id/advice_textview"
        android:layout_below="@id/header_imageview"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginRight="@dimen/default_margin"
        android:layout_marginTop="@dimen/default_margin"
        android:background="@drawable/rounded_shadows" />

    <!--Une fois le background défini, on évite d'y faire référence en vertical
        (pour éviter les dépendances circulaires).  -->
    <TextView
        android:id="@+id/login_textview"
        style="@style/TextAppearance.Label"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignLeft="@id/auth_background_view"
        android:layout_below="@id/header_imageview"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginTop="@dimen/large_margin"
        android:text="@string/Music_Screen_User_Label" />

    <TextView
        android:id="@+id/password_textview"
        style="@style/TextAppearance.Label"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_alignLeft="@id/auth_background_view"
        android:layout_below="@id/login_textview"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginTop="@dimen/default_margin"
        android:text="@string/Music_Screen_Password_Label" />

    <EditText
        android:id="@+id/login_edittext"
        style="@style/TextAppearance.Edition"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_alignBaseline="@id/login_textview"
        android:layout_alignRight="@id/auth_background_view"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginRight="@dimen/default_margin"
        android:layout_toRightOf="@id/password_textview"
        android:hint="@string/Music_Screen_User_Hint" />

    <EditText
        android:id="@+id/password_edittext"
        style="@style/TextAppearance.Edition"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_alignBaseline="@id/password_textview"
        android:layout_alignRight="@id/auth_background_view"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginRight="@dimen/default_margin"
        android:layout_toRightOf="@id/password_textview"
        android:hint="@string/Music_Screen_Password_Hint"
        android:inputType="textPassword" />

    <TextView
        android:id="@+id/advice_textview"
        style="@style/TextAppearance.Indication"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_alignRight="@id/auth_background_view"
        android:layout_below="@id/password_textview"
        android:layout_marginRight="@dimen/default_margin"
        android:layout_marginLeft="@dimen/large_margin"
        android:layout_marginTop="@dimen/small_margin"
        android:layout_alignLeft="@id/password_edittext"
        android:paddingBottom="@dimen/default_margin"
        android:text="@string/Music_Screen_Password_Indication" />
</RelativeLayout>

 

Ici, rien de particulier à optimiser. Le RelativeLayout remplit bien son rôle, aussi voulons-nous en conserver les avantages.

Passons donc ce code en ConstraintLayout :

 

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingBottom="@dimen/default_margin">

    .......

    <!--Authentication block-->

    <!--Adaptation de la technique pour le background avec des contraintes.
        Le fonctionnement reste similaire.-->
    <ImageView
        android:id="@+id/auth_background_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="@dimen/default_margin"
        android:background="@drawable/rounded_shadows"
        app:layout_constraintBottom_toBottomOf="@+id/advice_textview"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/header_imageview" />

<TextView
        android:id="@+id/login_textview"
        style="@style/TextAppearance.Label"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginStart="@dimen/default_margin"
        android:layout_marginTop="@dimen/large_margin"
        android:text="@string/Music_Screen_User_Label"
        app:layout_constraintLeft_toLeftOf="@+id/auth_background_view"
        app:layout_constraintTop_toBottomOf="@+id/header_imageview" />

    <TextView
        android:id="@+id/password_textview"
        style="@style/TextAppearance.Label"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginStart="@dimen/default_margin"
        android:layout_marginTop="@dimen/default_margin"
        android:text="@string/Music_Screen_Password_Label"
        app:layout_constraintLeft_toLeftOf="@+id/auth_background_view"
        app:layout_constraintTop_toBottomOf="@+id/login_edittext" />

    <EditText
        android:id="@+id/login_edittext"
        style="@style/TextAppearance.Edition"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_marginEnd="@dimen/default_margin"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginRight="@dimen/default_margin"
        android:layout_marginStart="@dimen/default_margin"
        android:hint="@string/Music_Screen_User_Hint"
        app:layout_constraintBaseline_toBaselineOf="@+id/login_textview"
        app:layout_constraintLeft_toRightOf="@+id/password_textview"
        app:layout_constraintRight_toRightOf="@+id/auth_background_view" />

    <EditText
        android:id="@+id/password_edittext"
        style="@style/TextAppearance.Edition"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_marginEnd="@dimen/default_margin"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginRight="@dimen/default_margin"
        android:layout_marginStart="@dimen/default_margin"
        android:hint="@string/Music_Screen_Password_Hint"
        android:inputType="textPassword"
        app:layout_constraintBaseline_toBaselineOf="@+id/password_textview"
        app:layout_constraintLeft_toRightOf="@+id/password_textview"
        app:layout_constraintRight_toRightOf="@+id/auth_background_view" />

    <TextView
        android:id="@+id/advice_textview"
        style="@style/TextAppearance.Indication"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="@dimen/default_margin"
        android:layout_marginRight="@dimen/default_margin"
        android:layout_marginTop="@dimen/small_margin"
        android:layout_marginLeft="@dimen/large_margin"
        android:paddingBottom="@dimen/default_margin"
        android:text="@string/Music_Screen_Password_Indication"
        app:layout_constraintLeft_toLeftOf="@+id/password_edittext"
        app:layout_constraintRight_toRightOf="@+id/auth_background_view"
        app:layout_constraintTop_toBottomOf="@+id/password_textview" />
</android.support.constraint.ConstraintLayout>

 

Rendu sur l’éditeur de layout :

Drawing

 

 

Comme vous pouvez le voir, le code est globalement similaire. Seuls les attributs liés au positionnement ont été changés par les contraintes du ConstraintLayout. Mais il y a deux différences notables.

 

La première, qui se voyait déjà sur le tableau comparatif : il n’y a pas d’équivalent direct au “layout_alignParent(X)” du RelativeLayout.

Pour en reproduire le fonctionnement, il faut placer une contrainte d’alignement “layout_constraint(X)_to(X)of” vers le ViewGroup parent. Il n’est d’ailleurs pas nécessaire de faire appel à l’Id du layout parent : il suffit d’utiliser le mot-clé “parent”, et le ConstraintLayout saura automatiquement le retrouver.

 

La seconde, c’est que nos “match_parent” ont été remplacés par un “0dp” et que des contraintes ont été ajoutées sur certains widgets. Et cela est dû à un changement particulier de la gestion de nos dimensions, que nous allons voir maintenant.

 

La gestion des dimensions : mesurez vos widgets en fonction de vos contraintes

Le MATCH_CONSTRAINT

 

Comme je disais : il n’y a pas de match_parent sur nos widgets.

En effet, la gestion des dimensions est quelque peu différente sur le ConstraintLayout. Le match_parent ne s’utilise pas avec lui, et ne doit pas être utilisé.
Si on veut que notre widget occupe tout l’espace disponible, il faut utiliser le principe du MATCH_CONSTRAINT.

 

Ne cherchez pas le mot-clé dans l’autocomplétion comme pour le match_parent, il n’existe pas. Il s’agit en réalité d’une pratique.

Pour l’utiliser, il faut placer deux contraintes sur un même axe, puis indiquer une taille de 0dp sur l’axe concerné. Appliqué à l’axe horizontal par exemple, on placera une contrainte à droite, une à gauche et l’attribut layout_width à 0dp.

 

Ce fonctionnement peut sembler plus complexe mais il est également plus souple : si le match_parent permet d’occuper l’espace sur le ViewGroup parent, le MATCH_CONSTRAINT, lui, permet d’occuper l’espace entre deux contraintes.

 

Si on reprend le code précédent, vous remarquerez que l’ImageView de background fait un MATCH_CONSTRAINT en plaçant ses contraintes à droite et à gauche du ViewGroup parent (axe horizontal), mais fait un autre MATCH_CONSTRAINT entre le bas du header et le bas du texte de conseil (axe vertical) :

 

<ImageView
        android:id="@+id/auth_background_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="@dimen/default_margin"
        android:background="@drawable/rounded_shadows"
        app:layout_constraintBottom_toBottomOf="@+id/advice_textview"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/header_imageview" />

 

(Note : cette technique n’est en réalité pas inédite, car il est déjà possible de le faire avec un RelativeLayout. D’ailleurs, elle était déjà utilisée pour le background dans notre exemple. Toutefois, le ConstraintLayout donne un nom à cette pratique et la rend plus fiable).

 

Le Dimension Ratio

 

Maintenant, voyons une autre façon de déterminer la taille d’un widget : sur notre layout, nous avons une image principale qui sert de header et accompagne le titre :

 

Drawing

 

L’image d’origine a un ratio de 2 pour 1, et ça tombe bien puisque c’est le ratio que l’on veut sur l’écran. On veut que l’image prenne toute la largeur de l’écran :

 

<ImageView
     android:id="@+id/header_imageview"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:contentDescription="@string/Music_Screen_Title"
     android:scaleType="centerCrop"
     android:src="@drawable/header_placerholder" />

 

Pour conserver le ratio de l’image, on met la hauteur en “wrap_content“. Seulement, que se passe-t-il si l’image source change de ratio ? Si, par exemple, on décide de changer pour une image 16/9, mais que l’on veut conserver un ratio de 2/1 ?

Exemple ici : si on change l’image du header par une autre qui a une hauteur beaucoup plus petite, voici ce qu’il arrive :

 

Drawing

 

Le titre n’a même plus assez de place pour apparaître correctement.

C’est un problème récurrent, notamment lorsque l’on affiche une image téléchargée (avec Picasso par exemple) et que l’on n’a aucun moyen de garantir le ratio de l’image source.

 

Alors comment faire ?

 

Le ContraintLayout apporte un attribut qui permet de résoudre ce problème : le Dimension Ratio.
L’attribut s’écrit “layout_DimensionRatio” dans le XML, et prend comme valeur un ratio au format “(width):(height)”.

L’idée est de placer une des tailles à 0dp et l’autre avec une valeur, et le Dimension Ratio se charge du reste. Il adaptera automatiquement la taille du widget pour correspondre au ratio indiqué.

 

Un exemple de ce que cela peut donner niveau syntaxe :

 

    <ImageView
        android:layout_width="50dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="3:1"
        android:src="@drawable/une_image" />

 

Ici, on indique à l’ImageView d’adapter sa hauteur pour qu’elle soit de trois fois celle de la largeur.

 

Mise en application sur l’image d’entête

 

Maintenant, on aimerait se servir de ces deux éléments (MATCH_CONSTRAINT et Dimension Ratio) pour faire prendre à notre image la largeur de l’écran et lui donner un ratio de 2/1. Sauf qu’il y a un problème : la taille qui est censée nous donner une valeur est en MATCH_CONSTRAINT, et donc à 0dp. Si les deux tailles sont à 0dp, comment le ratio peut savoir sur quelle taille se baser ?

C’est simple : il suffit de lui indiquer. Il faut précéder la valeur de ratio par une indication sur la taille voulue (H pour la hauteur, W pour la largeur).

Voyons ce que ça donne :

 

    <ImageView
        android:id="@+id/header_imageview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:contentDescription="@string/Music_Screen_Title"
        android:scaleType="centerCrop"
        android:src="@drawable/header_placerholder"
        app:layout_constraintDimensionRatio="H, 2:1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

 

A présent, voyons ce qui se passe si on change d’image :

 

Drawing

 

Le ratio est respecté !

 

Les Guidelines : des lignes directrices pour positionner vos widgets

 

Vous aurez peut-être pu remarquer que, dans notre exemple, on utilise de nombreuses marges. D’ailleurs, c’est presque toujours la même (@dimen/default_space, qui correspond à 12dp). Elle est notamment appliquée à toutes les ImageView de background.

En temps normal, pour éviter la répétition, on pourrait utiliser un padding dans le layout parent pour éviter d’avoir à répéter cette marge à chaque widget, mais l’image principale nous en empêche : elle doit prendre toute la largeur de l’écran.

 

C’est là que les Guidelines ont leur rôle à jouer.

 

La Guideline est un nouveau composant graphique, de la catégorie des “Virtual component”, qui accompagne le ConstraintLayout. Ces composants sont, comme leur nom l’indique, virtuels. Ce qui fait qu’ils n’apparaissent pas sur l’écran.

La Guideline (en français “ligne directrice”) permet de positionner une ligne de référence, verticalement ou horizontalement, vers laquelle vos widgets peuvent établir une contrainte.

 

Il existe trois contraintes spécifiques aux Guidelines, pour trois façons de la positionner :

  • layout_constraintGuide_begin (par rapport à la gauche / au haut du layout parent)
  • layout_constraintGuide_end (par rapport à la droite / au bas du layout parent)
  • layout_constraintGuide_percent (par rapport à un pourcentage de l’espace fourni par le layout parent)

L’attribut “orientation” est obligatoire, afin de déterminer l’axe sur lequel la Guideline s’applique.

 

Voici deux exemples de Guideline sur l’éditeur : la première est verticale et placée à 40dp (layout_constraintGuide_begin) et la seconde est horizontale et placée à 30% de la hauteur du layout (layout_constraintGuide_percent) :

 

Drawing

 

Ce composant est très utile pour répondre aux besoins les plus précis d’une maquette d’écran, définir des marges ou même pouvoir déplacer dynamiquement un ensemble d’éléments au runtime (en changeant la valeur de la contrainte de la Guideline).

Petite anecdote : la Guideline a été créée et nommée ainsi car elle s’inspire des tracés que font les designers d’application sur leurs maquettes.

 

Mise en application : les images de background

 

A présent, jouons un peu avec ce nouvel objet.

Ajoutons deux Guidelines de chaque côté de notre layout afin de définir des marges, puis adaptons nos widgets pour qu’ils placent une contrainte vers ces Guidelines plutôt que sur le ViewGroup parent :

 

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingBottom="@dimen/default_margin">

        <!--Guideline de gauche, avec une marge de 12dp-->
        <android.support.constraint.Guideline
        android:id="@+id/left_guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_begin="@dimen/default_margin" />

        <!--Guideline de droite, avec une marge de 12dp-->
        <android.support.constraint.Guideline
        android:id="@+id/right_guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_end="@dimen/default_margin" />

                ....

        <!--Authentification block-->

        <!--Les contraintes de droite et de gauche se font par rapport aux Guidelines. De cette façon,
        il n'est pas nécessaire de répéter les marges à chaque élément.-->
        <ImageView
        android:id="@+id/auth_background_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="@dimen/default_margin"
        android:background="@drawable/rounded_shadows"
        app:layout_constraintBottom_toBottomOf="@+id/advice_textview"
        app:layout_constraintLeft_toLeftOf="@+id/left_guideline"
        app:layout_constraintRight_toRightOf="@+id/right_guideline"
        app:layout_constraintTop_toBottomOf="@+id/header_imageview" />

                .... 

        <!--bloc de consommation-->

        <!--Idem ici : le background est placé par rapport aux Guidelines-->
        <ImageView
        android:id="@+id/consumption_background_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/rounded_reverse_shadows"
        app:layout_constraintBottom_toBottomOf="@+id/consumption_start_textview"
        app:layout_constraintLeft_toLeftOf="@+id/left_guideline"
        app:layout_constraintRight_toLeftOf="@+id/right_guideline"
        app:layout_constraintTop_toBottomOf="@+id/consumption_title_textview" />
</android.support.constraint.ConstraintLayout>

 

Comme vous pouvez voir dans le code, on utilise pour la Guideline de droite la contrainte “layout_constraintGuide_begin” avec une valeur de marge de 12dp et pour celle de gauche la contrainte “layout_constraintGuide_end” avec la même valeur. L’attribut “orientation”, toujours obligatoire, est à “vertical” ici.

 

Il s’agit là d’une utilisation possible pour les Guidelines. Une troisième utilisation, comme vu plus haut, peut être faite avec la contrainte “layout_constraintGuide_percent”.

Un exemple avec l’image qui sert de header : imaginons un instant que notre designer change d’avis après avoir vu le résultat d’un ratio. Il veut à présent que le widget prenne un certain pourcentage de la hauteur du layout, disons 20%. Peu importe le type d’écran et le ratio de l’image source. Dans ce cas, on peut placer une Guideline horizontale vers laquelle l’ImageView établira une contrainte en MATCH_CONSTRAINT :

 

        <!--Guideline horizontale placée à 20% de l'écran-->
        <android.support.constraint.Guideline
        android:id="@+id/header_guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="hor"
        app:layout_constraintGuide_end="@dimen/default_margin" />

        <!--Header-->

        <!--Utilisation de la Guideline horizontale en MATCH_CONSTRAINT pour définir la limite basse du Widget. -->
        <ImageView
        android:id="@+id/header_imageview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:contentDescription="@string/Music_Screen_Title"
        android:scaleType="centerCrop"
        android:src="@drawable/header_placerholder"
        app:layout_constraintBottom_toBottomOf="@id/header_guideline"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

 

A suivre

 

Nous venons de transformer un RelativeLayout en ConstraintLayout. Le nouveau layout est parfaitement utilisable, et d’ailleurs vous pouvez en retrouver le code intégral dans le fichier fragment_music_constraint_in_progress.xml.

 

Mais en regardant bien ce fichier, on peut se rendre compte qu’il reste pas mal d’optimisation à apporter. Notamment, plusieurs LinearLayouts y sont présents pour gérer des cas qui ne peuvent pas être gérés autrement avec un RelativeLayout.

 

Vous l’aurez compris : ce seront ces éléments que nous verrons dans la seconde partie. On y parlera également de l’éditeur graphique, ainsi que des quelques limites qui existent encore au ConstraintLayout.

© SOAT
Toute reproduction interdite sans autorisation de la société SOAT

Nombre de vue : 1390

AJOUTER UN COMMENTAIRE