Xamarin.Forms

Créer un contrôle réutilisable 100% Xamarin.Forms, partie 1

par
publié le
Image par PIRO4D de Pixabay

Dans cet article "from scratch" en deux parties, je vous présente une manière simple de créer une image circulaire avancée et réutilisable à partir de zéro, sans custom renderer, ni bibliothèque tierce.

Partie 1 :

  • Créer une image circulaire à l'aide de la propriété Clip et des Geometry récemment apparues dans Xamarin.Forms
  • Afficher un indicateur d'activité pendant le chargement de l'image
  • Afficher une image de substitution en cas d'absence d'image ou d'url invalide en utilisant la technique de superposition dans une Grid
  • Afficher une bordure autour de l'image à l'aide des Shape, autre nouveauté de Xamarin.Forms
  • Utiliser des fonctionnalités expérimentales avec la version stable actuelle de Xamarin.Forms

Partie 2 :

  • Créer un contrôle réutilisable et compatible MVVM
    • Définir des propriétés bindables (BindableProperty)
    • Utiliser les Converters

Les présentations sont faites, place au concret.


Certaines fonctionnalités comme les Shapes et leurs Geometries nécessitent Xamarin.Forms 4.8 ou supérieure, pensez bien à mettre-à-jour le package après avoir créé votre solution !

Commençons par la fin

Pas de suspens, je vous livre d'emblée un visuel pour que vous sachiez où je vous amène. Celui-ci simule une page de contact :

Démonstration de l'image circulaire avec ses différentes options
Démonstration de l'image circulaire avec ses différentes options

Niveau contenu, la page affiche une liste de contacts dont chaque photo provient d'une source différente :

  1. Une photo chargée à partir d'une url
  2. Une photo ajoutée au projet en tant que ressource
  3. Pas de photo
  4. Une photo pointant sur une url retournant une erreur 404
  5. Une photo pointant sur une url inaccessible

Les trois derniers cas présentent une image de substitution par défaut car aucune photo valide ne peut être affichée.

Les contacts enregistrés comme favoris sont signalés par une bordure jaune autour de la photo.

Pour finir, si vous observez bien l'animation vous remarquerez un bref indicateur de chargement sur les deux dernières photos, le temps que Xamarin.Forms échoue à charger les images.

Mais je vous vois frétiller d'impatience, vous êtes venu ici pour voir du code, alors allons-y

Clip clip clip, découpage de l'image

Plutôt que de vous livrer de gros blocs de code illisibles sur une page web, je vais dans cet article aller directement à l'essentiel, vous trouverez le code complet sur mon compte GitHub, comme d'habitude.

Pour commencer, comment afficher une image circulaire ?

Nous allons utiliser deux fonctionnalités apparues dernièrement dans Xamarin.Forms, la propriété Clip (découper en anglais) et les Geometries.

Clip est une propriété des VisualElements, de type Geometry et qui définit le contour du contenu d'un élément visuel. 

Ce n'est pas clair ? Un exemple, celui qui nous concerne : j'ai une image carrée et je souhaite afficher uniquement son contenu inscrit dans un cercle. Je vais donc définir une Geometry circulaire et l'appliquer à Clip pour découper l'image selon cette forme.

Attention les yeux, voici le code :

<Image Source="monimage.png" HeightRequest="200" WidthRequest="200">
    <Image.Clip>
        <EllipseGeometry RadiusX="100"
                         RadiusY="100"
                         Center="100,100" />
    </Image.Clip>
</Image>

La géométrie circulaire n'existant pas, nous nous baserons sur une ellipse. Celle-ci se définit à partir d'un centre et de deux rayons, horizontal (RadiusX) et vertical (RadiusY). Pour obtenir un cercle, il suffit que RadiusX et RadiusY aient la même valeur. Center permet de positionner l'ellipse par rapport à son contenant.

Pour simplifier le calcul et le positionnement, autant partir d'une image carrée.

Pour obtenir un cercle centré et inscrit dans le carré, les rayons seront moitié plus petits que le côté de l'image et le centre du cercle sera au centre de l'image.

Voilà, obtenir une image circulaire c'est devenu aussi simple que ça avec Xamarin.Forms. Beaucoup de texte et d'explications pour quelque chose devenu finalement assez trivial !

L'image de substitution

Comment faire pour afficher une image par défaut quand il n'y a pas d'image à afficher ?

On va utiliser une technique ancienne et bien connue mais toujours très utile : l'empilement dans une Grid. Les éléments contenus dans une même cellule d'une Grid se superposent les uns sur les autres, le dernier déclaré dans le fichier Xaml étant au-dessus, la lecture de celui-ci étant séquentielle.

Nous allons donc définir une Grid à une seule cellule et y ajouter deux Images : la première est destinée à afficher l'image de substitution, la seconde à afficher l'image proprement dite.

Pour que les deux images se superposent sans décalage, il est nécessaire que les deux composants Image et leurs ellipses aient les mêmes dimensions.

Nous renseignerons également les propriétés HorizontalOptions et VerticalOptions de la Grid avec des valeurs qui forceront celle-ci à adopter les mêmes dimensions que son contenu.

DRY - Je vais avoir besoin de créer deux fois une image circulaire simple, et il est probable que ce besoin se répète dans d'autres contextes. Dans la seconde partie de l'article, nous verrons comment créer un contrôle CircleImage réutilisable encapsulant le code précédent. 

<Grid HorizontalOptions="Center" VerticalOptions="Center">
    <local:CircleImage Source="monPlaceholder.png" ImageSize="128" />
    <local:CircleImage Source="monImage.png" ImageSize="128" />
</Grid>

De cette manière, l'image vient cacher le placeholder si elle est disponible, sinon on voit ce dernier à la place.

L'indicateur de chargement

Aujourd'hui c'est atelier empilage, nous allons encore superposer : ajoutons un ActivityIndicator à notre Grid.

L'ActivityIndicator a un visuel légèrement différent sous Android et iOS mais le principe reste le même : c'est un bidule qui tourne de façon hypnotique pour faire oublier le temps qui passe à l'utilisateur. Pour le voir tourner, il suffit d'affecter True à sa propriété IsRunning.

<Grid>
    <local:CircleImage x:Name="monPlaceholder" ImageSize="128" />
    <local:CircleImage x:Name="monImage" ImageSize="128" />
    
    <ActivityIndicator  IsRunning="True"
                        VerticalOptions="Center"
                        HorizontalOptions="Center"
                        />
</Grid>

Nous avons donc un indicateur qui tourne au centre de notre image. C'est bien, mais il tourne en permanence et nous aurions besoin de l'activer uniquement pendant le chargement de l'image.

Cela tombe bien, le contrôle Image expose une propriété IsLoading qui vaut True pendant le chargement de l'image et False une fois le chargement terminé, peu importe que l'opération ce soit bien déroulée ou non.

Reste une question : comment lier la propriété IsRunning de l'indicateur à la propriété IsLoading de l'image ? Par un Binding bien entendu ! Mais comment binder une propriété d'un contrôle à une propriété d'un autre contrôle ? En précisant la source du Binding !

Le plus simple ici est de donner un nom explicite à notre contrôle Image (monImage par exemple) et de se référer à ce nom comme source du Binding. La propriété visée sera définie comme Path du Binding : "{Binding Source={x:Reference monImage}, Path=IsLoading}"

Avec un peu plus de contexte, cela donne :

<Grid>
    <local:CircleImage x:Name="monPlaceholder" ImageSize="128" />
    <local:CircleImage x:Name="monImage" ImageSize="128" />
    
    <ActivityIndicator  IsRunning="{Binding Source={x:Reference monImage}, Path=IsLoading}"
                        VerticalOptions="Center"
                        HorizontalOptions="Center"
                        />
</Grid>

De cette façon, nous verrons un indicateur de chargement tournoyer au centre de l'image durant le chargement de celle-ci.

ActivityIndicator au centre d'une image
Et pourtant, elle tourne !

La bordure

Pour afficher une bordure autour de l'image, nous allons tirer parti des Shapes encore sous forme expérimentale sous Xamarin.Forms 4.8.

Si la version de Xamarin.Forms que vous utilisez n'intègre pas encore les Shapes de façon officielle, vous devrez déclarer la fonctionnalité expérimentale dans le constructeur de la classe App.xaml.cs sous peine de lever une InvalidOperationException au lancement de l'application :

public App()
{
    Device.SetFlags(new string[] { "Shapes_Experimental" });
    
    InitializeComponent();
    
    MainPage = new MainPage();
}

Les Shapes comme leur nom l'indique sont des formes que l'on peut ajouter en tant que VisualElement sur la page. La forme qui nous intéresse est là encore une Ellipse.

Attention à ne pas confondre l'EllipseGeometry qui est la description d'un objet géométrique, avec l'Ellipse qui est un contrôle Visuel !

Stroke et StrokeThickness sont les deux propriétés exposées par l'Ellipse qui définissent la couleur et l'épaisseur de son contour. Nous n'aurons pas besoin des autres propriétés, mais nous pourrions affiner en définissant le type de contour (pointillé...) ou la couleur de remplissage de l'ellipse.

<Ellipse Stroke="Yellow" StrokeThickness="2" />

Ensuite, c'est juste une histoire d'empilage dans la Grid, vous commencez à avoir l'habitude.

<Grid>
    <local:CircleImage x:Name="monPlaceholder" ImageSize="128" />
    <local:CircleImage x:Name="monImage" ImageSize="128" />
    
    <Ellipse Margin="0"
             HorizontalOptions="Center"
             VerticalOptions="Center"
             Stroke="Yellow"
             StrokeThickness="2"
             HeightRequest="128"
             WidthRequest="128"
             />
             
    <ActivityIndicator [...]  />
</Grid>

Ce qui donne :

Une image circulaire bordée de jaune
Une image circulaire bordée de jaune

Fin de (première) partie

L'article s'avère bien plus détaillé que je ne l'avais prévu au départ et ça commence à être bien consistant. Je vous laisse donc digérer tout ça et vous donne rendez-vous pour la seconde partie dans laquelle nous verrons comment assembler tout ceci pour en faire un contrôle réutilisable avec des propriétés Bindable pour le rendre "MVVM friendly".

Commentaires

Pour utiliser les commentaires, merci d'accepter les cookies du groupe "Disqus".