Il peut s’avérer très pratique de prendre une capture d’écran par exemple lors d’un rapport de bogue automatique ou autre. Mais cette fonction n’existe pas de base, toutefois il est très facile de la mettre en œuvre comme nous allons le voir…
Screenshot par programmation
Il peut arriver que votre App ait besoin de prendre un screenshot, soit pour ses besoins internes (rapport automatique de bogue par exemple) soit en tant que fonctionnalité offerte à l’utilisateur. De base Xamarin.Forms ne propose rien à ce sujet et les Xamarin.Essentials non plus me semble-t-il (ou j’ai mal vu ?). De fait le meilleur moyen de régler le problème est d’implémenter soi-même le code nécessaire, à la fois pour Android et iOS. UWP ne servant plus à faire des Apps mobiles on considérera que la problématique ne le concerne pas directement mais rien ne vous empêche d’ajouter le code UWP à la solution proposée bien entendu.
Certain se demanderont peut-être pourquoi j’ai choisi pour illustration l’image d’un Canon EOS 80D, d’abord par ce qu’il est difficile de représenter la fonction ”screenshot” et qu’un appareil photo illustre au moins le fait qu’il s’agit de prendre un cliché, et puis parce que c’est l’appareil que j’utilise depuis peu en complément de mon 650D et que c’est une super machine. A ceux qui n’avaient rien remarqué de spécial je viens de vous apprendre quelque chose dont vous vous fichez complètement ! Enfin pas tant que ça si vous regardez Dot.Vlog (http://www.youtube.com/TheDotBlog) sur Youtube car c’est avec ce matériel que je tourne certaines vidéos…
Programmation par injection de dépendance
Pour ajouter des fonctions de ce type à Xamarin.Forms on utilise principalement un procédé bien rodé : l’injection de dépendance.
Concrètement ici nous allons définir dans notre application Xamarin.Forms une interface, par exemple comme cela :
On pourrait choisir de retourner d’emblée une image mais ce n’est pas évident car nous allons produire des PNG et il n’existe pas de classe PNG pour recevoir une image de ce type, ce n’est qu’un tas d’octets. Nous verrons plus loin comment exploiter le résultat.
Sous Android il suffit de créer une classe qui capture l’écran en PNG et qui retourne le tout sous la forme d’un tableau d’octets, mais on n’oubliera pas bien entendu d’enregistrer la classe dans le système de dépendance Xamarin (première ligne)… Le code est le suivant :
On remarquera que la version Android ayant besoin de la MainActivity il faut la stocker quelque part… D’où la variable de type Activity. Mais comme le service de dépendance retourne une nouvelle instance à chaque fois le moyen de conserver cette information au travers de toutes les instances à venir et d’en faire une variable Static. Reste le problème de son initialisation. C’est pourquoi nous fournissons une méthode SetAndroidActivity. Elle sera appelée dans MainActivity juste avant le lancement de l’Application. Cette méthode n’apparait pas dans l’interface, elle est propre à la version Android qui seule la verra et l’utilisera…
Pour iOS on utilise la séquence suivante (le Mac est temporairement ailleurs désolé je n’ai pas de capture…) :
[assembly: Dependency(typeof(Screenshot))]
public namespace ScreenShotApp.iOS
{
public class Screenshot : IScreenShot
{
public byte[] Capture()
{
var capture = UIScreen.MainScreen.Capture();
using (NSData data = capture.AsPNG())
{
var bytes = new byte[data.Length];
Marshal.Copy(data.Bytes, bytes, 0, Convert.ToInt32(data.Length));
return bytes;
}
}
}
}
Rien de bien sorcier dans ces deux codes. Android utilise le contenu de la RootView de l’activité en cours pour dessiner dans un canvas puis le compresse en mode PNG avec une qualité de 90%, le code iOS est un peu plus simple puisqu’il existe une méthode de capture plus directe sous cet OS (Capture dans le MainScreen).
Quoi qu’il en soit, beauté de l’injection de dépendance, notre code Xamarin.Forms ne verra rien de ces détails !
Pour terminer l’implémentation native n’oublions pas d’initialiser la version Android dans MainActivity en appelant SetAndroidActivity de notre classe native. Sous iOS cela est inutile.
Dans la MainPage de notre Application Xamarin.Forms nous posons un label de titrage, un bouton pour déclencher la prise de vue et une image qui affichera une sorte de miniature de la capture :
L’image porte le nom “Shot” et le bouton est lié à un gestionnaire dans le code behind. Ce code est le suivant :
On commence par appeler notre service de capture et dans la foulée on demande de prendre le screenshot qu’on stocke dans la variable ScreenshotData.
Ici on pourrait très bien stocker ce tableau d’octets dans un fichier disque avec l’extension PNG.
Mais pour les besoins de la démo (et parce que cela peut servir dans certaines situations) nous allons juste afficher le résultat sans le rendre persistant. Il faut alors transformer le tableau d’octets en ImageSource qui sera assignée à la propriété Source de l’Image se trouvant sur notre page.
Le résultat après une capture est le suivant :
Un screnshot de l’émulateur nous montre à la fois notre application et le screenshot qu’elle a elle-même pris, en miniature dans le Frame en mode AspectFit… Une mise en abime intéressante !
Et MVVM dans tout ça ?
Ah… je le sentais venir… Et vous avez bien raison ! Et MVVM dans tout ça ?
Cela ne change rien ni à l’interface ni aux classes natives ni à leur éventuelle initialisation. Rien du tout.
La seule différence se trouve dans l’utilisation du résultat du screenshot qui est retourné comme un tableau d’octets. Dans la version barbare en code behind on transforme tout simplement les octets en ImageSource et on manipule directement l’objet Image.par son x:Name.
En MVVM on ferait comment ?
Plus de x:Name sur l’image. De toute façon interdiction d’accéder à un objet d’interface, donc obligation de passer par un Binding.
Oui mais même en publiant le tableau d’octets cela ne marchera pas… Il faudrait fournir tout de même un ImageSource. Il nous faut alors un Convertisseur… comme d’habitude !
Son code reprendra exactement celui du code behind mais enveloppé dans celui d’un IValueConverter.
Ce qui donnera pour ce dernier :
public class BytesToImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return null;
return ImageSource.FromStream(() => new MemoryStream((byte[])value));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
}
On n’omettra pas de déclarer une instance du convertisseur dans les ressources (de la page par exemple) :
<ContentPage.Resources>
<ResourceDictionary>
<converters:BytesToImageConverter x:Key="BytesToImage" />
</ResourceDictionary>
</ContentPage.Resources>
Et on utilisera tout cela dans un Binding sur la propriété Source d’un objet Image de cette façon :
<Image Source="{Binding Image, Converter={StaticResource BytesToImage}}" Aspect="AspectFit" />
La magie de l’injection de dépendance de code natif s’opérera sans problème tout en respectant les canons du dogme MVVM, un peu casse-pied je l’accorde car alourdissant parfois les choses, mais tellement nécessaire pour assurer un découplage salvateur…
Conclusion
Prendre des captures d’écran dans une App peut s’avérer utile dans divers contextes. Le faire de façon propre, c’est à dire réutilisable, souple et cross-plateforme n’est pas très compliquée grâce aux mécanismes de Xamarin.Forms.
Ajouter l’indispensable tournure si particulière de MVVM par dessus ne fait que compliquer un peu les choses mais pour un résultat de qualité “pro”.
Vous pouvez bien sûr remplacer la capture d’écran par n’importe quelle fonctionnalité qui ne peut être mise en place qu’en natif, cet article montre la façon de faire du cross-plateforme par injection de dépendance plus que l’exemple particulier de la capture d’écran. Si ce code vous est utile tant mieux, mais c’est bien la façon de procéder qui compte ici plus que ce dernier.
Pour en savoir toujours plus….
Stay Tuned !