Personnalisation Android : comment créer un composant d'interface utilisateur qui fait ce que vous voulez

Publié: 2022-03-11

Il n'est pas rare que les développeurs aient besoin d'un composant d'interface utilisateur qui n'est pas fourni par la plate-forme qu'ils ciblent ou qui est effectivement fourni, mais qui manque d'une certaine propriété ou d'un certain comportement. La réponse aux deux scénarios est un composant d'interface utilisateur personnalisé.

Le modèle d'interface utilisateur Android est intrinsèquement personnalisable, offrant les moyens de personnalisation et de test Android et la possibilité de créer des composants d'interface utilisateur personnalisés de différentes manières :

  • Héritez d'un composant existant (c'est-à-dire TextView , ImageView , etc.) et ajoutez/remplacez les fonctionnalités nécessaires. Par exemple, un CircleImageView qui hérite de ImageView , remplaçant la fonction onDraw() pour limiter l'image affichée à un cercle et ajoutant une fonction loadFromFile() pour charger une image à partir de la mémoire externe.

  • Créez un composant composé à partir de plusieurs composants. Cette approche tire généralement parti des mises en page pour contrôler la manière dont les composants sont disposés à l'écran. Par exemple, un LabeledEditText qui hérite de LinearLayout avec une orientation horizontale et contient à la fois un TextView agissant comme une étiquette et un EditText agissant comme un champ de saisie de texte.

    Cette approche pourrait également utiliser la précédente, c'est-à-dire que les composants internes pourraient être natifs ou personnalisés.

  • L'approche la plus polyvalente et la plus complexe consiste à créer un composant auto-dessiné . Dans ce cas, le composant hériterait de la classe View générique et remplacerait des fonctions telles que onMeasure() pour déterminer sa disposition, onDraw() pour afficher son contenu, etc. Les composants créés de cette manière dépendent généralement fortement de l'API de dessin 2D d'Android.

Étude de cas sur la personnalisation d'Android : CalendarView

Android fournit un composant CalendarView natif. Il fonctionne bien et fournit les fonctionnalités minimales attendues de tout composant de calendrier, affichant un mois complet et mettant en évidence le jour en cours. Certains pourraient dire que cela a l'air bien aussi, mais seulement si vous optez pour un look natif et que vous n'avez aucun intérêt à personnaliser son apparence.

Par exemple, le composant CalendarView ne fournit aucun moyen de modifier la façon dont un certain jour est marqué ou la couleur d'arrière-plan à utiliser. Il n'y a également aucun moyen d'ajouter du texte ou des graphiques personnalisés, pour marquer une occasion spéciale, par exemple. En bref, le composant ressemble à ceci, et presque rien ne peut être modifié :

Capture d'écran
CalendarView dans le thème AppCompact.Light .

Faire votre propre

Alors, comment fait-on pour créer sa propre vue d'agenda ? N'importe laquelle des approches ci-dessus fonctionnerait. Cependant, l'aspect pratique exclura généralement la troisième option (graphiques 2D) et nous laissera avec les deux autres méthodes, et nous emploierons un mélange des deux dans cet article.

Pour suivre, vous pouvez trouver le code source ici.

1. La disposition des composants

Tout d'abord, commençons par l'apparence du composant. Pour faire simple, affichons les jours dans une grille, et, en haut, le nom du mois avec les boutons "mois suivant" et "mois précédent".

Capture d'écran
Affichage du calendrier personnalisé.

Cette disposition est définie dans le fichier control_calendar.xml , comme suit. Notez que certains balisages répétitifs ont été abrégés avec ... :

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <!-- date toolbar --> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="12dp" android:paddingBottom="12dp" android:paddingLeft="30dp" android:paddingRight="30dp"> <!-- prev button --> <ImageView android: android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:src="@drawable/previous_icon"/> <!-- date title --> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@+id/calendar_prev_button" android:layout_toLeftOf="@+id/calendar_next_button" android:gravity="center" android:textAppearance="@android:style/TextAppearance.Medium" android:textColor="#222222" android:text="current date"/> <!-- next button --> <ImageView android: ... Same layout as prev button. android:src="@drawable/next_icon"/> </RelativeLayout> <!-- days header --> <LinearLayout android: android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="SUN"/> ... Repeat for MON - SAT. </LinearLayout> <!-- days view --> <GridView android: android:layout_width="match_parent" android:layout_height="340dp" android:numColumns="7"/> </LinearLayout>

2. La classe de composants

La mise en page précédente peut être incluse telle quelle dans une Activity ou un Fragment et cela fonctionnera correctement. Mais l'encapsuler en tant que composant d'interface utilisateur autonome empêchera la répétition du code et permettra une conception modulaire, où chaque module gère une responsabilité.

Notre composant d'interface utilisateur sera un LinearLayout , pour correspondre à la racine du fichier de mise en page XML. Notez que seules les parties importantes sont affichées à partir du code. L'implémentation du composant réside dans CalendarView.java :

 public class CalendarView extends LinearLayout { // internal components private LinearLayout header; private ImageView btnPrev; private ImageView btnNext; private TextView txtDate; private GridView grid; public CalendarView(Context context) { super(context); initControl(context); } /** * Load component XML layout */ private void initControl(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.control_calendar, this); // layout is inflated, assign local variables to components header = (LinearLayout)findViewById(R.id.calendar_header); btnPrev = (ImageView)findViewById(R.id.calendar_prev_button); btnNext = (ImageView)findViewById(R.id.calendar_next_button); txtDate = (TextView)findViewById(R.id.calendar_date_display); grid = (GridView)findViewById(R.id.calendar_grid); } }

Le code est assez simple. Lors de la création, le composant gonfle la mise en page XML, et lorsque cela est fait, il affecte les contrôles internes aux variables locales pour un accès plus facile par la suite.

3. Une certaine logique est nécessaire

Pour que ce composant se comporte réellement comme une vue de calendrier, une certaine logique métier est nécessaire. Cela peut sembler compliqué au début, mais il n'y a vraiment pas grand-chose à faire. Décomposons-le :

  1. La vue du calendrier est large de sept jours et il est garanti que tous les mois commenceront quelque part dans la première rangée.

  2. Tout d'abord, nous devons déterminer à quelle position le mois commence, puis remplir toutes les positions précédentes avec les chiffres du mois précédent (30, 29, 28, etc.) jusqu'à ce que nous atteignions la position 0.

  3. Ensuite, on remplit les jours du mois en cours (1, 2, 3… etc).

  4. Viennent ensuite les jours du mois suivant (encore une fois, 1, 2, 3, etc.), mais cette fois, nous remplissons uniquement les positions restantes dans la ou les dernières lignes de la grille.

Le schéma suivant illustre ces étapes :

Capture d'écran
Logique métier de l'affichage du calendrier personnalisé.

La largeur de la grille est déjà spécifiée à sept cellules, indiquant un calendrier hebdomadaire, mais qu'en est-il de la hauteur ? La plus grande taille de la grille peut être déterminée par le pire scénario d'un mois de 31 jours commençant un samedi, qui est la dernière cellule de la première ligne, et qui nécessitera 5 lignes supplémentaires pour s'afficher en entier. Ainsi, configurer le calendrier pour afficher six lignes (totalisant 42 jours) sera suffisant pour gérer tous les cas.

Mais tous les mois n'ont pas 31 jours ! Nous pouvons éviter les complications qui en découlent en utilisant la fonctionnalité de date intégrée d'Android, en évitant d'avoir à déterminer nous-mêmes le nombre de jours.

Comme mentionné précédemment, les fonctionnalités de date fournies par la classe Calendar rendent l'implémentation assez simple. Dans notre composant, la fonction updateCalendar() implémente cette logique :

 private void updateCalendar() { ArrayList<Date> cells = new ArrayList<>(); Calendar calendar = (Calendar)currentDate.clone(); // determine the cell for current month's beginning calendar.set(Calendar.DAY_OF_MONTH, 1); int monthBeginningCell = calendar.get(Calendar.DAY_OF_WEEK) - 1; // move calendar backwards to the beginning of the week calendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell); // fill cells (42 days calendar as per our business logic) while (cells.size() < DAYS_COUNT) { cells.add(calendar.getTime()); calendar.add(Calendar.DAY_OF_MONTH, 1); } // update grid ((CalendarAdapter)grid.getAdapter()).updateData(cells); // update title SimpleDateFormat sdf = new SimpleDateFormat("MMM yyyy"); txtDate.setText(sdf.format(currentDate.getTime())); }

4. Personnalisable dans l'âme

Étant donné que le composant responsable de l'affichage des jours individuels est un GridView , un bon endroit pour personnaliser l'affichage des jours est l' Adapter , car il est responsable de la conservation des données et du gonflement des vues pour les cellules de grille individuelles.

Pour cet exemple, nous aurons besoin des éléments suivants de notre CalendearView :

  • Le jour présent doit être en texte bleu gras .
  • Les jours en dehors du mois en cours doivent être grisés .
  • Les jours avec un événement doivent afficher une icône spéciale.
  • L'en-tête du calendrier doit changer de couleur en fonction de la saison (été, automne, hiver, printemps).

Les trois premières exigences sont simples à atteindre en modifiant les attributs de texte et les ressources d'arrière-plan. Implémentons un CalendarAdapter pour effectuer cette tâche. Il est assez simple qu'il puisse s'agir d'une classe membre dans CalendarView . En remplaçant la fonction getView() , nous pouvons répondre aux exigences ci-dessus :

 @Override public View getView(int position, View view, ViewGroup parent) { // day in question Date date = getItem(position); // today Date today = new Date(); // inflate item if it does not exist yet if (view == null) view = inflater.inflate(R.layout.control_calendar_day, parent, false); // if this day has an event, specify event image view.setBackgroundResource(eventDays.contains(date)) ? R.drawable.reminder : 0); // clear styling view.setTypeface(null, Typeface.NORMAL); view.setTextColor(Color.BLACK); if (date.getMonth() != today.getMonth() || date.getYear() != today.getYear()) { // if this day is outside current month, grey it out view.setTextColor(getResources().getColor(R.color.greyed_out)); } else if (date.getDate() == today.getDate()) { // if it is today, set it to blue/bold view.setTypeface(null, Typeface.BOLD); view.setTextColor(getResources().getColor(R.color.today)); } // set text view.setText(String.valueOf(date.getDate())); return view; }

L'exigence de conception finale demande un peu plus de travail. Commençons par ajouter les couleurs des quatre saisons dans /res/values/colors.xml :

 <color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>

Ensuite, utilisons un tableau pour définir la saison de chaque mois (en supposant que l'hémisphère nord, pour plus de simplicité ; désolé l'Australie !). Dans CalendarView , nous ajoutons les variables membres suivantes :

 // seasons' rainbow int[] rainbow = new int[] { R.color.summer, R.color.fall, R.color.winter, R.color.spring }; int[] monthSeason = new int[] {2, 2, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2};

De cette façon, la sélection d'une couleur appropriée se fait en sélectionnant la saison appropriée ( monthSeason[currentMonth] ) puis en choisissant la couleur correspondante ( rainbow[monthSeason[currentMonth] ), ceci est ajouté à updateCalendar() pour s'assurer que la couleur appropriée est sélectionnée chaque fois que le calendrier est modifié.

 // set header color according to current season int month = currentDate.get(Calendar.MONTH); int season = monthSeason[month]; int color = rainbow[season]; header.setBackgroundColor(getResources().getColor(color));

Avec cela, nous obtenons le résultat suivant :

Personnalisation Android
La couleur de l'en-tête change selon la saison.

Remarque importante en raison de la façon dont HashSet compare les objets, la vérification ci-dessus eventDays.contains(date) dans updateCalendar() ne donnera pas true pour les objets date à moins qu'ils ne soient exactement identiques. Il n'effectue aucune vérification spéciale pour le type de données Date . Pour contourner ce problème, cette vérification est remplacée par le code suivant :

 for (Date eventDate : eventDays) { if (eventDate.getDate() == date.getDate() && eventDate.getMonth() == date.getMonth() && eventDate.getYear() == date.getYear()) { // mark this day for event view.setBackgroundResource(R.drawable.reminder); break; } }

5. Ça a l'air moche au moment de la conception

Le choix d'Android pour les espaces réservés au moment de la conception peut être discutable. Heureusement, Android instancie en fait notre composant afin de le restituer dans le concepteur d'interface utilisateur, et nous pouvons exploiter cela en appelant updateCalendar() dans le constructeur du composant. De cette façon, le composant aura réellement un sens au moment de la conception.

Capture d'écran

Si l'initialisation du composant demande beaucoup de traitement ou charge beaucoup de données, cela peut affecter les performances de l'IDE. Dans ce cas, Android fournit une fonction astucieuse appelée isInEditMode() qui peut être utilisée pour limiter la quantité de données utilisées lorsque le composant est réellement instancié dans le concepteur d'interface utilisateur. Par exemple, s'il y a beaucoup d'événements à charger dans CalendarView , nous pouvons utiliser isInEditMode() à l'intérieur de la fonction updateCalendar() pour fournir une liste d'événements vide/limitée en mode conception, et charger la vraie sinon.

6. Invoquer le composant

Le composant peut être inclus dans des fichiers de mise en page XML (un exemple d'utilisation peut être trouvé dans activity_main.xml ) :

 <samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>

Et récupéré pour interagir avec une fois la mise en page chargée :

 HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);

Le code ci-dessus crée un HashSet d'événements, y ajoute le jour actuel, puis le transmet à CalendarView . En conséquence, CalendarView affichera le jour actuel en bleu gras et y placera également le marqueur d'événement :

Capture d'écran
CalendarView affichant un événement

7. Ajouter des attributs

Une autre fonctionnalité fournie par Android consiste à attribuer des attributs à un composant personnalisé. Cela permet aux développeurs Android utilisant le composant de sélectionner des paramètres via le XML de mise en page et de voir le résultat immédiatement dans le concepteur d'interface utilisateur, au lieu d'avoir à attendre et de voir à quoi ressemble CalendarView lors de l'exécution. Ajoutons la possibilité de modifier l'affichage du format de la date dans le composant, par exemple pour épeler le nom complet du mois au lieu de l'abréviation à trois lettres.

Pour ce faire, les étapes suivantes sont nécessaires :

  • Déclarez l'attribut. Appelons-le dateFormat et donnons-lui le type de données string . Ajoutez-le à /res/values/attrs.xml :
 <resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
  • Utilisez l'attribut dans la mise en page qui utilise le composant et donnez-lui la valeur "MMMM yyyy" :
 <samples.aalamir.customcalendar.CalendarView xmlns:calendarNS="http://schemas.android.com/apk/res/samples.aalamir.customcalendar" android: android:layout_width="match_parent" android:layout_height="wrap_content" calendarNS:dateFormat="MMMM yyyy"/>
  • Enfin, demandez au composant d'utiliser la valeur de l'attribut :
 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

] Générez le projet et vous remarquerez que les changements de date affichés dans le concepteur d'interface utilisateur utilisent le nom complet du mois, comme "juillet 2015". Essayez de fournir différentes valeurs et voyez ce qui se passe.

Capture d'écran
Modification des attributs CalendarView .

8. Interagir avec le composant

Avez-vous essayé d'appuyer sur un jour précis ? Les éléments internes de l'interface utilisateur de notre composant se comportent toujours comme prévu et déclencheront des événements en réponse aux actions de l'utilisateur. Alors, comment gérons-nous ces événements ?

La réponse comporte deux parties :

  • Capturer des événements à l'intérieur du composant, et
  • Signalez les événements au parent du composant (il peut s'agir d'un Fragment , d'une Activity ou même d'un autre composant).

La première partie est assez simple. Par exemple, pour gérer les éléments de grille à appui long, nous affectons un écouteur correspondant dans notre classe de composants :

 // long-pressing a day grid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> view, View cell, int position, long id) { // handle long-press if (eventHandler == null) return false; Date date = view.getItemAtPosition(position); eventHandler.onDayLongPress(date); return true; } });

Il existe plusieurs méthodes pour signaler des événements. Une simple et directe consiste à copier la façon dont Android le fait : il fournit une interface aux événements du composant qui est implémentée par le parent du composant ( eventHandler dans l'extrait de code ci-dessus).

Les fonctions de l'interface peuvent recevoir toutes les données pertinentes pour l'application. Dans notre cas, l'interface doit exposer un gestionnaire d'événements, auquel est transmise la date du jour pressé. L'interface suivante est définie dans CalendarView :

 public interface EventHandler { void onDayLongPress(Date date); }

L'implémentation fournie par le parent peut être fournie à la vue du calendrier via un setEventHandler() . Voici un exemple d'utilisation de `MainActivity.java' :

 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events); // assign event handler cv.setEventHandler(new CalendarView.EventHandler() { @Override public void onDayLongPress(Date date) { // show returned day DateFormat df = SimpleDateFormat.getDateInstance(); Toast.makeText(MainActivity.this, df.format(date), LENGTH_SHORT).show(); } }); }

Un appui long sur un jour déclenchera un événement d'appui long qui est capturé et géré par le GridView et signalé en appelant onDayLongPress() dans l'implémentation fournie, qui, à son tour, affichera la date du jour pressé à l'écran :

Capture d'écran

Une autre façon plus avancée de gérer cela consiste à utiliser Intents et BroadcastReceivers d'Android. Ceci est particulièrement utile lorsque plusieurs composants doivent être informés de l'événement du calendrier. Par exemple, si appuyer sur un jour dans le calendrier nécessite qu'un texte soit affiché dans une Activity et qu'un fichier soit téléchargé par un Service d'arrière-plan.

L'utilisation de l'approche précédente nécessitera que l' Activity fournisse un EventHandler au composant, gère l'événement, puis le transmette au Service . Au lieu de cela, le fait que le composant diffuse une Intent et que l' Activity et le Service acceptent via leurs propres récepteurs de BroadcastReceivers non seulement facilite la vie, mais aide également à découpler l' Activity et le Service en question.

Conclusion

Découvrez la puissance impressionnante de la personnalisation Android !
Tweeter

Voici comment créer votre propre composant personnalisé en quelques étapes simples :

  • Créez la mise en page XML et personnalisez-la en fonction de vos besoins.
  • Dérivez votre classe de composant du composant parent approprié, selon votre disposition XML.
  • Ajoutez la logique métier de votre composant.
  • Utilisez des attributs pour permettre aux utilisateurs de modifier le comportement du composant.
  • Pour faciliter l'utilisation du composant dans le concepteur d'interface utilisateur, utilisez la fonction isInEditMode() d'Android.

Dans cet article, nous avons créé une vue de calendrier à titre d'exemple, principalement parce que la vue de calendrier de stock fait, à bien des égards, défaut. Mais, vous n'êtes en aucun cas limité quant au type de composants que vous pouvez créer. Vous pouvez utiliser la même technique pour créer tout ce dont vous avez besoin, le ciel est la limite !

Merci d'avoir lu ce guide, je vous souhaite bonne chance dans vos efforts de codage !