Customiser les Styles & Themes sur Android
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.TextViewEditText
:?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
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 !