Customiser les Styles & Themes sur Android

le 09/12/2016 par Pierre Degand
Tags: Software Engineering

Maintenant que AppCompat est devenue une librairie incontournable pour les projets Android et le fonctionnement de celle ci reposant beaucoup sur les thèmes et styles customs, cela peut vite devenir frustrant de vouloir customiser son application car la différence entre un thème et un style reste souvent obscure.

Dans cet article, je vais essayer de démystifier un peu le fonctionnement de ces thèmes et styles pour vous aider à écrire du code plus simple et avoir une UI customisée pour vos besoins.

Qu'est ce qu'un style ?

En tant que développeur Android, vous avez probablement déjà écrit un tel layout :

<android.support.v7.widget.Toolbar
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="@drawable/bg_gradient"
  app:title="My app title"/>

Comment Android sait que cette Toolbar doit être dessiné avec un dégradé en background et le bon titre ?

En observant le code source d'Android, on peut remarquer que presque toutes les class des widgets ont 3 constructeurs (ou 4 depuis Lollipop). Comprenons l'utilité de ces derniers.

Par exemple, voici les 3 constructeurs de la Toolbar, un widget de la librairie appcompat-v7 :

public Toolbar(Context context) {
  this(context, null);
}

public Toolbar(Context context, @Nullable AttributeSet attrs) {
  this(context, attrs, R.attr.toolbarStyle);
}

public Toolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  ...
}

Le premier est simplement le constructeur utilisé quand une vue est créée directement depuis du code Java.

Le second, celui avec AttributeSet en unique paramètre, est le constructeur utilisé par le LayoutInflater quand un XML de layout est inflaté. Cet AttributeSet contient tous les attributs qui ont été spécifiés dans le layout XML. La Toolbar va ensuite lire le contenu de cet AttributeSet, chercher des attributs connus comme title ou background (entre autre, parmi ceux utilisé par Toolbar ou View) et appeler les méthodes correspondantes comme setTitle() ou setBackground() pour configurer le widget avec les bonnes valeurs qui étaient renseignées dans le XML.

Par example, dans le layout XML d'une activité, on peut trouver la Toolbar suivante :

<android.support.v7.widget.Toolbar
  style="@style/DefaultToolbar"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  app:title="My app title"/>

avec le fichier res/values/styles.xml contenant :

<style name="DefaultToolbar"
  <item name="android:background">@drawable/bg_gradient</item>
  <item name="titleTextColor">@android:color/red</item>
</style>

L'AttributSet donné au constructeur contiendra l'ensemble des attributs spécifiés dans le XML de la vue ainsi que ceux contenu dans le style. Si un attribut est défini à la fois dans le style et directement dans le XML de la vue, alors celui du XML sera utilisé.

Pour résumer, un style sur Android est donc un ensemble d'attributs qui sont donnés à une vue, directement dans le XML ou à travers le paramètre style.

Introduction aux thèmes

Pour comprendre l'intérêt du 3ème constructeur, il faut d'abord comprendre ce qu'est un thème.

Un Thème peut être vu comme un super style qui est appliqué à toute une Activity et à toutes les vues de cette Activity.

Par exemple, si le thème suivant est déclaré :

<style name="CustomTheme" parent="@style/Theme.Appcompat">
  <item name="android:text">Default text.</item>
</style>

et que ce thème est appliqué à une Activity dans le fichier AndroidManifest.xml, toutes les vues qui vont être inflaté dans cette Activity vont recevoir dans leur AttributeSet au moins l'attribut text avec la valeur spécifiée dans le thème.

Donc, si la TextView suivante est ajoutée au XML de l'Activity :

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"/>

La TextView va être affichée avec le texte "Default text." même si ce texte n'était spécifié nulle part dans le XML de l'Activity.

Évidemment, si un attribut est spécifié à la fois dans le thème puis surchargé dans le XML d'une vue, la valeur surchargée de la vue sera utilisée.

Les Attributs de Styles

Même si un thème peut être vu comme un style appliqué sur toutes les vues, il peut aussi contenir des références, ou attributs de styles, vers d'autres ressources. Ces attributs de styles peuvent être utilisés par d'autres styles ou vues pour avoir un comportement spécifié par le thème.

Par exemple, on peut trouver, dans les thèmes de AppCompat, l'attribut de style colorAccent. Voyons comment définir notre propre attribut de style.

Tout d'abord, l'attribut est défini dans un bloc <attr/> (ce bloc est souvent écrit dans un fichier res/values/attrs.xml).

<?xml version="1.0" encoding="UTF-8"?>
<resources>
  <attr name="favoriteColor" format="color"/>
</resources>

Ceci permet de dire à Android "Ici je définis un attribut de style qui s'appelle favoriteColor, il va référencer une couleur et n'importe quel thème peut spécifier la valeur de cet attribut de style".

Dans le thème de l'application, on peut donc donner une valeur à cet attribut de style :

<style name="AppTheme" parent="@style/Theme.Appcompat">
  <item name="favoriteColor">#FF00FF</item>
</style>

Et finalement, ce nouvel attribut de style peut être utilisé n'importe où, tant que notre AppTheme est appliqué. Par exemple, dans le XML d'une Activity qui utilise ce thème, on pourrait écrire :

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:textColor="?attr/favoriteColor">

La notation ?attr/ indique que la valeur de l'attribut de style favoritecolor est définie dans le thème. Si cette attribut n'est pas trouvé dans le thème de l'activité, l'application va crasher.

La puissance des attributs de styles est qu'ils peuvent référencer n'importe quelle type de valeur ! Une couleur, un drawable, une dimen, un integer ou même ... un autre style !

Les styles par défauts

Revenons au début de cette article où j'expliquais les différents constructeurs utilisés par les widgets car je ne me suis jamais arrêté sur le troisième.

Comme vous l'avez probablement remarqué, la seule chose que fait le deuxième constructeur est d'appeler le troisième avec un paramètre en dur : R.attr.toolbarStyle. (dans notre exemple de la Toolbar).

Ce troisième paramètre est en fait une référence vers un attribut de style qui pointe vers un style qui contient les valeurs par défaut de la vue.

L'attribut toolbarStyle est donc le style par défaut de la Toolbar !

Si on regarde le code source de AppCompat, on peut trouver dans les thèmes Theme.AppCompat (ou les thèmes enfants) la référence suivante :

<style name="Base.V7.Theme.AppCompat.Light" parent="Platform.AppCompat.Light">
  <item name="toolbarStyle">@style/Widget.AppCompat.Toolbar</item>
</style>

Et la valeur de ce style par défaut peut être vue dans les sources d'AppCompat.

À la construction d'une View, le style par défaut est utilisé avec l'AttributeSet pour résoudre les attributs utilisés pour configurer la vue. Tout les attributs présents dans le style par défaut qui ne sont pas présent dans l'AttributSet vont être récupérés dans le style par défaut.

Si un attribut de l'AttributeSet surcharge une valeur du style par défaut, la valeur de l'AttributeSet sera utilisée.

Voici une liste des styles par défaut de AppCompat pour quelques widgets :

  • TextView: ?android:attr/textViewStyle style par défaut : Widget.Material.Light.TextView
  • EditText: ?attr/editTextStyle style par défaut :Widget.AppCompat.EditText
  • Button: ?attr/buttonStyle style par défaut :Widget.AppCompat.Button
  • Toolbar: ?attr/toolbarStyle style par défaut :Widget.AppCompat.Toolbar
  • Checkbox: ?attr/checkboxStyle style par défaut :Widget.AppCompat.CompoundButton.CheckBox

Customiser AppCompat

Maintenant qu'on comprend un peu mieux comment fonctionnent les thèmes et les styles, on peut customiser les widgets de notre applications en utilisant des combinaisons de thèmes et de styles.

Par exemple, on veut une Toolbar avec un style customisé dans toutes notre application sans devoir copier/coller 10 lignes de XML dans toutes les vues de nos Activities.

On peut alors définir un style custom qui étend du style par défaut de la Toolbar et appliquer ce nouveau style custom dans le thème de l'application :

<style name="SuperCustomToolbar" parent="Widget.AppCompat.Toolbar">
  <item name="titleTextColor">#FF00FF</item>
  <item name="subtitleTextColor">#00FF00</item>
  <item name="background">#222222</item>
</style>

<style name="AppTheme" parent="Theme.AppCompat.Light">
  <item name="toolbarStyle">@style/SuperCustomToolbar</item>
</style>

Dans le AndroidManifest.xml, on applique le thème à notre Activity :

<application
 ...
 theme="@style/AppTheme">
  <activity android:name=".MainActivity"/>
</application>

Et dans le fichier de layout activity_main.xml on écrit simplement :

<android.support.v7.widget.Toolbar
  android:layout_width="match_parent"
  android:layout_height="wrap_content"/>

Et voila ! La Toolbar aura notre style custom sans devoir le spécifier dans la vue de l'activité !

Pour finir, je ne peux que vous conseiller de lire le code source d'Android et d'AppCompat pour trouver quels sont les attributs de styles par défaut utilisés par chacun des widgets que vous voulez customiser et à quelles valeurs ils font référence.

Le code source des librairies supports est normalement disponible directement dans Android Studio (cmd+clic sur un nom de class pour aller à la source) ou vous pouvez parcourir le code directement sur Github.

Pour trouver le style par défaut, chercher les contructeurs, vous trouverez ainsi l'attribut de style utilisé par le widget puis en parcourant les thèmes de AppCompat, vous trouverez la valeur par défaut de cet attribut.

Voici un petit exemple rapide pour illustrer comment retrouver le style par défaut de toolbarStyle :

//giphy.com/embed/3oz8xuz9yvNHfIZHtm

via GIPHY

Pour en apprendre plus sur les thèmes et les styles, vous pouvez lire la documentation officielle ou regarder ce talk de Google I/O 2016: Android themes & styles demystified.

Have fun customizing your AppCompat's themes !