Intermédiaire

Transformez vos layouts en ConstraintLayout – Partie 2

Lors de l’article précédent (que je vous conseille très fortement de lire avant de commencer cet article), nous avons découvert ce qu’était le ConstraintLayout et commencé à voir comment transformer un RelativeLayout en ConstraintLayout. Cela nous a permis également de découvrir comment faire certaines optimisations, comme l’utilisation des Guidelines pour éviter la répétition des marges, ainsi que la mise en place de ratios et d’une gestion plus simple des dimensions grâce aux Dimension Ratio et au MATCH_CONSTRAINT.

 

Cependant le ConstraintLayout permet d’aller encore plus loin. Un des problèmes que nous n’avons pas encore résolus est l’utilisation des LinearLayout dans un RelativeLayout. Nous sommes souvent obligés d’en intégrer dans un RelativeLayout pour gérer des placements par pourcentage ou encore une répartition dynamique des widgets dans l’espace.

 

Entrons maintenant dans le vif du sujet et voyons comment le ConstraintLayout permet d’optimiser son code afin d’obtenir une hiérarchie plate et de régler certaines problématiques que nous ne pouvions pas résoudre jusque-là. Même si, nous le verrons, le ConstraintLayout a ses limites.

 

Objectif : layout plat !

 

Pour rappel, dans l’article précédent, nous avons utilisé un exemple fictif d’application de streaming musical. Plus particulièrement, une page permettant à un utilisateur de consulter la consommation musicale liée à son compte.

Cet exemple est disponible sur le GitHub de Soat.

 

Note : cet exemple reprend beaucoup de cas de figure permettant de présenter des fonctionnalités du ConstraintLayout. Il n’en couvre pas l’intégralité pour autant. Notamment, ici on ne parlera pas de la gestion du Gone ou de certaines possibilités offertes par les contraintes (par exemple, il est possible de centrer un widget par rapport au bord d’un autre widget). Pour aller plus loin, je vous conseille de lire la documentation officielle.

 

La composition de l’écran était la suivante :

 

Drawing

 

Nous nous étions surtout intéressés au header et au bloc d’authentification. Maintenant, voyons comment transformer les blocs suivants.

 

Les bias : un positionnement encore plus précis

 

Une des problématiques récurrentes avec l’alignement des widgets, c’est le placement relatif. On utilise principalement les dp pour caler les éléments les uns par rapport aux autres, ce qui peut parfois avoir des effets indésirables en fonction de la taille de l’écran. Ces problèmes pourraient être résolus avec un placement sous forme de pourcentage.

C’est ici que les bias entrent en jeu : le bias est une unité de placement d’un widget entre deux contraintes, exprimée en pourcentage.

 

Le principe est assez proche du MATCH_CONSTRAINT : le widget doit avoir deux contraintes opposées (droite/gauche ou haut/bas). Mais au lieu d’avoir une taille à 0dp, qui faisait s’étendre le widget entre les deux contraintes, on lui donne une taille concrète : soit un wrap_content, soit une valeur en dp. Le widget se retrouve alors à mi-chemin entre les deux contraintes par défaut.

A partir de là, grâce au bias, on peut définir un alignement plus précis. Les contraintes qui permettent de les définir sont “layout_constraintHorizontalBias” et “layout_constraintVerticalBias” en fonction de l’axe souhaité.

 

Voici un exemple simple d’utilisation :

 

<android.support.constraint.ConstraintLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon_vinyl"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintHorizontal_bias="0.7"/>

</android.support.constraint.ConstraintLayout>

 

Drawing

 

Ici, on a défini une ImageView, contrainte au parent sur l’axe horizontal (contrainte à gauche et contrainte à droite), avec comme taille en largeur “wrap_content” et un bias horizontal défini à 0.7, ce qui place le centre du widget à 70% de l’écran.

A l’instar des Guidelines, le pourcentage s’exprime en décimal entre 0 et 1 .

Autre point : la valeur du bias part toujours du haut du widget (vertical) ou de sa gauche (horizontal). Le bias horizontal ici a une valeur de 0.7 car on veut être à 30% de la droite de l’écran, donc à 70% de la gauche.

 

Les bias peuvent également se manipuler dans l’éditeur, grâce aux curseurs situés sur le cadran des dimensions :

 

Drawing

 

Voyons à présent comment appliquer ce principe à notre exemple.

 

Mise en application : le titre de la page et le curseur de consommation

 

Drawing

 

Partons d’abord sur un exemple simple : nous avons un titre associé à notre header, et notre designer voudrait idéalement que le titre soit aligné sur le coin en bas à gauche de l’image, peu importe sa taille. Avec 20% de marge.

Avec le RelativeLayout, vous auriez été obligés de lui expliquer que ce n’est pas possible (ou bien si, en passant par des mesures de l’écran dans le code, mais soyons honnêtes : personne n’a envie de passer par des mesures dans le code). Dans notre exemple, nous sommes d’ailleurs passés par de simples marges :

 

<!-- Le titre est aligné en bas à gauche de l'image principale et décalé avec des marges.
        Cependant si le ratio de l'image change, les marges n'aligneront plus le titre correctement. -->
    <TextView
        android:id="@+id/title_textview"
        style="@style/TextAppearance.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@id/header_imageview"
        android:layout_marginBottom="@dimen/large_margin"
        android:layout_marginLeft="@dimen/large_margin"
        android:text="@string/Music_Screen_Title" />

 

Mais ce n’est pas une solution pérenne car, en fonction de la taille du device, le rendu pourra être très différent. Les bias nous offrent une solution bien plus élégante ici :

 

<!-- Utilisation d'un bias vertical et horizontal. On place le titre à 20% de la largeur du header
        et à 80% de la hauteur du header. -->
    <TextView
        android:id="@+id/title_textview"
        style="@style/TextAppearance.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/Music_Screen_Title"
        app:layout_constraintBottom_toBottomOf="@+id/header_imageview"
        app:layout_constraintHorizontal_bias="0.2"
        app:layout_constraintLeft_toLeftOf="@+id/header_imageview"
        app:layout_constraintRight_toRightOf="@+id/header_imageview"
        app:layout_constraintTop_toTopOf="@+id/header_imageview"
        app:layout_constraintVertical_bias="0.8" />

 

 

Autre exemple : concentrons-nous à présent sur la partie du layout gérant la consommation musicale.

 

Drawing

 

On voudrait placer un curseur sur une ligne pour indiquer à l’utilisateur le pourcentage de musique qu’il a écouté par rapport au seuil maximum défini par son compte.

Sauf qu’avec un RelativeLayout, on ne sait pas gérer un placement en pourcentage. On utilise donc un LinearLayout avec une combinaison de poids, ce qui donne ça :

 

<!-- On utilise un LinearLayout avec un widget Space et un poids dessus, afin de
        placer l'ImageView au bon endroit (pourcentage par rapport à la largeur de l'écran).
        Le poids du Space sera modifié dynamiquement dans le code. -->
<LinearLayout
        android:id="@+id/consumption_marker_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/consumption_title_textview"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginRight="@dimen/default_margin"
        android:layout_marginTop="@dimen/default_margin"
        android:orientation="horizontal"
        android:weightSum="1">

        <Space
            android:id="@+id/consumption_marker_position"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="0.6" />

        <ImageView
            android:id="@+id/consumption_marker_imageview"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/Content_Desc_Pin_Indication"
            android:src="@drawable/icon_pin" />
</LinearLayout> 

 

Ce n’est bien sûr pas l’idéal. On est obligé d’ajouter un nouveau niveau de hiérarchie, et le principe ressemble plus à un hack qu’à une véritable construction de layout.

Voyons ce que cela donne avec un bias :

 

<!--Utilisation du bias pour positionner le marqueur par rapport à un pourcentage de l'écran.
        Le biais est modifié dynamiquement dans le code.-->
    <ImageView
        android:id="@+id/consumption_marker_imageview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/default_margin"
        android:contentDescription="@string/Content_Desc_Pin_Indication"
        android:src="@drawable/icon_pin"
        tools:layout_constraintHorizontal_bias="0.6"
        app:layout_constraintLeft_toLeftOf="@+id/left_guideline"
        app:layout_constraintRight_toLeftOf="@+id/right_guideline"
        app:layout_constraintTop_toBottomOf="@+id/consumption_title_textview" />

(Note : ici, on utilise le namespace “tools” pour l’attribut “layout_constraintHorizontal_bias“. Ce namespace permet d’indiquer que cet attribut n’est utilisé que sur la preview d’Android Studio).

 

Bien sûr, cette valeur sera à modifier dans le code, en fonction de la consommation de l’utilisateur. Ce qui va nous donner l’occasion de voir un peu comment les contraintes peuvent être modifiées durant le runtime.

 

Modifier ses contraintes dynamiquement

 

Lorsqu’il s’agit de modifier la configuration d’un ViewGroup dans le code, une classe vient vite en tête : la classe LayoutParams. Cette “inner class”, qui est surchargée par chaque ViewGroup, contient la majorité des propriétés qui les définissent : la gravité, les marges pour la classe ViewGroup, les poids pour les LinearLayout ou encore les règles d’alignement pour un RelativeLayout.

Le ConstraintLayout aussi possède sa classe LayoutParams. Cependant, il est déconseillé de l’utiliser. A la place, je vous propose un autre objet qui accompagne le ConstraintLayout : le ConstraintSet.

 

Le ConstraintSet ressemble au LayoutParams, à ceci près qu’il n’est pas contenu par le ConstraintLayout. C’est même plutôt l’inverse : le ConstraintSet permet de contenir les contraintes d’un ConstraintLayout.

Un exemple sera plus parlant pour expliquer la différence. Notre objectif sur le bloc de consommation est de pouvoir changer la position de notre curseur en fonction d’un pourcentage. Voici comment on fait :

 

Avec le LinearLayout :

On passe ici par les LayoutParams du LinearLayout : on crée un nouvel objet qui contient les attributs du ViewGroup, on en change le poids puis on donne ce nouvel objet au LinearLayout.

 

private View consumptionMarkerPosition;

@Override
protected void initConsumptionValue(float consumptionPercentage) {
LinearLayout.LayoutParams newLayoutParams = new LinearLayout.LayoutParams(consumptionMarkerPosition.getLayoutParams());
newLayoutParams.weight = consumptionPercentage;
consumptionMarkerPosition.setLayoutParams(newLayoutParams);
}

 

Avec le ConstraintLayout :

Avec un ConstraintSet, l’opération est similaire tout en étant légèrement différente : d’abord on clone les contraintes dans le ConstraintSet, puis on les modifie, puis on ré-applique le contenu du ConstraintSet sur le ConstraintLayout.

 

private ConstraintLayout mainLayout;

@Override
protected void initConsumptionValue(float consumptionPercentage) {
ConstraintSet set = new ConstraintSet();
set.clone(mainLayout); // On "clone" toutes les contraintes du ConstraintLayout dans le ConstraintSet
set.setHorizontalBias(consumption_marker_imageview, consumptionPercentage); // On modifie la valeur du bias
set.applyTo(mainLayout); // Puis on applique le contenu du set sur le ConstraintLayout
}

 

L’avantage de ce système est que l’on a à présent un objet qui contient une liste de contraintes indépendantes du ConstraintLayout lui-même (et qui contient uniquement une liste de contraintes, contrairement au LayoutParams qui hérite de toutes les propriétés de ses parents). Ces contraintes peuvent donc être appliquées à un ou plusieurs autres ConstraintLayout, ou conservées pour être réappliquées ultérieurement, ce qui est pratique pour gérer des animations par exemple.

Bien sûr, toutes les contraintes peuvent être modifiées de la sorte : les contraintes de placement, les ratios, les contraintes des Guidelines… ainsi que les propriétés de la fonctionnalité que nous allons voir maintenant.

 

Les chaînes : les avantages du LinearLayout

 

Il ne reste plus grand chose à revoir sur notre exemple à présent. Mais les derniers morceaux nous donnent du fil à retordre : il y a les boutons liés à l’authentification, et surtout le gros bloc affichant la dernière musique en bas de l’écran.

Les deux sont faits à l’aide de LinearLayout afin que les widgets se répartissent équitablement sur l’espace disponible. En effet, si on veut par exemple qu’un ensemble de widgets soit centré par rapport à un autre, le LinearLayout est bien plus indiqué : il suffit d’utiliser l’attribut “gravity” avec la valeur “center“.

 

Pour résoudre ça, il est temps de voir un des derniers tours disponibles dans le sac du ConstraintLayout : les chaînes (ou chains en anglais).

 

Les chaînes sont un moyen de lier des widgets ensemble et de les faire se répartir entre deux contraintes. Dit autrement : elles forment un groupe entre plusieurs widgets sur un axe donné.

Elles fonctionnent grâce à une suite de contraintes réciproques : pour former une chaîne entre deux widgets, il faut que le premier établisse une contrainte vers le second, et que le second établisse la contrainte réciproque vers la première.

 

Un exemple illustré sera plus parlant :

 

Drawing

 

La chaîne existe à partir du moment où A fait une contrainte vers B et B une contrainte vers A.

 

Mise en application : les boutons d’authentification

 

Drawing

 

Pour comprendre l’intérêt des chaînes, retournons sur les deux boutons liés à l’authentification : le bouton “Réinitialiser” et “Valider”. Ceux-ci doivent se répartir dans la largeur, sans que la taille du bouton ne soit modifiée. Aussi, avec un RelativeLayout, nous utilisons un LinearLayout avec des objets Space et des poids, un peu comme pour le curseur de consommation vu plus haut :

 

<LinearLayout
        android:id="@+id/button_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/advice_textview"
        android:gravity="center">

        <Space
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <Button
            android:id="@+id/reset_button"
            style="@style/FlatButton.Primary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="@dimen/default_margin"
            android:paddingRight="@dimen/default_margin"
            android:text="@string/Music_Screen_Reset_Button" />

        <Space
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <Button
            android:id="@+id/validation_button"
            style="@style/FlatButton.Primary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="@dimen/default_margin"
            android:paddingRight="@dimen/default_margin"
            android:text="@string/Music_Screen_Validation_Button" />

        <Space
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
</LinearLayout>

 

L’idée ici est que les trois widgets “Space” se répartissent l’espace restant dans le LinearLayout (Les deux boutons étant en wrap_content occupent déjà une partie de l’espace qui est non réductible). Chaque Space a un poids de 1, ce qui fait qu’ils ont tous la même taille.

Le problème est assez évident : l’utilisation des Space et d’un LinearLayout ajoutent un temps de traitement supplémentaire et la lisibilité est mauvaise.

Utilisons à présent les chaînes pour voir en quoi elles résolvent le problème :

 

<Button
        android:id="@+id/reset_button"
        style="@style/FlatButton.Primary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="@dimen/default_margin"
        android:paddingRight="@dimen/default_margin"
        android:text="@string/Music_Screen_Reset_Button"
        app:layout_constraintLeft_toLeftOf="@+id/left_guideline"
        app:layout_constraintRight_toLeftOf="@+id/validation_button"
        app:layout_constraintTop_toBottomOf="@+id/auth_background_view" />

    <Button
        android:id="@+id/validation_button"
        style="@style/FlatButton.Primary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="@dimen/default_margin"
        android:paddingRight="@dimen/default_margin"
        android:text="@string/Music_Screen_Validation_Button"
        app:layout_constraintLeft_toRightOf="@+id/reset_button"
        app:layout_constraintRight_toLeftOf="@+id/right_guideline"
        app:layout_constraintTop_toBottomOf="@+id/auth_background_view" />

 

Dans cet exemple, le bouton de réinitialisation fait une “constraintRight_toLeftOf” vers le bouton de validation, et le bouton de validation une “constraintLeft_toRightOf” (la contrainte réciproque) vers le bouton de réinitialisation.

L’effet de cette chaîne est la répartition de nos deux widgets sur tout l’espace disponible. Plus besoin de Space, plus besoin de LinearLayout : le comportement existe grâce à la chaîne.

 

En réalité, le comportement de répartition des widgets est configurable, grâce à la contrainte “layout_constraintHorizontal_chainStyle“. Cette contrainte s’applique au premier widget de la chaîne, et a comme valeur par défaut “spread” (ce qui donne l’effet vu précédemment).

 

Maintenant, si on veut que les widgets soient regroupés au centre, et non plus répartis dans l’espace ? C’est simple, il suffit de lui donner la valeur “packed“.

Voici ce que ça donnerait si on l’appliquait à notre exemple :

 

Drawing

 

Il existe de nombreuses valeurs pour configurer la répartition de vos widgets sur une chaîne. Je vous suggère de jeter un oeil à la documentation du ConstraintLayout pour connaître les différentes configurations possibles.

 

Dernier point non négligeable et qui termine ma comparaison avec le LinearLayout : il est possible d’utiliser des poids sur vos widgets dans une chaîne ! Il suffit d’utiliser le même attribut que pour le LinearLayout (layout_weight) pour accorder plus ou moins d’espace à vos widgets (en sachant que, comme ailleurs, ceux-ci peuvent être mis en MATCH_CONSTRAINT).

 

Et cette possibilité, nous allons pouvoir la mettre à l’épreuve dans la dernière partie.

 

Combinaison de plusieurs contraintes différentes

 

Ce chapitre va nous permettre de voir comment les différentes contraintes se combinent et comment elles agissent ensemble.

Nous allons pour cela nous attaquer au bloc “dernière musique”. Ce bloc, dont nous parlions plus tôt, est complexe à réaliser de par son design. Complexité que nous allons pouvoir diminuer grâce aux différentes contraintes que nous avons pu voir jusque-là.

 

Drawing

 

Le bloc d’origine est entièrement fait avec des LinearLayout. Pas le choix, vu les consignes de design associées à ce bloc :

  • Le bloc est divisé en trois : la pochette d’album, les infos principales (nom de la chanson, auteur et nom de l’album) et les infos secondaires (durée, année de parution et genre).
  • La pochette d’album doit avoir la même hauteur que bloc d’info principal et doit être carrée.
  • Le bloc d’infos principal prend toute la largeur disponible entre la pochette d’album et les infos secondaires.
  • Le bloc d’infos secondaire a la même hauteur que le bloc d’infos principal et ses éléments se répartissent sur cette hauteur.

Ces consignes sont impossibles à respecter avec un RelativeLayout seul, du moins si on veut avoir un layout qui s’adapte sur différentes tailles d’écran (ce qui est un peu l’objectif, quand même).

Nous allons donc passer par une série de layouts imbriqués :

 

<LinearLayout
        android:id="@+id/last_song_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/last_song_title_textview"
        android:layout_marginBottom="@dimen/default_margin"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginRight="@dimen/default_margin"
        android:background="@drawable/rounded_reverse_shadows"
        android:orientation="horizontal"
        android:padding="@dimen/default_margin">

        <ImageView
            android:id="@+id/last_song_cover"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:layout_marginRight="@dimen/default_margin"
            android:contentDescription="@string/Content_Desc_Album_Picture"
            tools:src="@drawable/icon_vinyl" />

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical">

            <TextView
                android:id="@+id/last_song_name"
                style="@style/TextAppearance.Info.Primary"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                tools:text="Winter's Wolves" />

            <TextView
                android:id="@+id/last_song_band"
                style="@style/TextAppearance.Info.Secondary"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                tools:text="The Sword" />

            <TextView
                android:id="@+id/last_song_album"
                style="@style/TextAppearance.Info.Secondary"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                tools:text="Age of Winters" />

        </LinearLayout>

        <ImageView
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="@dimen/default_margin"
            android:layout_marginRight="@dimen/default_margin"
            android:background="@android:color/white" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="center_vertical"
            android:orientation="vertical">

            <TextView
                android:id="@+id/last_song_duration"
                style="@style/TextAppearance.Info"
                android:layout_width="wrap_content"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:gravity="top"
                tools:text="Durée : 3 min 56" />

            <TextView
                android:id="@+id/last_song_year"
                style="@style/TextAppearance.Info"
                android:layout_width="wrap_content"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:gravity="center"
                tools:text="Année : 2016" />

            <TextView
                android:id="@+id/last_song_type"
                style="@style/TextAppearance.Info"
                android:layout_width="wrap_content"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:gravity="bottom"
                tools:text="Genre : Death Metal" />

        </LinearLayout>
</LinearLayout>

 

Voilà, deux niveaux de hiérarchie de vues en plus et une pochette d’album avec un ratio variable (l’image de la pochette est téléchargée via Picasso).

Vous savez ce qu’il nous reste à faire maintenant ! Nous allons passer en ConstraintLayout, et pouvoir réutiliser toutes les techniques que l’on a pu observer jusqu’à maintenant :

 

<!-- Utilisation d'un bias horizontal pour placer le titre à 10% de la largeur du bloc.-->
    <TextView
        android:id="@+id/last_song_title_textview"
        style="@style/TextAppearance.Subtitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/large_margin"
        android:background="@drawable/rounded_top_shadows"
        android:text="@string/Music_Screen_Last_Song_Subtitle"
        app:layout_constraintLeft_toLeftOf="@+id/left_guideline"
        app:layout_constraintHorizontal_bias="0.1"
        app:layout_constraintTop_toBottomOf="@+id/consumption_start_textview"
        app:layout_constraintRight_toLeftOf="@+id/right_guideline" />

    <!-- Utilisation d'un ImageView pour gérer le background, comme pour les blocs précédents. 
        Les contraintes gauche et droite sont alignées sur les Guidelines définies précédemment. 
        Le bloc d'information principal (titre, groupe et album) sert de référence en hauteur pour le background.-->
    <ImageView
        android:id="@+id/last_song_background"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/rounded_reverse_shadows"
        app:layout_constraintBottom_toBottomOf="@+id/last_song_main_layout"
        app:layout_constraintLeft_toLeftOf="@+id/left_guideline"
        app:layout_constraintRight_toLeftOf="@+id/right_guideline"
        app:layout_constraintTop_toBottomOf="@+id/last_song_title_textview" />

   <!-- La pochette d'album utilise un dimension ratio de 1:1, calculée sur la largeur (préfixe "W" devant le ratio). 
     La hauteur est calculée à partir de celle du bloc d'information principal (titre, groupe et album). 
     On applique également le chainStyle ici vu qu'il s'agira du premier élément de la chaîne à venir
     (note : il n'est pas obligatoire vu que l'on veut configurer la chaîne en "spread" et qu'il s'agit de la valeur par défaut). -->
    <ImageView
        android:id="@+id/last_song_cover"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_gravity="center"
        android:layout_marginBottom="@dimen/default_margin"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginTop="@dimen/default_margin"
        android:contentDescription="@string/Content_Desc_Album_Picture"
        android:src="@drawable/icon_vinyl"
        app:layout_constraintBottom_toBottomOf="@+id/last_song_background"
        app:layout_constraintDimensionRatio="W,1:1"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintLeft_toLeftOf="@+id/last_song_background"
        app:layout_constraintRight_toLeftOf="@+id/last_song_main_layout"
        app:layout_constraintTop_toTopOf="@+id/last_song_background"
        android:layout_marginStart="@dimen/default_margin" />

   <!-- Ce LinearLayout est le deuxième élément de la chaîne, à qui on donne un poids de 1 pour qu'il prenne toute la largeur restante sur la chaîne. 
     A noter que, comme dans un LinearLayout, la largeur est définie à 0dp puisqu'elle est gérée par le poids. 
     L'attribut ne peut être utilisé qu'au sein d'une chaîne. On est obligé d'utiliser des LinearLayout par la suite pour que la chaîne horizontale 
     s'adapte à la largeur maximale entre les trois éléments (titre, groupe et album). 
     De plus, cet élément détermine la taille en hauteur du background et de la pochette d'album. Il n'est donc pas encore possible de s'en passer.-->
    <LinearLayout
        android:id="@+id/last_song_main_layout"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="@dimen/default_margin"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintLeft_toRightOf="@+id/last_song_cover"
        app:layout_constraintRight_toLeftOf="@+id/last_song_separator"
        app:layout_constraintTop_toTopOf="@+id/last_song_background">

        <TextView
            android:id="@+id/last_song_name"
            style="@style/TextAppearance.Info.Primary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="Freya" />

        <TextView
            android:id="@+id/last_song_band"
            style="@style/TextAppearance.Info.Secondary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="The Sword" />

        <TextView
            android:id="@+id/last_song_album"
            style="@style/TextAppearance.Info.Secondary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="Age of Winters" />

    </LinearLayout>

        <!-- Troisième élément de la chaîne servant de séparateur entre les blocs d'infos principales et secondaires.-->
    <ImageView
        android:id="@+id/last_song_separator"
        android:layout_width="1dp"
        android:layout_height="0dp"
        android:layout_marginBottom="@dimen/default_margin"
        android:layout_marginTop="@dimen/default_margin"
        android:background="@android:color/white"
        app:layout_constraintBottom_toBottomOf="@+id/last_song_background"
        app:layout_constraintLeft_toRightOf="@+id/last_song_main_layout"
        app:layout_constraintRight_toLeftOf="@+id/last_song_second_layout"
        app:layout_constraintTop_toTopOf="@+id/last_song_background" />

    <!-- Quatrième et dernier élément de la chaîne, un LinearLayout à nouveau, avec les infos secondaires. Même remarque que précédemment : pour que la chaîne s'adapte correctement en largeur, ces éléments doivent se trouver dans un même layout.-->
    <LinearLayout
        android:id="@+id/last_song_second_layout"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_margin="@dimen/default_margin"
        android:gravity="center_vertical"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="@+id/last_song_background"
        app:layout_constraintLeft_toRightOf="@+id/last_song_separator"
        app:layout_constraintRight_toRightOf="@+id/last_song_background"
        app:layout_constraintTop_toTopOf="@+id/last_song_background">

        <TextView
            android:id="@+id/last_song_duration"
            style="@style/TextAppearance.Info"
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:gravity="top"
            tools:text="Durée : 3 min 56" />

        <TextView
            android:id="@+id/last_song_year"
            style="@style/TextAppearance.Info"
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:gravity="center"
            tools:text="Année : 2016" />

        <TextView
            android:id="@+id/last_song_type"
            style="@style/TextAppearance.Info"
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:gravity="bottom"
            tools:text="Genre : Death Metal" />
    </LinearLayout>

 

Cependant, comme vous pouvez le constater, celui-ci contient encore des LinearLayout imbriqués.

 

En effet, même si on peut réduire l’arborescence grâce aux chaînes, il n’est pas possible de créer de chaînes multiples (verticales puis horizontales) comme le permettent deux LinearLayout imbriqués. Essayez donc de le faire, vous vous rendrez vite compte du problème : imaginons former une chaîne verticale avec les éléments du bloc d’infos principal. Si vous créez votre chaîne horizontale depuis l’ImageView de la pochette d’album, vers quel autre widget le relier ? Le titre, le nom du groupe, l’album ? Il ne peut pas être relié aux trois éléments à la fois, puisque l’on ne peut créer qu’une seule contrainte depuis l’ImageView. Pas d’autre choix, pour le moment, que de passer par un LinearLayout pour contenir ces widgets.

De même, si l’on veut que le séparateur soit correctement aligné, le bloc d’informations secondaires doit lui aussi être un LinearLayout, afin d’avoir la mesure de l’élément le plus large.

 

Car oui, malgré toutes ses qualités, le ConstraintLayout n’est pas parfait.

 

Les limites du ConstraintLayout

 

En plus des limites exposées précédemment, il existe d’autres points à problèmes dont il est important de parler :

 

  • Il n’existe pas de gestion des widgets par groupe pour gérer la visibilité. Il est très courant de vouloir masquer des éléments de l’écran dynamiquement. Le problème avec le ConstraintLayout est que ses widgets sont à plat. Du coup, si on veut garder ce principe, il n’est pas possible de cacher un ensemble d’éléments en cachant un ViewGroup parent comme on pourrait le faire dans un LinearLayout. Cela oblige à changer leur visibilité un par un. Ce qui est fait d’ailleurs dans le code de l’application. Les chaînes auraient pu permettre ça mais rien ne permet de cacher tous les éléments d’un coup. Avoir un attribut “groupId” qui permettrait d’appliquer une modification à un ensemble de widgets pourrait simplifier ce genre de manipulation. Ou bien avoir un ViewGroup virtuel.

 

  • L’ajout dynamique de widgets est compliqué, tout comme il peut l’être avec un RelativeLayout. Un des avantages du LinearLayout est de pouvoir ajouter / retirer des vues dynamiquement et laisser le ViewGroup s’adapter en fonction. Ici, il faut définir de nouvelles contraintes dans le code ou les enlever en fonction des besoins, ce qui en complexifie l’usage.

 

  • Enfin, il subsiste quelques bugs gênants aujourd’hui : par exemple, les chaînes ne fonctionnent pas encore bien avec les layouts “inclus” (comprendre : ajoutés au layout grâce à la balise “include” ), il existe pas mal de problèmes avec les marges (qui ne sont pas prises en compte avec certaines contraintes), et les contraintes répondent mal à des changements de position dûs à des attributs comme la rotation.

 

Il faut cependant relativiser : la plupart de ces problèmes existaient déjà avec le RelativeLayout. Et comme nous avons pu le voir, le ConstraintLayout permet de corriger de nombreux problèmes que le RelativeLayout n’est pas en mesure de résoudre. D’autant que le ConstraintLayout est toujours en développement, et n’existe que depuis quelques mois.

 

Par ailleurs, l’histoire de vous mettre l’eau à la bouche : pour avoir pu participer à une conférence réalisée par un développeur de Google sur le ConstraintLayout, j’ai pu constater que les problèmes cités étaient déjà pris en compte, et que des solutions sont en cours d’études. Solutions toutes plus alléchantes les unes que les autres !

L’une d’entre elles consisterait en un nouvel objet virtuel, baptisé Barrière, qui agirait comme une Guideline inversée, contrainte à plusieurs widgets, et se placerait par rapport au plus grand d’entre eux. Cela permettrait de résoudre le problème noté plus haut qui nous obligeait à avoir un LinearLayout pour connaître le widget le plus large (l’information est à prendre avec d’énormes pincettes, ce composant n’étant qu’un projet à l’heure actuelle).

 

Conclusion

 

Capable de proposer les avantages du RelativeLayout tout en palliant un grand nombre de ses défauts, le ConstraintLayout se révèle être une solution à beaucoup de situations qui nous obligeaient jusqu’à maintenant à utiliser des combinaisons de ViewGroup.

Les nouvelles options ajoutées permettent de répondre à des designs exigeants, en se basant plus sur les proportions de l’écran que sur des marges arbitraires et souvent problématiques sur certaines tailles d’écrans.

 

Alors, est-ce que je vous recommande de l’utiliser ? Oui.

Mais j’y mettrais tout de même un bémol.

 

Car, même s’il permet de gérer la plupart des cas de figure, certaines possibilités restent manquantes et resteront plus simples et plus performantes à faire avec des LinearLayout. Le ConstraintLayout brille surtout dans la gestion des layouts complexes, tels que celui que nous avons pu voir.

De plus, le ConstraintLayout est encore en bêta et plusieurs problèmes d’intégration dans Android Studio subsistent, notamment en ce qui concerne la preview de vos layouts qui est encore imparfaite. Si vous êtes sur un projet sensible, il peut être plus prudent d’attendre sa version stable.

 

Ce qui est sûr, c’est que le ConstraintLayout remplace avantageusement le RelativeLayout et pourra, à terme, le remplacer dans tous les cas de figure. Ma prescription dès lors : usage à volonté !

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

Nombre de vue : 753

COMMENTAIRES 1 commentaire

  1. GeoD dit :

    Bonjour Yann.

    Un énorme merci pour ce double article / tutoriel / cours démonstratif qui, concrètement, est une véritable perle !
    Il m’a grandement dépanné et je t’en remercie.

    Même si https://codelabs.developers.google.com/codelabs/constraint-layout/index.html?index=..%2F..%2Findex#0 est très sympa à suivre, il ne va pas aussi loin.

    A bientôt 🙂

AJOUTER UN COMMENTAIRE