I. Les Styles

Cet article vient en complément de l'introduction des styles et des thèmes par Cyril Mottier (que vous pouvez lire ici), toutefois, nous allons repasser brièvement sur les concepts de base.

I-A. Définition

Les « styles » permettent de transformer des éléments structurés (HTML, XML) simples et pas forcément très lisibles en une version formatée et affichable. On pense bien entendu aux « Cascading Style Sheet » (CSS) du langage HTML (SGML) qui remontent aux années 1980.

Le style permet donc de définir un certain nombre d'attributs (cosmétiques ou non) qui seront les valeurs par défaut des éléments auxquels le style est appliqué.

L'intérêt est double : d'une part permettre d'automatiquement fournir des attributs cosmétiques à des éléments, mais surtout de maintenir, à un seul endroit, ce qu'on appelle le « Look'n Feel », c'est-à-dire la marque visuelle du site (ou de l'application).

I-B. Dans Android

I-B-1. Petit rappel sur les ressources

Les ressources sont stockées d'une manière un peu spéciale dans Android. Toutes les ressources partagent le même paradigme d'accès :

en Java, les ressources sont accessibles par l'utilisation de R.xxxx.nnnn, où xxxx est le type de ressource ('layout', 'id', 'string', etc. et bien entendu 'style'), et nnnn son « nom ». Les ressources prédéfinies d'Android sont aussi accessibles en Java par android .R.xxxx.nnnn. Dans tous les cas, ce sont des valeurs entières (int) générées à la compilation ;

dans les fichiers XML les ressources sont référencées par l'utilisation de '@xxxx/nnnn' (ou de '@android :xxxx/nnnn' pour les ressources natives à Android) ;

ainsi, une ressource de type « string » sera référencée en Java par : R.string.LeNomDeMaString et en XML : @string/LeNomDeMaString.

I-B-2. Utilisation des Styles

Au contraire de HTML (où chaque 'classe' a son propre style et chaque élément peut avoir plusieurs classes, et donc plusieurs styles), sous Android, chaque élément (View) ne peut avoir qu'un seul et unique style.

Le choix du style est fait au moment de la construction de l'élément, ou bien de manière explicite (un attribut 'android:style' a été spécifiquement passé à la construction de l'élément) ou bien de manière implicite (le style par défaut de l'élément est utilisé).

 
Sélectionnez
<LinearLayout>
	<TextView id='id1' android:style='@style/MyStyle'/>
	<TextView id='id2'/>
</LinearLayout> 

Dans l'exemple ci-dessus, on a deux éléments "TextView", l'un (id1) aura un style explicitement défini, l'autre (id2) aura le style « par défaut » des TextViews. Nous verrons plus loin comment ce style par défaut est défini.

Il est bien sûr possible d'utiliser directement les styles Android. « @android:style/ListView » par exemple sera le style par défaut appliqué aux éléments ListView.

I-B-3. Définir son propre style

Les styles sont des ressources de type « style ». Et donc, référencés en XML par « @style ». Il suffit donc de créer un fichier XML (dans le répertoire 'values' des ressources) contenant un ou plusieurs styles. Par exemple :

 
Sélectionnez
<resource>
	<color name="light_grey">#e6eced</color>    
	<style name="MyStyle">
		<item name="android:background">@color/light_grey</item>
	</style>
</resource>

Ici, le style "MyStyle" a été défini et va appliquer automatiquement une couleur de background à tous les éléments qui l'utiliseront.

I-B-4. Définition en Cascade

Il est possible d'utiliser un « parent » pour le style. Tous les attributs du style parent seront inclus (et éventuellement surchargés par le style en cours).

Le parent peut être « implicite » en utilisant le point (.) pour spécifier le nom du style :

 
Sélectionnez
<resource>
	<style name="Layout">
		<item name="android:background">@color/light_grey</item>
		<item name="android:padding">5sp</item>
	</style>
	<style name="Layout.Bar">
		<item name="android:background">@color/dark_grey</item>
	</style>
<resource>

Ici, le style « Layout.Bar » aura comme parent le style « Layout » (et donc avec un attribut android:padding the « 5 sp »).

Il peut aussi être explicite en utilisant l'attribut « parent » :

 
Sélectionnez
<resource>
	<style name="MyButton" parent="@android:style/Widget.Button">
		<item name="android:background">@drawable/yellow_button</item>
		<item name="android:textAppearance">@style/TextAppearance.Label</item>
		<item name="android:textColor">@color/red</item>
		<item name="android:minWidth">80dp</item>
	</style>
</resource>

Ici le style "MyButton" aura comme parent le style prédéfini « Widget.Button » d'Android, mais une largeur minimale de 80 dp etc.

I-B-5. Que peut-on « styliser » ?

Tout d'abord qui peut recevoir un style et quel attribut peut-on lui appliquer ? Tous les éléments d'interface (View) peuvent recevoir un style. Quant aux attributs « stylisables », vous trouverez la liste exhaustive dans android.R.stylable.

Voici quelques attributs cosmétiques « classiques » :

  • minWidth, minHeight : très utiles pour les widgets ;
  • padding : correspond à la taille additionnelle autour de l'élément ;
  • background : le « drawable » à dessiner en fond (généralement une couleur, ou un dégradé) ;
  • textColor, textSize, textStyle et typeface (TextView) : définissent la manière donc le texte sera écrit ;
  • margin (ViewGroup) : espace à l'intérieur de l'élément, qui ne sera pas utilisé par les éléments fils.

Quant aux styles desquels vous pouvez hériter (que vous pouvez utiliser comme parents), ce sont des ressources de type style donc vous trouverez votre bonheur dans android.R.style.

I-B-6. Le cas particulier de textAppearance

Pour un certain nombre d'éléments, il est possible de spécifier un second style : textAppearance. Cet attribut fait référence à un style, mais seuls les attributs du style concernant le texte seront utilisés, c'est-à-dire : textColor (mais aussi textColorLink, textColorHighlight.), textSize, textStyle, typeface, et textAllCaps.

Cela évite par exemple d'avoir quatre styles différents de « boutons » en fonction de la taille du texte... Il y a un seul et unique style de bouton (@android:style/Widget.Button) et quatre styles différents de texte ('normal', 'Small', 'Medium' et 'Large') : les « TextAppearance.Small », etc.

Comme nous le verrons plus tard, cette séparation est d'autant plus pratique qu'il est possible de « modifier » les TextAppearance.Small.

I-C. Conclusion sur les styles

Normalement, à ce point, vous devriez pouvoir assigner un style à vos éléments d'interface, et ainsi vous éviter de modifier tous vos éléments quand vous décidez de changer la fonte de votre application.

II. Les thèmes

Vous devez tout de même vous dire que décidément, placer l'attribut 'style' à tous vos éléments va être très long, ennuyeux, et pas du tout pratique, et je vous l'accorde. Il existe néanmoins un outil pour vous éviter ce long travail : les thèmes.

II-A. Utilisation

Un thème n'est rien de plus qu'une liste de valeurs d'attributs (un peu comme les styles au demeurant). La liste de ces attributs (de thème) est accessible dans android.R.attr. Mais nous verrons plus loin qu'on peut y rajouter des attributs propres à l'application.

Pour des raisons de simplicité (mais pas forcément de lisibilité et/ou compréhension), Google a décidé de représenter les thèmes comme des ressources de type « style » qui doivent avoir comme parent le style « Theme » d'une manière ou d'une autre (directement ou non donc). Ainsi, si vous déclarez un style « Toto » dont le parent est Theme.Holo.Dark, vous aurez déclaré un thème !

Chaque activité reçoit un thème unique. Si une activité n'a pas de thème spécifique, le thème est celui par défaut de l'application.

Ce thème est utilisé pendant la construction de l'interface (inflater, setContentView) pour récupérer certains attributs par défaut (y compris le style). Ceci est aussi vrai pour les « layouts » système. Plutôt que de définir votre propre R.layout. list_item pour utiliser vos attributs cosmétiques, vous pourrez utiliser celui d'Android (android.R.layout.single_list_item), qui sera customisé en fonction du thème !

Les valeurs des attributs du thème sont accessibles depuis tout fichier XML par l'utilisation de  « ?attr/nom_de_lattribut » ou « ?android:attr/nom_de_lattribut » s'il est natif à Android.

Ainsi,

 
Sélectionnez
<LinearLayout>
	<TextView id='id1' android :textAppearance=' ?android:attr/textAppearanceSmall'/>
</LinearLayout>

va indiquer que l'attribut « textAppearanceSmall » du thème en cours doit être utilisé. Dans le cas d'un thème « Holo.Dark », le texte sera blanc, dans le cas de « Holo.Light », il sera donc noir.

Il y a bien sûr de fortes chances pour que la valeur de cet attribut soit simplement « @android:style/TextAppearance.Small  ». 

II-B. Créer un thème

Comme nous l'avons vu, les thèmes sont définis exactement comme les styles. Mais pour vous aider à vous y retrouver, je vous conseille vivement de séparer vos fichiers de thème et vos fichiers de styles.

II-B-1. Surcharger un thème existant

La liste des attributs est assez volumineuse, mais comme votre thème aura forcément un thème comme parent, tous les attributs non définis seront ceux du parent.

 
Sélectionnez
<resource>
	<style name="MyTheme" parent="@android:style/Theme.Holo.Light">
		<item name="android:colorBackground">@color/light_grey</item>
		<item name="android:colorBackgroundCacheHint">@color/light_grey</item>
	</style>
</resource>

Une fois que vous avez défini ce thème, vous devriez le voir apparaitre dans l'éditeur d'interface. Par défaut le background des éléments est désormais la couleur light_grey. Et les ListView utiliseront cette couleur pour le « fading ». Et pourtant sans rien toucher à aucun attribut des layouts !

À noter qu'à partir d'Android 3.0, le thème « Theme.Holo » est à privilégier.

II-B-2. Surcharger les styles par défaut

Dans la liste des attributs des thèmes, vous trouverez un certain nombre d'entrées intéressantes, en particulier sur les styles par défaut. Par exemple, l'attribut : buttonStyle. Cet attribut définit le style par défaut des boutons.

Pour le surcharger, commencez par créer votre propre style de bouton :

 
Sélectionnez
<resource>
	<style name="MyButton" parent="@android:style/Widget.Button">
		<item name="android:background">@drawable/MyButton</item>
	</style>
</resource>

Puis surchargez l'attribut buttonStyle dans le thème :

 
Sélectionnez
<resource>
	<style name="MyTheme" parent="@android:style/Theme.Holo">
		<item name="android:buttonStyle">@style/MyButton</item>
	</style>
</resource>

Et voilà. Tous vos boutons auront désormais le style « MyButton » par défaut. Et, plus intéressant encore, tout layout (système) incluant un bouton aura ce style !

À noter qu'une fois le thème sélectionné dans l'éditeur d'interface, vous verrez aussi directement l'aspect des boutons dans la sélection à gauche.

II-B-3. Créer ses propres attributs

Bien sûr, il serait utile de pouvoir rajouter des attributs spécifiques à votre application. Là encore c'est possible. En utilisant la ressource « attr » 

 
Sélectionnez
<resource>
	<attr name="generalMargin" format="reference|dimension"/>
	<style name="MyTheme" parent="@android:style/Theme.Holo">
		<item name="generalMargin">5sp</item>
	</style>
</resource>

Et bien évidemment, vous pourrez utiliser votre attribut avec « ?attr/generalMargin »

 
Sélectionnez
<resource>
	<style name="MyButton" parent="@android:style/Widget.Button">
		<item name="android:padding">?attr/generalMargin</item>
	</style>
</resource>

II-C. Spécifier le thème pour votre activité

Bien entendu, l'éditeur d'interface d'Eclipse vous permet de choisir le thème pour afficher tel ou tel layout, mais il vous faudra éditer le 'manifest' (là où est déclarée votre activité) pour lui attribuer un thème. C'est d'ailleurs comme cela qu'on fait une véritable boite de dialogue.

Bien entendu, si l'ensemble de votre application n'utilise qu'un seul thème, libre à vous de l'utiliser comme thème par défaut au niveau de l'application.

Il est possible de modifier le thème depuis Java, en utilisant la ressource ad hoc R.style.MyTheme : setTheme() dans l'activité. À noter que le thème ne modifie pas l'UI en cours, il faut donc reconstruire la vue, et appeler la fonction avant le setContentView. C'est la seule méthode pour proposer plusieurs thèmes à l'utilisateur dans la même application.

Prenons un exemple, j'ai une activité qui peut s'afficher sous forme de dialogue ('PICK'), ou 'normalement' ('VIEW').

J'ai donc deux thèmes : « MyTheme » qui hérite de « @android :style/Theme.Holo » et « MyTheme.Dialog » qui hérite de « @android :style/Theme.Holo.Dialog ». Il suffit dans mon activité que j'écrive :

 
Sélectionnez

@Override
public void onCreate(Bundle savedInstanceState)
{
	super.onCreate(savedInstanceState); // <= toujours en premier
	boolean isDialogCall = getIntent().getName().equals(Intent.ACTION_PICK);    
	setTheme(isDialogCall ? R.style.MyTheme.Dialog : R.style.MyTheme);
	setContentView(R.layout.my_layout); // <= maintenant on peut créer les views
}

À noter qu'il n'est pas nécessaire d'avoir surchargé le thème pour le faire, on aurait très bien pu utiliser android.R.style.Holo_Dark et android.R.style.Holo_Dark_Dialog par exemple.

III. Un Exemple

Dans cet exemple, nous allons partir d'une application très simple, incluant des boutons, des TextView, des CheckBox et même une ListView qui utilise les layouts par défaut. Et nous allons la customiser pour avoir 'notre' look'n feel (de très mauvais goût ici, mais c'est juste pour les besoins de la démonstration), sans toucher un pouce du fichier de layout initial.

D'abord le fichier main.xml du layout principal de l'activité. Le code Java ne fait que remplir la List-View avec des données.

 
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
	<TextView
	    android:layout_width="fill_parent"
	    android:layout_height="wrap_content"
	    android:text="@string/hello" />
	<LinearLayout
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content" >
		<Button android:id="@+id/button1"            
		    android:layout_width="wrap_content"            
		    android:layout_height="wrap_content"
		    android:text="Button" />
		<Button  android:id="@+id/button2"            
		    style="?android:attr/buttonStyleSmall"            
		    android:layout_width="wrap_content"            
		    android:layout_height="wrap_content"            
		    android:text="Button" />
		<ToggleButton android:id="@+id/toggleButton1"            
		    android:layout_width="wrap_content"
		    android:layout_height="wrap_content"
		    android:text="ToggleButton" />
	</LinearLayout>
	<LinearLayout
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content" >
		<CheckBox android:id="@+id/checkBox1"            
		    android:layout_width="wrap_content"            
		    android:layout_height="wrap_content"            
		    android:text="CheckBox" />
		<RadioButton android:id="@+id/radioButton1"
		    android:layout_width="wrap_content"            
		    android:layout_height="wrap_content"           
		    android:text="RadioButton" />
	</LinearLayout>
	<ListView android:id="@+id/listView1"        
	    android:layout_width="match_parent"
	    android:layout_height="wrap_content" >
		<!-- Preview: listitem=@android:layout/simple_list_item_checked -->
	</ListView>
</LinearLayout>

Comme vous pouvez le voir, aucune customisation d'interface, rien que des éléments par défaut sans modification cosmétique. Nous n'y toucherons plus.

Au passage, vous noterez que le « Small Button » indique clairement aller chercher son style dans le thème.

Voici le résultat :

image

III-A. Tout commence par un thème

Créer le thème dans un fichier « themes.xml » :

image

Le contenu est simple :

 
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<resources>
	<style name="ThemeTest" parent="@android:style/Theme.Holo">
	</style>
</resources>

Une fois le fichier enregistré, vous pourrez voir le thème apparaitre dans l'éditeur de layouts, mais avant, nous allons modifier le 'manifest' de notre application pour que l'activité utilise ce thème par défaut.

Nous allons donc rajouter l'attribut suivant dans la déclaration de l'activité :

android:theme="@style/ThemeTest"

Pour l'instant, rien n'a changé, puisque nous héritons simplement de 'Theme.Holo'.

III-B. Réglages de base

Ici, nous allons commencer par modifier le 'background', et la couleur du texte.

Pour le background, déjà, première complication, il nous faut passer par un nom de couleur. Qu'à cela ne tienne, on définit une ressource de type « color » (dans le même fichier), et on y fait référence avec @color/le_nom_de_la_couleur.

Il en est de même pour la couleur du texte.

Voici le fichier themes.xml après modification et le résultat :

 
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<resources>
	<color name="dark_green">#002000</color>
	<color name="yellow">#ffff00</color>
	<style name="ThemeTest" parent="@android:style/Theme.Holo">        
		<item name="android:background">@color/dark_green</item>        
		<item name="android:textColor">@color/yellow</item>
	</style>
</resources>
image

Premiers constats : la couleur du fond a bien été changée partout, mais la couleur de texte n'a été appliquée que sur les TextViews ! Voici pourquoi.

Si on regarde le style Widget.Holo.Button (le style par défaut des boutons), celui-ci fait directement référence à une couleur : @android:color/primary_text_holo_dark, qui est en fait un sélecteur de couleur (un peu comme un sélecteur de background) dont je vous mets le code ici :

 
Sélectionnez
<selector xmlns:android="http://schemas.android.com/apk/res/android">
	<item android:state_enabled="false" android:color="@android:color/bright_foreground_disabled_holo_dark"/>
	<item android:state_window_focused="false" android:color="@android:color/bright_foreground_holo_dark"/>
	<item android:state_pressed="true" android:color="@android:color/bright_foreground_holo_dark"/>
	<item android:state_selected="true" android:color="@android:color/bright_foreground_holo_dark"/>
	<item android:state_activated="true" android:color="@android:color/bright_foreground_holo_dark"/>
	<item android:color="@android:color/bright_foreground_holo_dark"/>
	<!-- not selected -->
</selector>

Ce qui veut dire, qu'en fonction de l'état du bouton, la couleur ne sera pas la même, et que le style du bouton va définir en dur la couleur du texte !

III-C. Surcharger les Styles

Il va donc falloir que nous surchargions, les styles de boutons. Pour les faire pointer sur notre couleur.

On commence par créer le style de bouton  (on va en profiter pour faire de même avec les CheckBoxes et radio-buttons). Voici donc le nouveau fichier de thème :

 
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<resources>
	<color name="dark_green">#002000</color>
	<color name="yellow">#ffff00</color>
	<style name="Widget.ThemeTest.Button" parent="@android:style/Widget.Holo.Button">
		<item name="android:textColor">?android:attr/textColor</item>
	</style>
	<style name="Widget.ThemeTest.Button.Small" parent="@android:style/Widget.Holo.Button.Small">
		<item name="android:textColor">?android:attr/textColor</item>
	</style>
	<style name="Widget.ThemeTest.Button.Toggle" parent="@android:style/Widget.Holo.Button.Toggle">
		<item name="android:textColor">?android:attr/textColor</item>
	</style>
	<style name="Widget.ThemeTest.CompoundButton.CheckBox" parent="@android:style/Widget.Holo.CompoundButton.CheckBox">
		<item name="android:textColor">?android:attr/textColor</item>
	</style>
	<style name="Widget.ThemeTest.CompoundButton.RadioButton" parent="@android:style/Widget.Holo.CompoundButton.RadioButton">
		<item name="android:textColor">?android:attr/textColor</item>
	</style>
	<style name="ThemeTest" parent="@android:style/Theme.Holo">
		<item name="android:background">@color/dark_green</item>
		<item name="android:textColor">@color/yellow</item>
		<item name="android:buttonStyle">@style/Widget.ThemeTest.Button</item>
		<item name="android:buttonStyleSmall">@style/Widget.ThemeTest.Button.Small</item>
		<item name="android:buttonStyleToggle">@style/Widget.ThemeTest.Button.Toggle</item>
		<item name="android:checkboxStyle">@style/Widget.ThemeTest.CompoundButton.CheckBox</item>
		<item name="android:radioButtonStyle">@style/Widget.ThemeTest.CompoundButton.RadioButton</item>
	</style>
</resources>

Et le résultat :

image

Finalement, nous avons bien la bonne couleur partout, et il suffit de la changer dans le thème (textColor) !

On pourrait dire : tant de code que ça dans les thèmes et styles, juste pour changer les couleurs ? Oui sans doute, pour une seule fenêtre dans une application de test, il vaut probablement mieux utiliser les attributs de style directement. Mais si vous avez une dizaine de layouts, et que vous utilisez ceux du système (ListView ?), le passage par le thème devient bien vite nécessaire (et vous évite de réinventer la roue dans bien des cas).

IV. Conclusion

Grâce aux thèmes, vous pouvez, depuis un unique endroit modifier l'aspect visuel de votre application. Mieux encore, les ressources pouvant être sélectionnées en fonction du device, de la langue, etc., il est possible d'avoir des thèmes variables en fonction de la taille de l'écran, de son orientation.

Le plus intéressant encore une fois, est de voir, d'un coup, l'ensemble des layouts système adopter votre « Look'n Feel ».

Pour vous simplifier la vie, je vous conseille de suivre ces quelques règles :

  • mis à part les attributs de structuration des layouts (gravité, width/height.), tout attribut d'un élément d'interface doit être défini par le style ;
  • dès qu'une valeur est utilisée par plusieurs styles, la définir comme attribut dans le thème ;
  • conserver au maximum les styles par défaut (et les surcharger depuis le thème), ceci permet d'utiliser les layouts système de manière transparente et avec le même Look'n Feel.

Si vous comptez fournir plusieurs « thèmes » à votre application, je vous conseille aussi :

  • de ne jamais pointer directement sur un style, mais sur un attribut du thème qui définira le style. Vous pourrez ainsi, en fonction du thème, modifier le style des éléments.

V. Liens

Remerciements

J'adresse ici tous mes remerciements à l'équipe de rédaction de « Developpez.com » pour le temps qu'ils ont bien voulu passer à la correction et à l'amélioration de cet article. En particulier Max, Feanorin, MrDuChnok, et ClaudeLELOUP