I. Sommaire▲
Cet article décrit la création d'une vue personnalisée avec Android. Il est basé sur Eclipse 4.4, Java 1.6 et Android 4.4.
II. Vue personnalisée▲
II-A. Vue par défaut▲
Le framework Android fournit plusieurs vues par défaut, mais le développeur peut aussi créer ses propres vues et les utiliser dans ses applications. La classe de base pour les vues est View.
II-B. Comment Android dessine la hiérarchie des vues▲
Quand une activité reçoit le focus, elle doit fournir le nœud racine de sa hiérarchie de layout au système Android. La procédure de dessin est ensuite démarrée.
Le dessin commence avec le nœud racine du layout. La hiérarchie de layout est traversée dans l'ordre des déclarations, c'est-à-dire les parents sont dessinés avant leurs enfants et les enfants le sont dans l'ordre de leur déclaration.
Le dessin de layout s'effectue en deux passes.
- La passe d'évaluation - implémentée dans la méthode measure(int, int), c'est une traversée de haut en bas dans la hiérarchie de la vue. Chaque vue stocke son évaluation.
- La passe layout - implémentée dans la méthode layout(int, int), c'est aussi une traversée de haut en bas dans la hiérarchie de la vue. Pendant cette phase, chaque layout manager est responsable du positionnement de tous ses enfants. Il utilise les tailles précalculées dans la passe d'évaluation.
Les passes évaluation et layout se déroulent toujours ensemble.
Les layout managers peuvent lancer la passe d'évaluation plusieurs fois. Par exemple : LinearLayout supporte l'attribut weight, lequel distribue l'espace vide restant parmi les vues et les mesures RelativeLayout des vues enfants plusieurs fois, pour régler les contraintes données par le fichier de layout.
Une vue ou une activité peut déclencher la passe d'évaluation et la passe layout avec un appel à la méthode requestLayout().
Après l'évaluation et le calcul du layout, les vues se dessinent elles-mêmes. Cette opération peut être déclenchée avec la méthode invalidate() de la classe View.
II-C. Les raisons de création des vues▲
Une vue est typiquement créée pour fournir une expérience utilisateur impossible avec les vues par défaut. Utiliser des vues personnalisées permet au développeur d'effectuer des optimisations de performance. En effet, dans le cas d'un layout personnalisé, le développeur peut optimiser le layout manager pour son cas d'utilisation.
II-D. La responsabilité des vues▲
Les vues sont responsables de l'évaluation, de la pagination, et du dessin d'elles-mêmes et de leurs éléments enfants (en cas d'utilisation de ViewGroup). Les vues sont aussi responsables de la sauvegarde de leur état UI et de la gestion des touch events.
II-E. Les façons de créer des vues personnalisées▲
Les vues personnalisées sont classées soit comme vues composées, soit comme vues personnalisées. Il est possible de créer une vue personnalisée par :
- Vues composées ;
- Vues personnalisées :
II-F. Utiliser de nouvelles vues dans des fichiers layout▲
Les vues composées et personnalisées peuvent être utilisées dans des fichiers layout. Pour cela, vous avez besoin du nom complet qualifié, c'est-à-dire à l'aide du nom du package et de la classe.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns
:
android
=
"http://schemas.android.com/apk/res/android"
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"match_parent"
android
:
orientation
=
"vertical"
>
<Button
android
:
id
=
"@+id/button1"
android
:
layout_width
=
"wrap_content"
android
:
layout_height
=
"wrap_content"
android
:
text
=
"Button"
/>
<de.vogella.android.ownview.MyDrawView
android
:
id
=
"@+id/myDrawView1"
android
:
layout_width
=
"wrap_content"
android
:
layout_height
=
"wrap_content"
/>
</LinearLayout>
Astuce : optionnellement, vous pouvez aussi déclarer votre espace de noms dans le fichier de layout, comme pour l'espace de noms Android.
II-G. Capture d'écran (images) de vues▲
Chaque classe View supporte la création d'une image de son affichage courant. Le code suivant montre un exemple de cela.
# Construction du cache de dessin
view.buildDrawingCache
(
);
# Création Bitmap
Bitmap cache =
view.getDrawingCache
(
);
# Sauvegarde Bitmap
saveBitmap
(
cache);
view.destroyDrawingCache
(
);
III. Vues composées (compound views)▲
Les vues composées (également appelées composants composés) sont des ViewGroups préconfigurés basés sur des vues existantes avec certaines interactions prédéfinies.
Celles-ci permettent également d'ajouter des API personnalisées pour mettre à jour et interroger l'état de la vue composée.
Pour un tel contrôle, vous définissez un fichier de layout et l'assignez à votre vue composée.
Dans l'implémentation de votre vue composée, vous devez prédéfinir l'interaction avec la vue. Vous devez définir un fichier layout et étendre la classe ViewGroup correspondante. Dans cette classe, vous étendez le fichier layout et implémentez la logique de connexion de la vue.
Pour des questions de performances, vous pouvez réécrire votre vue composée dans une vue personnalisée avec extends View. Cela peut généralement aplatir votre hiérarchie de vue. Le dessin nécessitera dans ce cas une traversée moindre et sera plus rapide s'il est correctement implémenté.
IV. Créer une vue personnalisée (custom view)▲
IV-A. Créer une vue▲
En étendant la classe View ou une de ses sous-classes, vous pouvez créer votre propre vue personnalisée.
Pour dessiner la vue, utilisez la méthode onDraw(). Avec celle-ci, vous recevez un Canvas Object, lequel vous permet d'effectuer dessus, des opérations de tracé. Ex. tracer des lignes, un cercle, du texte ou des bitmaps. Si la vue doit être redessinée, appelez la méthode invalidate(), laquelle déclenche un appel à la méthode onDraw() de la vue.
Si vous définissez vos propres vues, assurez-vous de consulter la classe ViewConfiguration, car elle contient plusieurs constantes de définition de vues.
Pour dessiner vos vues, vous utilisez généralement l'API Canvas 2D.
IV-B. Mesure▲
Le layout manager appelle la méthode onMeasure() de la vue. La vue reçoit le paramètre de layout depuis le layout manager. Un layout manager a la charge de déterminer la taille de tous ses enfants.
La vue doit appeler la méthode setMeasuredDimenstion(int, int) avec le retour de la fonction.
IV-C. Définir des custom layout managers▲
Vous pouvez implémenter votre layout manager personnalisé en étendant la classe ViewGroup. Cela vous permet d'implémenter de façon plus efficace des layout managers ou d'implémenter des effets actuellement inexistants dans la plate-forme Android.
Un layout manager personnalisé peut surcharger les méthodes onMeasure() et onLayout(), et spécialiser le calcul de ses enfants. Par exemple, il peut omettre le support de consommation du temps de layout_weight de la classe LinearLayout.
Pour calculer la taille d'un enfant, vous pouvez utiliser la méthode measureChildWithMargins() de la classe ViewGroup.
Stocker des paramètres de layout dans une classe interne à votre implémentation de ViewGroup est une bonne pratique. Par exemple, ViewGroup.LayoutParams implémente les commandes de paramètres de layout et LinearLayout.LayoutParams implémente les paramètres additionnels spécifiques à LinearLayout, comme par exemple, le paramètre layout_weight.
V. Cycle de vie▲
V-A. Cycle de vie des événements liés à la fenêtre▲
Une vue est affichée si elle est attachée à une hiérarchie de layout attachée elle-même à une fenêtre. Une vue a plusieurs ancres de cycle de vie.
La méthode onAttachedToWindow() est appelée une fois la fenêtre accessible.
La méthode onDetachedFromWindow() est utilisée quand la vue est retirée de son parent (et si le parent est détaché d'une fenêtre). Ceci se produit si l'activité est recyclée, par exemple (ex. via l'appel à la méthode finished()) ou si la vue est recyclée dans une ListView. La méthode onDetachedFromWindow() peut être utilisée pour stopper les animations et nettoyer les ressources utilisées par la vue.
V-B. Parcours des événements du cycle de vie▲
Le parcours des événements du cycle de vie consiste à animer, mesurer, placer et dessiner.
Toutes les vues doivent savoir comment se mesurer et se placer elles-mêmes. La méthode requestLayout() peut indiquer à la vue de se mesurer et de se repositionner elle-même.Comme cette opération peut influer sur le layout d'autres vues, elle appelle aussi la méthode requestLayout() de son parent.
Cet appel récursif est la raison pour laquelle vous ne devriez pas gérer vous-même, depuis la vue, les profondeurs de vues comme les mesures et opérations des layout qui peuvent être très coûteuses, si énormément de hiérarchies de vues sont recalculées.
La méthode onMeasure() détermine la taille pour la vue et ses enfants, elle doit régler la dimension via la méthode setMeasuredDimension(), qui est appelée avant son retour.
Onlayout() positionne les vues sur la base du résultat de l'appel à la méthode onMeasure(), Cet appel se produit une fois, c'est pourquoi onMeasure() ne peut se produire qu'une fois.
V-C. Cycle de vie d'une activité▲
Les vues n'ont pas accès aux événements du cycle de vie des activités. Si les vues doivent être informées des événements, vous devez créer une interface dans la vue à partir de laquelle vous voulez appeler les méthodes du cycle de vie de l'activité.
VI. Définir des attributs supplémentaires pour votre vue personnalisée▲
Vous pouvez définir des attributs supplémentaires pour vos vues personnalisées ou composées. Pour définir des attributs supplémentaires, créez un fichier attrs.xml dans votre dossier res/values. La suite montre un exemple d'attributs définis pour une nouvelle vue nommée ColorOptionsView.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable
name
=
"ColorOptionsView"
>
<attr
name
=
"titleText"
format
=
"string"
localization
=
"suggested"
/>
<attr
name
=
"valueColor"
format
=
"color"
/>
</declare-styleable>
</resources>
Pour utiliser ces attributs dans votre fichier layout, vous devez déclarer ceux-ci dans l'en-tête XML. Dans le listing suivant, ceci est fait via la partie du code xmlns:custom. Ces attributs sont aussi assignés à la vue.
<LinearLayout
xmlns
:
android
=
"http://schemas.android.com/apk/res/android"
xmlns
:
tools
=
"http://schemas.android.com/tools"
<!-- défini un nouvel espace de noms pour vos attributs -->
xmlns
:
custom
=
"http://schemas.android.com/apk/res/com.vogella.android.view.compoundview"
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"match_parent"
android
:
orientation
=
"vertical"
>
<!-- Supposons que ceci soit votre nouveau composant. Il utilise vos nouveaux attributs -->
<com.vogella.android.view.compoundview.ColorOptionsView
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"?android:attr/listPreferredItemHeight"
custom
:
titleText
=
"Background color"
custom
:
valueColor
=
"@android:color/holo_green_light"
/>
</LinearLayout>
L'exemple suivant montre comment vos composants peuvent accéder à ces attributs.
package
com.vogella.android.view.compoundview;
import
android.content.Context;
import
android.content.res.TypedArray;
import
android.util.AttributeSet;
import
android.view.Gravity;
import
android.view.LayoutInflater;
import
android.view.View;
import
android.widget.ImageView;
import
android.widget.LinearLayout;
import
android.widget.TextView;
public
class
ColorOptionsView extends
View {
private
View mValue;
private
ImageView mImage;
public
ColorOptionsView
(
Context context, AttributeSet attrs) {
super
(
context, attrs);
TypedArray a =
context.obtainStyledAttributes
(
attrs,
R.styleable.Options, 0
, 0
);
String titleText =
a.getString
(
R.styleable.Options_titleText);
int
valueColor =
a.getColor
(
R.styleable.Options_valueColor,
android.R.color.holo_blue_light);
a.recycle
(
);
// plus de choses ...
}
}
VII. Exercice : vue composée▲
VII-A. Créer un projet▲
Créez un nouveau projet Android avec les données suivantes :
Tableau 1. Nouveau projet Android
Propriété |
Valeur |
Application Name |
Exemple vue composée |
Project Name |
com.vogella.android.customview.compoundview |
Package name |
com.vogella.android.customview.compoundview |
API (Minimum, Target, Compile with) |
Latest |
Template |
Empty Activity |
Activity |
MainActivity |
Layout |
activity_main |
VII-B. Définir et utiliser des attributs supplémentaires▲
Créez le fichier d'attributs attrs.xml suivant dans votre dossier res/values :
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable
name
=
"Options"
>
<attr
name
=
"titleText"
format
=
"string"
localization
=
"suggested"
/>
<attr
name
=
"valueColor"
format
=
"color"
/>
</declare-styleable>
</resources>
Changez le fichier layout utilisé par l'activité, par celui-ci :
<LinearLayout
xmlns
:
android
=
"http://schemas.android.com/apk/res/android"
xmlns
:
tools
=
"http://schemas.android.com/tools"
xmlns
:
custom
=
"http://schemas.android.com/apk/res/com.vogella.android.view.compoundview"
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"match_parent"
android
:
orientation
=
"vertical"
android
:
showDividers
=
"middle"
android
:
divider
=
"?android:attr/listDivider"
tools
:
context
=
".MainActivity"
>
<com.vogella.android.view.compoundview.ColorOptionsView
android
:
id
=
"@+id/view1"
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"?android:attr/listPreferredItemHeight"
android
:
background
=
"?android:selectableItemBackground"
android
:
onClick
=
"onClicked"
custom
:
titleText
=
"Background color"
custom
:
valueColor
=
"@android:color/holo_green_light"
/>
<com.vogella.android.view.compoundview.ColorOptionsView
android
:
id
=
"@+id/view2"
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"?android:attr/listPreferredItemHeight"
android
:
background
=
"?android:selectableItemBackground"
android
:
onClick
=
"onClicked"
custom
:
titleText
=
"Foreground color"
custom
:
valueColor
=
"@android:color/holo_orange_dark"
/>
</LinearLayout>
VII-C. Créer une vue composée▲
Créez le fichier de layout suivant, nommé view_color_options.xml pour votre vue composée :
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns
:
android
=
"http://schemas.android.com/apk/res/android"
>
<TextView
android
:
layout_width
=
"0dp"
android
:
layout_height
=
"wrap_content"
android
:
layout_weight
=
"1"
android
:
layout_centerVertical
=
"true"
android
:
layout_marginLeft
=
"16dp"
android
:
textSize
=
"18sp"
/>
<View
android
:
layout_width
=
"26dp"
android
:
layout_height
=
"26dp"
android
:
layout_centerVertical
=
"true"
android
:
layout_marginLeft
=
"16dp"
android
:
layout_marginRight
=
"16dp"
/>
<ImageView
android
:
layout_width
=
"wrap_content"
android
:
layout_height
=
"wrap_content"
android
:
layout_marginRight
=
"16dp"
android
:
layout_centerVertical
=
"true"
android
:
visibility
=
"gone"
/>
</merge>
Créez la vue composée suivante :
package
com.vogella.android.customview.compoundview;
import
com.vogella.android.view.compoundview.R;
import
android.content.Context;
import
android.content.res.TypedArray;
import
android.util.AttributeSet;
import
android.view.Gravity;
import
android.view.LayoutInflater;
import
android.view.View;
import
android.widget.ImageView;
import
android.widget.LinearLayout;
import
android.widget.TextView;
public
class
ColorOptionsView extends
LinearLayout {
private
View mValue;
private
ImageView mImage;
public
ColorOptionsView
(
Context context, AttributeSet attrs) {
super
(
context, attrs);
TypedArray a =
context.obtainStyledAttributes
(
attrs,
R.styleable.ColorOptionsView, 0
, 0
);
String titleText =
a.getString
(
R.styleable.ColorOptionsView_titleText);
int
valueColor =
a.getColor
(
R.styleable.ColorOptionsView_valueColor,
android.R.color.holo_blue_light);
a.recycle
(
);
setOrientation
(
LinearLayout.HORIZONTAL);
setGravity
(
Gravity.CENTER_VERTICAL);
LayoutInflater inflater =
(
LayoutInflater) context
.getSystemService
(
Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate
(
R.layout.view_color_options, this
, true
);
TextView title =
(
TextView) getChildAt
(
0
);
title.setText
(
titleText);
mValue =
getChildAt
(
1
);
mValue.setBackgroundColor
(
valueColor);
mImage =
(
ImageView) getChildAt
(
2
);
}
public
ColorOptionsView
(
Context context) {
this
(
context, null
);
}
public
void
setValueColor
(
int
color) {
mValue.setBackgroundColor
(
color);
}
public
void
setImageVisible
(
boolean
visible) {
mImage.setVisibility
(
visible ? View.VISIBLE : View.GONE);
}
}
VII-D. Create Activity▲
Changez votre activité par le code suivant et lancez votre application.
package
com.vogella.android.customview.compoundview;
import
com.vogella.android.view.compoundview.R;
import
android.app.Activity;
import
android.os.Bundle;
import
android.view.Menu;
import
android.view.View;
import
android.widget.Toast;
public
class
MainActivity extends
Activity {
@Override
protected
void
onCreate
(
Bundle savedInstanceState) {
super
.onCreate
(
savedInstanceState);
setContentView
(
R.layout.activity_main);
}
@Override
public
boolean
onCreateOptionsMenu
(
Menu menu) {
// Étend le menu ; ceci ajoute des items à la barre d'actions si elle est présente.
getMenuInflater
(
).inflate
(
R.menu.activity_main, menu);
return
true
;
}
public
void
onClicked
(
View view) {
String text =
view.getId
(
) ==
R.id.view1 ? "Arrière-plan"
: "Avant-plan"
;
Toast.makeText
(
this
, text, Toast.LENGTH_SHORT).show
(
);
}
}
L'application lancée devrait ressembler à la capture d'écran suivante :
VIII. l'API Canvas▲
VIII-A. Vue d'ensemble▲
L'API Canvas permet de créer des effets graphiques complexes.
Vous peignez sur une surface bitmap. La classe Canvas fournit les méthodes de tracé pour dessiner sur un bitmap et la classe Paint spécifie comment le dessin est effectué sur le bitmap.
VIII-B. La classe Canvas▲
L'objet Canvas contient le bitmap sur lequel vous dessinez. Il fournit également les méthodes pour les opérations de dessin, ex. drawARGB() pour dessiner avec une couleur précise, drawBitmap() pour dessiner un bitmap, drawText() pour dessiner un texte, drawRoundRect() pour tracer un rectangle avec des coins arrondis et bien plus.
VIII-C. La classe Paint▲
Pour dessiner sur un objet Canvas, vous utilisez un objet de type Paint.
La classe Paint permet de spécifier la couleur, la police de caractères et certains effets pour les opérations de dessin.
La méthode setStyle() permet quant à elle de spécifier l'effet barré (Paint.Style.STROKE), le remplissage (Paint.Style.FILL) ou les deux (Paint.Style.STROKE_AND_FILL).
Vous pouvez affecter le canal Alpha du dessin via la méthode setAlpha().
Via les Shaders, vous pouvez définir si le dessin est rempli avec plus d'une couleur.
VIII-D. Shader▲
Un shader permet de définir le contenu qui peut être dessiné sur un objet Paint. Par exemple, vous pouvez utiliser un BitmapShader pour définir qu'un bitmap doit être utilisé pour dessiner. Ceci vous permet, par exemple, de dessiner une image avec des coins arrondis. Définissez simplement un BitmapShader pour votre objet Paint et utilisez la méthode drawRoundRect() pour dessiner un rectangle avec des coins arrondis.
Les autres Shaders fournis par la plate-forme Android sont LinearGradient, RadialGradient et SweepGradient pour dessiner des couleurs dégradées.
Pour utiliser un Shader, assignez-le à votre objet Paint via la méthode setShader().
Si la zone remplie est plus grande que le Shader, vous pouvez définir via le Shader tile mode, comment le reste doit être rempli. La constante Shader.TileMode.CLAMP définit que les coins des bords devraient être utilisés pour remplir l'espace supplémentaire, la constante Shader.TileMode.REPEAT définit que l'image sera répétée.
IX. Données de vue persistante▲
La plupart des vues standards peuvent sauvegarder leur état, et donc peuvent être gardées persistantes par le système. Le système Android appelle la méthode onSaveInstanceState() et onRestoreInstanceState(Parcable) pour sauvegarder et restaurer l'état de la vue.
La règle appliquée par convention consiste à étendre View.BasedSavedState, comme une classe interne statique dans la vue, pour la persistance des données.
Android recherche l'ID de la vue à partir du layout et passe un bundle à la vue permettant la restauration de son état.
Vous devez sauvegarder et restaurer l'interface utilisateur telle que l'utilisateur l'a laissé. Ex. la position des scrollings et la sélection active.
X. Exemple pour une vue personnalisée▲
Voici un exemple de vue personnalisée à l'URL suivante :
XI. Liens et documentation▲
XII. Remerciements Developpez▲
Vous pouvez retrouver l'article original à l'adresse Creating custom and compound Views in Android - Tutorial. Nous remercions Lars Vogel qui nous a aimablement autorisés à traduire et héberger ses articles.
Nous remercions aussi chrtophe pour sa traduction, ainsi que jacques_jean pour sa relecture orthographique.