Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Xamarin.Forms RoutingEffect : faîtes sauter les particules !

Xamarin.Forms natif / pas natif ? Certains trolls traînent encore… Ici je vous montre comment utiliser une librairie JAR native au sein d’un projet Android natif le tout depuis Xamarin.Forms… Natif ? Certainement ! Fun ? Assurément !

Les particules c’est sympa

Quoi de mieux pour un écran annonçant une bonne nouvelle, une fête, un anniversaire, ou même pour un bouton qui doit “pétiller” qu’un bon jet de particules colorées ? De confettis tournoyants ?

Les animations de ce type sont toujours un petit plus. Bien utilisées elles améliorent l’UX, utilisées juste pour la décoration elles peuvent tout de même amener un peu de vie dans des écrans figés et tout ce qui peut apparaître “organique” plait à l’humain car ça lui ressemble. Il s’identifie mieux. Un robot à forme humanoïde produira de l’empathie (ou de la peur) on l’a vu avec les robots “maltraités” pour les tests de Boston Dynamics. En revanche un type qui casse un ordinateur en forme de boîte de cube tout le monde s’en fiche…

Pensez organique dans vos UI, même flat, ce qui semble paradoxal mais ne l’est pas… Des animations bien vues, un peu de “swing” de “chabada” de chaloupé dans le passage d’une page à une autre, d’une info à l’autre, sans exagération donne à l’humain l’impression que ses gestes agissent sur un objet réel, et ça il aime… D’ailleurs “Mterial design” est à la croisée des chemins entre le pur flat Metro et le skeuomorphisme appeulien, on reste flat visuellement on n’ajoute pas de 3D bidon ou de reflets pseudo-réalistes en revanche on prend le côté vivant des choses dans les transitions, les compositions visuelles. La vie c’est le mouvement. Ce qui est immobile est mort. C’est aussi simple. Et ce qui se meut avec grâce est vivant, ce qui bouge de façon mécanique est robotisé. En partant de ces constatations on peut construire des UI épurées, modernes, mais organiques.

Les particules font parties de ces animations organiques qui plaisent.

A vous de bien les utiliser… Moi je vais juste vous montrer comment le faire techniquement.

Les RoutingEffect’s

Il me faudrait un article complet pour couvrir se sujet et ici je ne vais faire qu’exploiter le principe. Cela devrait suffire à vous montrer comment le mettre en œuvre. Mais je vais préciser un peu tout de même.

Commençons par lire la doc… mais ça je vous laisse le faire tout seul est c’est ici : Xamarin.Forms Effets.

Le principe est le suivant, dixit la doc :

Les interfaces utilisateur de Xamarin.Forms sont rendues à l'aide des contrôles natifs de la plate-forme cible, permettant aux applications Xamarin.Forms de conserver l'apparence appropriée pour chaque plate-forme. Les effets permettent de personnaliser les contrôles natifs sur chaque plate-forme sans avoir à recourir à une implémentation de rendu personnalisée.

On comprend ainsi que les effets servent à faciliter la personnalisation des contrôles réellement utilisés par les parties natives (iOS, Android, UWP…) sans se lancer dans la création d’un Custom Renderer (rendu personnalisé) plus complexe à mettre en place.

Je n’irai pas beaucoup plus loin pour les raisons expliquée plus haut et aussi parce que j’ai déjà consacré un article à la comparaison entre Effets et Rendus Personnalisés… C’est ici : Xamarin.Forms : si on parlait des effets ?

A quoi cela ressemble ?

Pour tout ce qui est visuel j’aime montrer le résultat avant le code, on comprend mieux où on veut en venir…

Voici une animation GIF d’une capture de l’émulateur Android durant mes tests :

Particles


Là où on tape sur l’écran un jet de particules apparait. Dans cette App j’ai ajouté deux sliders en haut de l’écrans pour modifier la vitesse des particules et leur durée de vie. On pourrait agir sur d’autres paramètres.

Les particules

Les particules elles-mêmes ne sont que des images PNG avec canal de transparence. L’App dispose de plusieurs images et même de confettis animés (images Android au format XML qui décrit l’animation), vous trouverez tout cela dans le code que je vais mettre à disposition car sinon nous perdrons trop de temps dans les détails que vous pouvez voir directement sous Visual Studio.

Le système de gestion des particules en revanche est plus subtile car il provient d’une librairie java appelée Leonids, elle ne pèse que 81 Ko mais c’est de l’Android “pur jus”, du natif de chez natif.

Pour utiliser une librairie native il faut faire une sorte de “binding”, une liaison, qui adapte au passage le nom des variables ou autre. C’est une stratégie très simple sur laquelle je ne m’arrêterai pas car nous allons utiliser un projet GitHub qui propose justement un binding avec Leonids. Il s’appelle LeonidBind ce qui est logique… et on ajoute ce projet dans la solution et c’est tout bon ! Comme le source est fourni vous pourrez là aussi regarder de près comment c’est fait, ce qui est très intéressant. Pour compléter vous pourrez lire la documentation “Liaison d’un fichier JAR” traduite en français chez Microsoft.

How-to

Armés de ces précieuses informations regardons maintenant le “comment faire”. Réunir tout ça et produire une App qui marche…

La première chose est la création de l’effet. Comme j’ai expliqué pas mal de choses dans l’article cité plus haut je vais vous montrer le code uniquement, lisez l’article en question pour comprendre les détails si besoin est.

On commence par ajouter l’effet dans le code Xamarin.Forms :

image

Ici nous allons détourner un peu l’esprit des Effets Xamarin.Forms il faut malgré tout le dire. Comme expliqué plus en amont les effets servent à modifier facilement le comportement d’un contrôle natif sans avoir à recréer un contrôle avec un Rendu Personnalisé. C’est vrai qu’il s’agit là de l’utilisation première de cette stratégie. Mais on peut aussi la détourner à notre profit pour contrôler non pas un simple contrôle natif qui existerait déjà dans Xamarin.Forms mais pour piloter une librairie native totalement nouvelle… C’est ce que nous faisons dans cet exemple. Mais le principe de déclaration et d’utilisation des Effets restent identiques.

On s’en doute ce n’est pas avec le code présenté ci-dessus qu’on va faire exploser des particules… ce code est juste une sorte d’interface qui ne contient que des propriétés…

Les Effets ont des noms et surtout une sorte de namespace pour éviter les collisions entre des effets de différentes sources qui porteraient les mêmes noms, ce qui est courant pour des choses… courantes ! (je vous dois combien monsieur Lapalisse ?).

Ici l’effet s’appelle ParticleEffet, ce qui n’est guère original mais très descriptif, c’est un “bon” nom. Mais il lui faut un namespace pour le différencier d’autres ParticleEffet que d’autres pourraient créer… En l’espère l’exemple nous provient de XamGirl, une nana qui développe et qui est jolie, réflexion totalement machiste assumée, il y a trop peu de filles dans notre métier, réflexion féministe totalement assumée, faites le tri selon vos convictions ! Or Charlin Agramonte nous vient de République Dominicaine et bosse pour CrossGeeks. Si je n’aime pas la copie servile même en citant les sources, je déteste encore plus le vol sans citation des sources. Voilà qui est fait et pourquoi je n’ai pas changé le namespace dans le code.

Toutefois j’ai pas mal modifié le code en fonction de mes standards qui sont un poil plus exigeants, mais c’est une autre affaire (et c’est loin d’être parfait vu que c’est juste une démo parmi toutes celles que je produis et que vous vous doutez bien que je n’ai pas tout mon temps pour cette activité gratuite…).

Et donc on l’appellerio CrossGeeks.ParticleEffets. Et c’est dans la boîte.

Le code natif

Plongeons dans les entrailles du code natif Android, enfin de notre projet Xamarin.Droid qui bien que natif reste en C# et .NET ! On ne va pas se lancer trop loin dans le java bricolé de Google (pas le temps de rigoler sur les procès Oracle / Google à propos de ce langage, appels, cours suprême etc, un vrai feuilleton).

Donc dans notre projet natif Android nous allons ajouter la classe qui faire le travail… Enfin, en partie puisqu’il va utiliser la fameuse librairie native (un using en haut du code vous le montre) :

Ici le code iOS pour changer, pour vous faire voir que ça marche quasi identiquement (le code iOS est dans le projet à télécharger dans le zip fourni, voir en fin d’article) :

using System;
using System.Linq;
using System.Threading.Tasks;
using CoreAnimation;
using Foundation;
using ParticlesSample.iOS.Effects;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ResolutionGroupName("CrossGeeks")]
[assembly: ExportEffect(typeof(ParticleEffect), "ParticleEffect")]
namespace ParticlesSample.iOS.Effects
{
    public class ParticleEffect : PlatformEffect
    {
        readonly UITapGestureRecognizer tapDetector;

        public ParticleEffect()
        {
            tapDetector = new UITapGestureRecognizer(() =>
            {
                var control = Control ?? Container;
                var tapPoint = tapDetector.LocationInView(control);

                var effect = (ParticlesSample.Effects.ParticleEffect)Element.Effects.FirstOrDefault(p => p is ParticlesSample.Effects.ParticleEffect);

                var mode = effect?.Mode ?? ParticlesSample.Effects.EmitMode.OneShot;
                var lifeTime = effect?.LifeTime??1.5f;
                var numberOfItems = effect?.NumberOfParticles ?? 4000;
                var scale = effect?.Scale ?? 1.0f;
                var speed = effect?.Speed*1000 ?? 100.0f;
                var image = effect?.Image ?? "Icon";

                var emitterLayer = new CAEmitterLayer();


                emitterLayer.Position = tapPoint;
                emitterLayer.Shape = CAEmitterLayer.ShapeCircle;
            

                var cell = new CAEmitterCell();

                cell.Name = "pEmitter";

                cell.BirthRate = numberOfItems;
                cell.Scale =0f;
                cell.ScaleRange = scale;
                cell.Velocity = speed;
                cell.LifeTime =(float)lifeTime;

                cell.EmissionRange = (nfloat)Math.PI * 2.0f;
                cell.Contents = UIImage.FromBundle(image).CGImage;

                emitterLayer.Cells = new CAEmitterCell[] { cell };

               
                control.Layer.AddSublayer(emitterLayer);

                if (mode == ParticlesSample.Effects.EmitMode.OneShot)
                {
                    Task.Delay(1).ContinueWith(t => {

                        Device.BeginInvokeOnMainThread(() =>
                        {
                            emitterLayer.SetValueForKeyPath(NSNumber.FromInt32(0), new NSString("emitterCells.pEmitter.birthRate"));

                        });

                    });
                }
               
            }); 
        }
        protected override void OnAttached()
        {
            var control = Control ?? Container;

            control.AddGestureRecognizer(tapDetector);
            tapDetector.Enabled = true;


        }



        protected override void OnDetached()
        {
            var control = Control ?? Container;
            control.RemoveGestureRecognizer(tapDetector);
            tapDetector.Enabled = false;
        }

    }
}

Si je ne me focalise pas trop sur la version iOS c’est que sous cet OS même si la construction via des Effets Xamarin.Forms restent la même il n’y a pas d’utilisation de librairie tierce ce qui rend l’exercice natif moins sportif. En … effet, sous iOS on dispose de CAEmitterLayer qui n’est autre qu’une surface capable d’animer et de faire le rendu de particules… Pour ces trucs là, comme pour le son et le midi, c’est mieux chez Apple même si je ne les aime pas, il faut dire la vérité et leurs APIs sont autrement plus complètes que celles de Google ou Microsoft.

Et maintenant, le code Android que vous attendiez !

using System.Linq;
using DroidView = Android.Views;
using Com.Plattysoft.Leonids;
using ParticlesSample.Effects;
using Plugin.CurrentActivity;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using ParticleEffect = ParticlesSample.Droid.Effects.ParticleEffect;
using Android.Support.V4.Content;

[assembly: ResolutionGroupName("CrossGeeks")]
[assembly: ExportEffect(typeof(ParticleEffect), "ParticleEffect")]
namespace ParticlesSample.Droid.Effects
{
    public class ParticleEffect : PlatformEffect
    {
        ParticleSystem particleSystem;

        protected override void OnAttached()
        {
            var control = Control ?? Container;
            control.Touch += OnControlTouch;
        }

        protected override void OnDetached()
        {
            var control = Control ?? Container;
            control.Touch -= OnControlTouch;
        }


        void OnControlTouch(object sender, Android.Views.View.TouchEventArgs e)
        {
         
                var effect = (ParticlesSample.Effects.ParticleEffect)Element.Effects.FirstOrDefault(p => p is ParticlesSample.Effects.ParticleEffect);

                var mode = effect?.Mode ?? ParticlesSample.Effects.EmitMode.OneShot;
                var lifeTime = (long)(effect?.LifeTime * 1000 ?? (long)1500);
                var numberOfItems = effect?.NumberOfParticles ?? 4000;
                var scale = effect?.Scale ?? 1.0f;
                var speed = effect?.Speed ?? 0.1f;
                var image = effect?.Image ?? "ic_launcher";

                switch (e.Event.Action)
                {
                    case DroidView.MotionEventActions.Down:

                        var drawableImage = ContextCompat.GetDrawable(CrossCurrentActivity.Current.Activity, CrossCurrentActivity.Current.Activity.Resources.GetIdentifier(image, "drawable", CrossCurrentActivity.Current.Activity.PackageName));
                        particleSystem = new ParticleSystem(CrossCurrentActivity.Current.Activity, numberOfItems, drawableImage, lifeTime);
                        particleSystem
                          .SetSpeedRange(0f, speed)
                          .SetScaleRange(0, scale)
                          .Emit((int)e.Event.GetX(), (int)e.Event.GetY(), numberOfItems);
                        
                        break;
                    case DroidView.MotionEventActions.Move:
                  
                        break;
                    case DroidView.MotionEventActions.Up:
                        if (mode == EmitMode.OneShot)
                        {
                            particleSystem?.StopEmitting();
                        }

                       break;
                }

        }


    }
}

A chaque fois vous noterez que le code commence par des attributs qui servent à faire la liaison avec la classe que vous avons déclarée. Tout cela est le mécanisme normal des Effets.

En route !

Ne reste plus qu’à faire marcher tout ça…

Pour cela l’App Xamarin.Forms possède une MainPage dans laquelle je n’ai pas fait grand chose d’autre que de créer deux Sliders et d’ajouter une surface (ici un StackLayout) auquel j’ai ajouter l’effet et hop ! l’affaire est dans le sac (avec juste un poil de code-behind pour gérer le changement de valeur des Sliders et l’appliquer à l’objet Effet qui se trouve en mémoire et ce via un x:Name qui lui a été donné).

image

C’est simple, ça marche, c’est génial !

Pour aller plus loin le mieux est de jouer avec la démo vous-mêmes en téléchargeant le code : https://www.dropbox.com/s/smqko8qmx0orql4/ParticlesSample.zip?dl=0

Conclusion

Xamarin.Forms c’est du natif et donc on peut interagir à volonté avec n’importe quelle librairie native de chaque plateforme. Ici c’est un prétexte visuel distrayant pour parler aussi des Effets et de leur détournement pour piloter une librairie native tierce.

Un bon environnement de développement n’est pas seulement un ensemble langage / IDE plaisant à utiliser, c’est aussi un framework puissant et une grande facilité de travailler avec les spécificités de chaque OS sans demander la permission à un éditeur qui détient seul le pouvoir de vous donner accès ou non à du code natif.

Xamarin.Forms répond à toutes ces exigences et c’est pourquoi malgré la concurrence (ce qui est sain en soi), cela reste la technologie que je continue de plébisciter pour faire du cross-plateforme…

Mais on verra bien d’autres choses ! alors…

Stay Tuned !

blog comments powered by Disqus