Xamarin vous semble figé avec son XAML sans primitives graphiques ? C’est oublié SkiaSharp… Alors découvrez comment développer un contrôle graphique personnalisé avec cette librairie !
Les papiers des lecteurs
Cette rubrique est la vôtre, Dot.Blog publie de temps en temps des articles écrits par ses lecteurs, saisissez cette possibilité !
Ce mois-ci c’est Patrick Breil qui s’y colle. Patrick développe des applications mobiles pour l’entreprise où il exerce ses talents par exemple des suivis de dossiers pour des techniciens qui interviennent sur site et qui peuvent ainsi prendre connaissance des tâches à effectuer et saisir le détail de leurs interventions. L’adoption de tels logiciels réclame non seulement un fonctionnel à la hauteur mais aussi un look & feel comparable aux autres Apps que l’utilisateur à l’habitude de voir sur son smartphone. Cela place la barre assez haute !
Les papiers des lecteurs c’est un retour d’expérience qu’on partage, c’est noble et c’est utile ! Vous aussi vous pouvez me proposer vos articles, pensez-y…
SkiaSharp
Pour ceux qui ne connaissent pas : c’est une API 2D complètement portable iOS, Android, UWP et même Mac OSX ou WPF. SkiaSharp ce sont un peu les primitives graphiques qui manquent au XAML de Xamarin.
J’ai déjà présenté cette librairie l’année dernière et je renvoie le lecteur intéressé à ce papier : SkiaSharp et Xamarin.Forms le dessin 2D cross-plateforme.
Créer un contrôle personnalisé avec SkiaSharp
Il est temps de laisser la parole à Patrick pour son papier…
Objectif
Créer un contrôle permettant d’indiquer une valeur de % sur un arc, un peu comme une barre de progression mais qui aurait été courbée (dessin ci-dessous).
Création du projet
Un classique mais il faut bien commencer par là : Nouveau projet > Prism Xamarin.Forms
(Patrick a fait des captures parfois un peu justes niveau résolution mais on comprend l’action !)
Ajout du Package Nuget SkiaSharp
Création du contrôle utilisateur :
Créer un nouveau dossier ‘UserControls’, puis un nouveau ContentView ‘Gauge’
Le xaml du contrôle ne va pas contenir grand-chose si ce n’est un SKCanvasView qui contiendra le dessin de notre contrôle.
Une fois saisi SKCanvasView il est proposé d’ajouté l’espace de nom SkiaSharp.Views.Forms.
Il suffit ensuite d’ajouter un gestionnaire d’événements à PaintSurface.
Nous passons ensuite au code behind, et plus précisément sur la méthode SKCanvasView_PaintSurface qui sera invoqué par l’événement ‘InvalidateSurface’ du CanvasView.
private void SKCanvasView_PaintSurface(object sender, SkiaSharp.Views.Forms.SKPaintSurfaceEventArgs e)
{
SKSurface surface = e.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
int width = e.Info.Width;
int height = e.Info.Height;
canvas.Translate(width / 2, height / 2);
SKRect Rect = new SKRect(-100, -100, 100, 100);
}
Le code ajouté sur la méthode positionne le point 0,0 au milieu de la surface définit en fonction du contenant de notre contrôle.
Le point 0,0 se situe en haut et à gauche.
Notre contrôle sera centré est définit dans un carré de 200 x 200.
Nous définissons un rectangle (qui pour le coup est carré !)
SKRect Rect = new SKRect(-100, -100, 100, 100);
Il faut maintenant adapter la taille de notre contrôle en fonction de la taille du contenant.
L’échelle retenue sera la plus petite calculée pour la largeur et la hauteur.
float scale = Math.Min((width / Rect.Width), (height / Rect.Height));
canvas.Scale(scale);
Passons au dessin du fond de notre gauge. Pour cela nous devons définir un chemin qui sera parcouru par un pinceau.
Le chemin représente le milieu de l’épaisseur du pinceau, soit 75 + (25/2) = 87.5.
Le code suivant, instancie un chemin path.
using (SKPath path = new SKPath())
{
path.ArcTo(new SKRect(-87.5f,-87.5f,87.5f,87.5f), 135, 270, false);
canvas.DrawPath(path, new SKPaint
{
Color = Color.Gray.ToSKColor(),
Style = SKPaintStyle.Stroke,
StrokeWidth = 25
});
}
Un arc est décrit par la méthode ArcTo.
- Le rectangle décrit l’espace dans lequel sera tracé notre arc. Il s’agit bien d’un rectangle ce qui signifie qu’il est possible de décrire une section d’une ellipse si le rectangle n’est pas un carré !
- La première valeur correspond à l’angle de départ dans le sens horaire (ou inverse trigonométrique), soit dans notre cas 45° + 90° = 135 °
- La seconde l’angle décrit par notre arc, soit 360° -90° =270°
- Le booléen indique si la suite du chemin part ou non de la fin de notre arc.
Nous traçons ensuite notre arc avec un pinceau d’une épaisseur de 25 correspondant à la différence de nos deux rayons.
Pour tester le résultat (provisoire), il suffit d’ajouter notre contrôle au Xaml
<ContentPage xmlns=http://xamarin.com/schemas/2014/forms
xmlns:x=http://schemas.microsoft.com/winfx/2009/xaml
xmlns:usercontrols="clr-namespace:ControleSKiaSharp.UserControls"
x:Class="ControleSKiaSharp.Views.MainPage"
Title="{Binding Title}">
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<usercontrols:Gauge HorizontalOptions="FillAndExpand"VerticalOptions="FillAndExpand"/>
</StackLayout>
</ContentPage>
Notre contrôle est centré et occupe l’espace disponible et s’adapte au changement de taille de la fenêtre.
Ajoutons maintenant le second arc correspondant à la valeur à représenter.
La variable valeur contient la valeur à représenter.
L’angle décrit est calculé en fonction de l’angle complet de notre compteur soit 270°.
La fonction pourrait être adapter pour des plages de valeurs différentes, ainsi qu’un angle de représentation plus petit ou plus grand.
float valeur = 55;
float AngleValeur = (270 * valeur) / 100;
using (SKPath path = new SKPath())
{
path.ArcTo(new SKRect(-87.5f, -87.5f, 87.5f, 87.5f), 135, AngleValeur, false);
canvas.DrawPath(path, new SKPaint
{
Color = Color.CornflowerBlue.ToSKColor(),
Style = SKPaintStyle.Stroke,
StrokeWidth = 25
});
}
Ajoutons maintenant l’affichage de valeur au centre de notre contrôle.
canvas.DrawText(valeur.ToString("0.0") + "%", 0, 10, new SKPaint
{
Color = Color.CornflowerBlue.ToSKColor(),
Style = SKPaintStyle.Fill,
StrokeWidth = 1,
TextAlign = SKTextAlign.Center,
TextSize = 40
});
Notre contrôle à l’apparence souhaité mais pour le moment son utilité est plutôt limitée. Il faut que nous rendions Bindable la propriété valeur pour qu’elle soit dynamique.
// NDE : Patrick a eu un problème pour définir la propriété en float et il a réussi à le faire en utilisant le type string.
// Ce n’est évidemment pas optimal… j’attends qu’il m’envoie le code source du projet pour regarder
// et je vous tiendrai au courant !
float valeur = 0.0f;
public static readonly BindableProperty ValeurProperty =
BindableProperty.Create(propertyName: "Valeur",
returnType: typeof(string),
declaringType: typeof(Gauge),
defaultValue: "0.0",
defaultBindingMode: BindingMode.Default,
propertyChanged: UpdateValeur);
public string Valeur
{
get { return (string)GetValue(ValeurProperty); }
set { SetValue(ValeurProperty, value); }
}
private static void UpdateValeur(BindableObject bindable, object oldValue, object newValue)
{
var ctrl = (Gauge)bindable;
var v = (string)newValue;
ctrl.valeur = float.Parse(v.Replace(".",","));
ctrl.canvas.InvalidateSurface();
}
On modifie ensuite le Xaml de la page pour lier la propriété du contrôle avec un Slider
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:usercontrols="clr-namespace:ControleSKiaSharp.UserControls"
x:Class="ControleSKiaSharp.Views.MainPage"
Title="{Binding Title}">
<Grid HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Slider x:Name="Slider" Minimum="0" Maximum="100"/>
<usercontrols:Gauge Grid.Row="1" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
Valeur="{Binding Source={x:Reference Slider}, Path=Value}"/>
</Grid>
</ContentPage>
Notre contrôle est maintenant fonctionnel et peut être utilisé en MVVM.
SkiaSharp est une bonne technologie pour créer ses propres contrôles et peux nous dispenser de télécharger des Packages complet pour n’utiliser que quelques composants.
Ce contrôle est relativement simple, mais il est possible d’en créer de plus complexe en ajoutant des propriétés dynamique comme les couleurs, les unités, des listes de valeurs, etc…
Exemple : Une application mobile de contrôle pour Terrarium.
Conclusion
Remercions à nouveau Patrick pour ce bel effort ! Cela vous prouve qu’en prenant un peu le temps on peut écrire des papiers intéressants et les partager avec la communauté… je vous incite à suivre cet exemple !
Cela prouve aussi que Xamarin.Forms a atteint une vraie maturité. Son C# est à la hauteur (c’est le même que pour WPF ou UWP), son XAML s’est complexifié avec le temps et supporte l’essentiel de ce langage, et pour la partie graphique, problématique dans un environnement cross-plateforme, nous disposons de librairies simples d’utilisation qui comblent le fossé avec le XAML classique.
Il est donc possible à la fois de gagner du temps (un code, de multiples cibles natives) sans rien sacrifier au look & feel si essentiel sur les smartphones.
A vos claviers ! (pour coder mais aussi pour écrire un papier des lecteurs !)
Stay Tuned !