Créer des composants de vue personnalisée

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

Android propose un modèle de composants sophistiqué et puissant pour créer votre UI, basé sur les classes de mise en page fondamentales View et ViewGroup. La plate-forme inclut diverses sous-classes View et ViewGroup prédéfinies (appelées respectivement widgets et mises en page) que vous pouvez utiliser pour construire votre UI.

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

Parmi les mises en page disponibles figurent LinearLayout, FrameLayout, RelativeLayout et d'autres. Pour obtenir d'autres exemples, consultez Mises en page courantes.

Si aucun des widgets ou mises en page prédéfinis ne répond à vos besoins, vous pouvez créer votre propre sous-classe View. Si vous n'avez besoin que d'apporter de petites modifications à un widget ou une mise en page existants, vous pouvez créer une sous-classe du widget ou de la mise en page et remplacer ses méthodes.

La création de vos propres sous-classes View vous permet de contrôler précisément l'apparence et le fonctionnement d'un élément d'écran. Pour vous donner une idée du contrôle que vous obtenez avec les vues personnalisées, voici quelques exemples de ce que vous pouvez faire avec :

  • Vous pouvez créer un type View entièrement personnalisé, par exemple un bouton de "contrôle du volume" rendu à l'aide de graphiques 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 de liste déroulante (combinaison d'une liste déroulante et d'un champ de texte à saisie libre), un sélecteur à double volet (un volet de gauche et un volet de droite avec une liste dans chacun où vous pouvez réattribuer l'élément à une liste), etc.
  • Vous pouvez remplacer la façon dont un composant EditText est affiché à l'écran. L'application exemple NotePad l'utilise à bon escient pour créer une page de bloc-notes lignée.
  • Vous pouvez capturer d'autres événements, comme des frappes sur le clavier, 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 obtenir des informations de référence détaillées, consultez la classe View.

Approche de base

Voici un aperçu général 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. Remplacez certaines méthodes de la superclasse. Les méthodes de la superclasse à remplacer commencent par on, par exemple onDraw(), onMeasure() et onKeyDown(). Cela ressemble 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. Peut-être souhaitez-vous un VU-mètre graphique qui ressemble à une ancienne jauge analogique, ou une vue de texte karaoké où une balle rebondissante se déplace le long des mots pendant que vous chantez avec une machine à karaoké. Vous pouvez avoir besoin d'une fonctionnalité que les composants intégrés ne peuvent pas fournir, quelle que soit la façon dont vous les combinez.

Heureusement, vous pouvez créer des composants qui ont l'apparence et le comportement de votre choix, en étant limité uniquement par votre imagination, la taille de l'écran et la puissance de traitement disponible. N'oubliez pas que votre application peut devoir s'exécuter sur un appareil dont la puissance est nettement inférieure à celle de votre poste de travail.

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 étendre cette vue pour créer votre nouveau super composant.
  • Vous pouvez fournir un constructeur, qui peut prendre des attributs et des paramètres du fichier XML. Vous pouvez également utiliser vos propres attributs et paramètres, tels que la couleur et la plage du vumètre, ou la largeur et l'amortissement de l'aiguille.
  • Vous souhaiterez probablement créer vos propres écouteurs d'événements, accesseurs de propriétés et modificateurs, ainsi qu'un comportement plus sophistiqué dans votre classe de composants.
  • Vous souhaitez presque certainement remplacer onMeasure() et vous devrez probablement aussi remplacer onDraw() si vous voulez que le composant affiche quelque chose. Bien que les deux aient un comportement par défaut, le onDraw() par défaut ne fait rien, et le onMeasure() par défaut définit toujours une taille de 100 x 100, ce qui n'est probablement pas ce que vous souhaitez.
  • Vous pouvez également remplacer d'autres méthodes on, si nécessaire.

Étendre onDraw() et onMeasure()

La méthode onDraw() fournit un Canvas sur lequel vous pouvez implémenter tout ce que vous souhaitez : des graphiques 2D, d'autres composants standards ou personnalisés, du texte stylisé ou tout ce qui vous vient à l'esprit.

onMeasure() est un peu plus complexe. onMeasure() est un élément essentiel du contrat de rendu entre votre composant et son conteneur. onMeasure() doit être remplacé pour signaler efficacement et précisément les mesures de ses parties contenues. Cela est légèrement complexifié par les exigences de limites 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 sont calculées. Si vous n'appelez pas cette méthode à partir d'une méthode onMeasure() remplacée, une exception est générée 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 pour les restrictions sur les mesures de largeur et de hauteur que vous produisez. Les paramètres widthMeasureSpec et heightMeasureSpec sont des codes entiers représentant des dimensions. Pour obtenir une référence complète aux types de restrictions que ces spécifications peuvent exiger, 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 une largeur et une hauteur de mesure, qui sont nécessaires pour afficher le composant. Il doit essayer de respecter les spécifications transmises, mais il peut les dépasser. Dans ce cas, le parent peut choisir ce qu'il faut faire, y compris écrêter, faire défiler, générer une exception ou demander à onMeasure() de réessayer, peut-être avec des spécifications de mesure différentes.
  • Une fois la largeur et la hauteur calculées, appelez la méthode setMeasuredDimension(int width, int height) avec les mesures calculées. Dans le cas contraire, une exception est générée.

Voici un récapitulatif des autres méthodes standards que le framework appelle sur les 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 forme qui est appelée lorsque la vue est gonflée à partir d'un fichier de mise en page. La deuxième forme 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 ont été gonflés à partir du 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é lorsqu'un événement de touche enfoncée se produit.
onKeyUp(int, KeyEvent) Appelé lorsqu'un événement de touche relâchée se produit.
onTrackballEvent(MotionEvent) Appelé lorsqu'un événement de mouvement du trackball se produit.
onTouchEvent(MotionEvent) Appelé lorsqu'un événement de mouvement de l'écran tactile se produit.
Concentration onFocusChanged(boolean, int, Rect) Appelé lorsque la vue gagne ou perd le focus.
onWindowFocusChanged(boolean) Appelé lorsque la fenêtre contenant la vue est ciblée ou perd le focus.
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 cherchez plutôt à assembler un composant réutilisable composé d'un groupe de contrôles existants, la création d'un composant composé (ou contrôle composé) peut être la meilleure solution. En résumé, cela regroupe un certain nombre de contrôles ou de vues plus atomiques dans un groupe logique d'éléments qui peuvent être traités comme une seule entité. Par exemple, une boîte combinée peut être une combinaison d'un champ EditText sur une seule ligne et d'un bouton adjacent avec une liste pop-up associée. Si l'utilisateur appuie sur le bouton et sélectionne un élément dans la liste, le champ EditText est renseigné. Toutefois, il peut également saisir directement quelque chose dans le champ EditText s'il le souhaite.

Dans Android, deux autres vues sont facilement disponibles pour cela : Spinner et AutoCompleteTextView. Quoi qu'il en soit, ce concept de boîte combinée constitue un bon exemple.

Pour créer un composant composé :

  • 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 d'un type quelconque. Créez donc une classe qui étend un Layout. Dans le cas d'une boîte combinée, vous pouvez utiliser un LinearLayout avec une orientation horizontale. Vous pouvez imbriquer d'autres mises en page à l'intérieur, de sorte que le composant composé peut être arbitrairement complexe et structuré.
  • Dans le constructeur de la nouvelle classe, prenez tous les paramètres attendus par la super-classe et transmettez-les d'abord au constructeur de la super-classe. Vous pouvez ensuite configurer les autres vues à utiliser dans votre nouveau composant. C'est ici que vous créez le champ EditText et la liste pop-up. Vous pouvez introduire vos propres attributs et paramètres dans le fichier XML que votre constructeur peut extraire et utiliser.
  • Vous pouvez également créer des écouteurs pour les événements que vos vues contenues peuvent générer. Par exemple, une méthode d'écouteur pour l'écouteur de clics sur l'élément de liste permet de mettre à jour le contenu de EditText si une sélection de liste est effectuée.
  • Vous pouvez également créer vos propres propriétés avec des accesseurs et des modificateurs. Par exemple, définissez la valeur EditText initialement dans le composant et interrogez son contenu si nécessaire.
  • Vous pouvez également remplacer onDraw() et onMeasure(). Ce n'est généralement pas nécessaire lors de l'extension d'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, comme onKeyDown(), par exemple pour choisir certaines valeurs par défaut dans la liste pop-up d'une boîte combinée lorsqu'une certaine touche est appuyée.

L'utilisation d'un Layout comme base pour un contrôle personnalisé présente plusieurs avantages, dont les suivants :

  • Vous pouvez spécifier la mise en page à l'aide des fichiers XML déclaratifs, comme pour un écran d'activité. Vous pouvez également créer des vues de manière programmatique 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 créer rapidement 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 similaire à celui que vous souhaitez, vous pouvez l'étendre et remplacer 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 sans frais un comportement qui fait ce que vous voulez.

Par exemple, l'application exemple NotePad illustre de nombreux aspects de l'utilisation de la plate-forme Android. Parmi eux, l'extension d'une vue EditText pour créer un bloc-notes ligné. Il ne s'agit pas d'un exemple parfait, et les API permettant d'effectuer cette opération peuvent changer, mais il illustre 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 points à noter dans ce fichier :

  1. Définition

    La classe est définie avec 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 de pouvoir être 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 dites "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 plutôt que comme un élément fortement lié à NoteEditor. Il s'agit d'une façon plus propre de créer des classes internes si elles n'ont pas besoin d'accéder à l'état de la classe externe. Cela permet de limiter 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. Une fois terminé, la nouvelle classe peut remplacer une vue EditText normale.

  2. Initialisation de la classe

    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 accepter et les transmettre également au constructeur de la super-classe.

  3. Méthodes remplacées

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

    Pour cet exemple, la substitution 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 la fin de la méthode. La méthode de la 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 avez maintenant votre composant personnalisé, mais comment l'utiliser ? Dans l'exemple NotePad, le composant personnalisé est utilisé directement à partir de la mise en page déclarative. Consultez 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 une façon 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 comme classe interne, vous pouvez déclarer le composant de vue 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 au constructeur EditText. Il s'agit donc des mêmes paramètres que ceux que vous utilisez pour une vue EditText. Vous pouvez également ajouter vos propres paramètres.

La création de composants personnalisés est aussi complexe que vous le souhaitez.

Un composant plus sophistiqué peut remplacer encore plus de méthodes on et introduire ses propres méthodes d'assistance, ce qui permet de personnaliser considérablement ses propriétés et son comportement. Votre imagination et les besoins de votre composant sont les seules limites.