Créer des composants de vue personnalisée

Essayer Compose
Jetpack Compose est le kit d'outils d'interface utilisateur recommandé pour Android. Découvrez comment utiliser les mises en page dans Compose.

Android propose un modèle composé de composants sophistiqués et performant pour créer votre interface utilisateur. Il est basé sur les classes de mise en page de base View et ViewGroup. La plate-forme comprend diverses sous-classes View et ViewGroup prédéfinies, respectivement appelées widgets et mises en page, que vous pouvez utiliser pour créer votre interface utilisateur.

Voici une liste partielle des widgets disponibles : Button, TextView, EditText, ListView, CheckBox, RadioButton, Gallery, Spinner, ainsi que AutoCompleteTextView, ImageSwitcher et TextSwitcher, plus spécifiques.

Voici quelques-unes des mises en page disponibles : LinearLayout, FrameLayout, RelativeLayout, etc. Pour obtenir plus d'exemples, consultez la section Mises en page courantes.

Si aucun des widgets ni aucune mise en page prédéfinis ne répondent à vos besoins, vous pouvez créer votre propre sous-classe View. Si vous ne devez apporter que de légers ajustements à un widget ou à une mise en page existants, vous pouvez sous-classer le widget ou la mise en page et remplacer ses méthodes.

En créant vos propres sous-classes View, vous pouvez contrôler précisément l'apparence et le fonctionnement d'un élément de l'écran. Voici quelques exemples de ce que vous pouvez faire avec les vues personnalisées:

  • Vous pouvez créer un type View entièrement personnalisé (par exemple, un bouton de "contrôle du volume", rendu à l'aide de graphismes 2D, qui ressemble à une commande électronique analogique).
  • Vous pouvez combiner un groupe de composants View en un seul composant, par exemple pour créer une zone combinée (combinaison d'une liste pop-up et un champ de saisie libre), une commande de sélection à double volet (volet gauche et droit avec une liste dans laquelle vous pouvez réaffecter quel élément se trouve dans quelle liste), etc.
  • Vous pouvez ignorer la façon dont un composant EditText s'affiche à l'écran. L'application exemple NotePad s'en sert pour créer une page de bloc-notes bordée de lignes.
  • Vous pouvez capturer d'autres événements (comme les pressions sur les touches) et les gérer de manière personnalisée, par exemple pour un jeu.

Les sections suivantes expliquent comment créer des vues personnalisées et les utiliser dans votre application. Pour en savoir plus, consultez la classe View.

Approche de base

Voici une présentation générale de ce que vous devez savoir pour créer vos propres composants View:

  1. Étendez une classe ou une sous-classe View existante avec votre propre classe.
  2. Ignorez certaines méthodes de la super-classe. Les méthodes de super-classe à remplacer commencent par on (par exemple, onDraw(), onMeasure() et onKeyDown()). Cette opération est semblable aux événements on dans Activity ou ListActivity que vous remplacez pour les hooks de cycle de vie et d'autres fonctionnalités.
  3. Utilisez votre nouvelle classe d'extension. Une fois l'opération terminée, vous pouvez utiliser votre nouvelle classe d'extension à la place de la vue sur laquelle elle était basée.

Composants entièrement personnalisés

Vous pouvez créer des composants graphiques entièrement personnalisés qui s'affichent comme vous le souhaitez. Vous préférez peut-être un VU-mètre graphique ressemblant à une vieille jauge analogique, ou un affichage de texte à reprendre en chœur dans lequel une balle rebondit à mesure que vous chantez avec une machine de karaoké. Vous pouvez vouloir quelque chose que les composants intégrés ne peuvent pas faire, quelle que soit la façon dont vous les combinez.

Heureusement, vous pouvez créer des composants qui s'affichent et se comportent comme vous le souhaitez, mais ne sont limités que par votre imagination, la taille de l'écran et la puissance de traitement disponible, en gardant à l'esprit que votre application peut être exécutée sur un appareil consommant beaucoup moins d'énergie que votre poste de travail de bureau.

Pour créer un composant entièrement personnalisé, tenez compte des points suivants:

  • La vue la plus générique que vous pouvez étendre est View. Vous commencez donc généralement par l'étendre pour créer votre nouveau super composant.
  • Vous pouvez fournir un constructeur, qui peut récupérer des attributs et des paramètres du fichier XML, et utiliser vos propres attributs et paramètres, tels que la couleur et la plage du VU-mètre, ou la largeur et l'amortissement de l'aiguille.
  • Vous souhaitez probablement créer vos propres écouteurs d'événements, accesseurs et modificateurs de propriété, ainsi qu'un comportement plus sophistiqué dans votre classe de composant.
  • Vous souhaiterez probablement ignorer onMeasure(). Vous devrez probablement aussi remplacer onDraw() si vous souhaitez que le composant affiche quelque chose. Bien que les deux aient un comportement par défaut, la onDraw() par défaut n'a aucun effet, et la onMeasure() par défaut définit toujours une taille de 100 x 100, ce qui n'est probablement pas souhaitable.
  • Si nécessaire, vous pouvez également remplacer d'autres méthodes on.

Étendre onDraw() et onMeasurement()

La méthode onDraw() fournit un Canvas sur lequel vous pouvez implémenter tout ce que vous souhaitez: graphismes 2D, autres composants standards ou personnalisés, texte stylisé ou tout autre élément auquel vous pouvez penser.

onMeasure() est un peu plus complexe. onMeasure() est un élément essentiel du contrat de rendu entre votre composant et son conteneur. Vous devez remplacer onMeasure() pour générer des rapports précis et efficaces sur les mesures des parties qu'il contient. Ceci est légèrement plus complexe par les exigences de limite du parent, qui sont transmises à la méthode onMeasure(), et par l'obligation d'appeler la méthode setMeasuredDimension() avec la largeur et la hauteur mesurées une fois qu'elles ont été calculées. Si vous n'appelez pas cette méthode à partir d'une méthode onMeasure() remplacée, une exception se produit au moment de la mesure.

Dans les grandes lignes, l'implémentation de onMeasure() se présente comme suit:

  • La méthode onMeasure() remplacée est appelée avec des spécifications de largeur et de hauteur, qui sont traitées comme des exigences liées aux restrictions sur les mesures de largeur et de hauteur que vous produisez. Les paramètres widthMeasureSpec et heightMeasureSpec sont tous deux des codes entiers représentant des dimensions. Pour en savoir plus sur le type de restrictions imposées par ces spécifications, consultez la documentation de référence sous View.onMeasure(int, int). Cette documentation de référence explique également l'ensemble de l'opération de mesure.
  • La méthode onMeasure() de votre composant calcule la largeur et la hauteur de mesure, qui sont nécessaires pour afficher le composant. Elle doit essayer de respecter les spécifications transmises, même si elle peut les dépasser. Dans ce cas, le parent peut choisir quoi faire, y compris rogner l'image, faire défiler l'écran, générer une exception ou demander à onMeasure() de réessayer, éventuellement avec des spécifications de mesure différentes.
  • Lorsque la largeur et la hauteur sont calculées, appelez la méthode setMeasuredDimension(int width, int height) avec les mesures calculées. À défaut, une exception est générée.

Voici un résumé des autres méthodes standards que le framework appelle des vues:

Catégorie Méthodes Description
Création Constructeurs Il existe une forme du constructeur qui est appelée lorsque la vue est créée à partir du code, et une autre appelée lorsque la vue est gonflée à partir d'un fichier de mise en page. Le deuxième formulaire analyse et applique les attributs définis dans le fichier de mise en page.
onFinishInflate() Appelée après qu'une vue et tous ses enfants sont gonflés à partir d'un fichier XML
Mise en page onMeasure(int, int) Appelée pour déterminer les exigences de taille pour cette vue et tous ses enfants.
onLayout(boolean, int, int, int, int) Appelée lorsque cette vue doit attribuer une taille et une position à tous ses enfants.
onSizeChanged(int, int, int, int) Appelé lorsque la taille de cette vue est modifiée.
Dessin onDraw(Canvas) Appelé lorsque la vue doit afficher son contenu.
Traitement des événements onKeyDown(int, KeyEvent) Appelée lorsqu'un événement de touche Bas se produit.
onKeyUp(int, KeyEvent) Appelé lorsqu'un événement "touche montante" se produit.
onTrackballEvent(MotionEvent) Appelé lorsqu'un événement de mouvement du trackball se produit.
onTouchEvent(MotionEvent) Appelé en cas de mouvement de l'écran tactile.
Mise au point onFocusChanged(boolean, int, Rect) Appelé lorsque la vue gagne ou perd le focus.
onWindowFocusChanged(boolean) Appelé lorsque la fenêtre contenant la vue est sélectionnée ou perdue.
Association... onAttachedToWindow() Appelé lorsque la vue est associée à une fenêtre.
onDetachedFromWindow() Appelé lorsque la vue est détachée de sa fenêtre.
onWindowVisibilityChanged(int) Appelé lorsque la visibilité de la fenêtre contenant la vue est modifiée.

Commandes composées

Si vous ne souhaitez pas créer un composant entièrement personnalisé, mais que vous souhaitez plutôt créer un composant réutilisable constitué d'un groupe de commandes existantes, il est préférable de créer un composant composé (ou commande composée). En résumé, cela rassemble un certain nombre de commandes ou de vues plus atomiques en un groupe logique d'éléments pouvant être traités comme un seul élément. Par exemple, une boîte combinée peut être la combinaison d'un champ EditText d'une seule ligne et d'un bouton adjacent auquel est associée une liste pop-up. Si l'utilisateur appuie sur le bouton et sélectionne un élément dans la liste, le champ EditText est renseigné, mais il peut également saisir du texte directement dans EditText s'il le souhaite.

Dans Android, deux autres vues sont déjà disponibles pour effectuer cette opération: Spinner et AutoCompleteTextView. Quoi qu'il en soit, ce concept de liste déroulante en est un bon exemple.

Pour créer un composant composé, procédez comme suit:

  • Comme pour un Activity, utilisez l'approche déclarative (basée sur XML) pour créer les composants contenus, ou imbriquez-les de manière programmatique à partir de votre code. Le point de départ habituel est un Layout. Vous devez donc créer une classe qui étend un Layout. Dans le cas d'une boîte combinée, vous pouvez utiliser une LinearLayout avec une orientation horizontale. Vous pouvez imbriquer d'autres mises en page à l'intérieur, de sorte que le composant composé puisse être arbitrairement complexe et structuré.
  • Dans le constructeur de la nouvelle classe, prenez les paramètres attendus par la super-classe et transmettez-les d'abord au constructeur de super-classe. Vous pouvez ensuite configurer les autres vues à utiliser dans votre nouveau composant. C'est ici que vous allez créer le champ EditText et la liste pop-up. Vous pouvez introduire dans le code XML vos propres attributs et paramètres que votre constructeur peut extraire et utiliser.
  • Si vous le souhaitez, vous pouvez créer des écouteurs pour les événements que les vues contenues peuvent générer. Il s'agit par exemple d'une méthode d'écouteur permettant à l'écouteur de clics de l'élément de liste de mettre à jour le contenu de EditText si une sélection est effectuée dans la liste.
  • Vous pouvez également créer vos propres propriétés avec des accesseurs et des modificateurs. Par exemple, laissez la valeur EditText être initialement définie dans le composant et interroger son contenu si nécessaire.
  • Si vous le souhaitez, remplacez onDraw() et onMeasure(). Cela n'est généralement pas nécessaire lorsque vous étendez un Layout, car la mise en page a un comportement par défaut qui fonctionne probablement bien.
  • Vous pouvez également remplacer d'autres méthodes on, telles que onKeyDown(), par exemple pour choisir certaines valeurs par défaut dans la liste pop-up d'une boîte combinée lorsque l'utilisateur appuie sur une touche donnée.

L'utilisation d'un Layout comme base d'une commande personnalisée présente les avantages suivants:

  • Vous pouvez spécifier la mise en page à l'aide des fichiers XML déclaratifs, comme avec un écran d'activité, ou créer des vues par programmation et les imbriquer dans la mise en page à partir de votre code.
  • Les méthodes onDraw() et onMeasure(), ainsi que la plupart des autres méthodes on, ont un comportement approprié. Vous n'avez donc pas besoin de les remplacer.
  • Vous pouvez rapidement construire des vues composées arbitrairement complexes et les réutiliser comme s'il s'agissait d'un seul composant.

Modifier un type de vue existant

Si un composant est semblable à celui que vous souhaitez, vous pouvez l'étendre et ignorer le comportement que vous souhaitez modifier. Vous pouvez faire tout ce que vous faites avec un composant entièrement personnalisé, mais en commençant par une classe plus spécialisée dans la hiérarchie View, vous pouvez obtenir un comportement qui fait ce que vous voulez sans frais.

Par exemple, l'application exemple NotePad présente de nombreux aspects de l'utilisation de la plate-forme Android. Par exemple, vous pouvez étendre une vue EditText pour créer un bloc-notes bordé. Cet exemple n'est pas parfait. Les API permettant de le faire peuvent changer, mais cela démontre les principes.

Si vous ne l'avez pas déjà fait, importez l'exemple NotePad dans Android Studio ou consultez la source à l'aide du lien fourni. En particulier, consultez la définition de LinedEditText dans le fichier NoteEditor.java.

Voici quelques éléments à prendre en compte dans ce fichier:

  1. Définition

    La classe est définie à l'aide de la ligne suivante:
    public static class LinedEditText extends EditText

    LinedEditText est défini comme une classe interne dans l'activité NoteEditor, mais il est public afin qu'il soit accessible en tant que NoteEditor.LinedEditText depuis l'extérieur de la classe NoteEditor.

    De plus, LinedEditText est static, ce qui signifie qu'il ne génère pas les "méthodes synthétiques" qui lui permettent d'accéder aux données de la classe parente. Cela signifie qu'il se comporte comme une classe distincte et non comme quelque chose de étroitement lié à NoteEditor. Il s'agit d'un moyen plus propre de créer des classes internes si elles n'ont pas besoin d'accéder à l'état à partir de la classe externe. Cela permet de réduire la taille de la classe générée et de l'utiliser facilement à partir d'autres classes.

    LinedEditText étend EditText, qui est la vue à personnaliser dans ce cas. Lorsque vous avez terminé, la nouvelle classe peut se substituer à une vue EditText normale.

  2. Initialisation des classes

    Comme toujours, le super est appelé en premier. Il ne s'agit pas d'un constructeur par défaut, mais d'un constructeur paramétré. Le EditText est créé avec ces paramètres lorsqu'il est gonflé à partir d'un fichier de mise en page XML. Par conséquent, le constructeur doit les utiliser et les transmettre également au constructeur de super-classe.

  3. Méthodes de remplacement

    Cet exemple ne remplace que la méthode onDraw(), mais vous devrez peut-être en remplacer d'autres lorsque vous créerez vos propres composants personnalisés.

    Pour cet exemple, le remplacement de la méthode onDraw() vous permet de peindre les lignes bleues sur le canevas de la vue EditText. Le canevas est transmis à la méthode onDraw() remplacée. La méthode super.onDraw() est appelée avant sa fin. La méthode de super-classe doit être appelée. Dans ce cas, appelez-le à la fin après avoir peint les lignes que vous souhaitez inclure.

  4. Composant personnalisé

    Vous disposez désormais de votre composant personnalisé, mais comment pouvez-vous l'utiliser ? Dans l'exemple NotePad, le composant personnalisé est utilisé directement à partir de la mise en page déclarative. Examinez donc note_editor.xml dans le dossier res/layout:

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />
    

    Le composant personnalisé est créé en tant que vue générique dans le fichier XML, et la classe est spécifiée à l'aide du package complet. La classe interne que vous définissez est référencée à l'aide de la notation NoteEditor$LinedEditText, qui est un moyen standard de faire référence aux classes internes dans le langage de programmation Java.

    Si votre composant de vue personnalisée n'est pas défini en tant que classe interne, vous pouvez le déclarer avec le nom de l'élément XML et exclure l'attribut class. Exemple:

    <com.example.android.notepad.LinedEditText
      id="@+id/note"
      ... />
    

    Notez que la classe LinedEditText est désormais un fichier de classe distinct. Lorsque la classe est imbriquée dans la classe NoteEditor, cette technique ne fonctionne pas.

    Les autres attributs et paramètres de la définition sont ceux transmis au constructeur de composant personnalisé, puis transmis au constructeur EditText. Ce sont donc les mêmes paramètres que ceux que vous utilisez pour une vue EditText. Vous pouvez également ajouter vos propres paramètres.

La procédure de création de composants personnalisés dépend de vos besoins.

Un composant plus sophistiqué peut remplacer encore plus de méthodes on et introduire ses propres méthodes d'assistance, en personnalisant considérablement ses propriétés et son comportement. La seule limite est votre imagination et ce que vous avez besoin du composant pour faire.