Appel d’événements à partir d’effets

Un effet peut définir et appeler un événement, en signalant des changements dans la vue native sous-jacente. Cet article explique comment implémenter le suivi de l’interaction tactile multipoint de bas niveau et comment générer des événements qui indiquent une activité tactile.

L’effet décrit dans cet article fournit l’accès aux événements tactiles de bas niveau. Ces événements de bas niveau ne sont pas disponibles via les classes GestureRecognizer existantes, mais ils sont très importants pour certains types d’applications. Par exemple, une application de peinture tactile a besoin de suivre les doigts individuels au fil de leur déplacement sur l’écran. Un clavier musical doit détecter les appuis et les relâchements sur les touches individuelles, mais aussi un doigt glissant d’une touche à l’autre dans un glissando.

Un effet est idéal pour le suivi des doigts tactiles multiples, car il peut être attaché à n’importe quel Xamarin.Forms élément.

Événements tactiles des plateformes

Les plateformes iOS, Android et Windows universelle incluent toutes une API de bas niveau qui permet aux applications de détecter l’activité tactile. Ces plateformes distinguent toutes trois types d’événements tactiles de base :

  • Enfoncé, quand un doigt touche l’écran
  • Déplacé, quand un doigt touchant l’écran se déplace
  • Relâché, quand le doigt est détaché de l’écran

Dans un environnement d’interaction tactile multipoint, plusieurs doigts peuvent toucher l’écran en même temps. Les différentes plateformes incluent un numéro d’identification (ID) que les applications peuvent utiliser pour faire la distinction entre plusieurs doigts.

Dans iOS, la classe UIView définit trois méthodes remplaçables, TouchesBegan, TouchesMoved, et TouchesEnded, correspondant à ces trois événements de base. L’article Suivi de l’interaction tactile multipoint explique comment utiliser ces méthodes. Cependant, un programme iOS n’a pas besoin de remplacer une classe qui dérive de UIView pour utiliser ces méthodes. Dans iOS, UIGestureRecognizer définit également ces trois mêmes méthodes, et vous pouvez attacher une instance d’une classe qui dérive de UIGestureRecognizer à n’importe quel objet UIView.

Dans Android, la classe View définit une méthode remplaçable nommée OnTouchEvent pour traiter toutes les activités tactiles. Le type de l’activité tactile est défini par les membres d’énumération Down, PointerDown, Move, Up et PointerUp, comme décrit dans l’article Suivi de l’interaction tactile multipoint. Dans Android, View définit également un événement nommé Touch, qui permet d’attacher un gestionnaire d’événements à n’importe quel objet View.

Sur la plateforme Windows universelle (UWP), la classe UIElement définit des événements nommés PointerPressed, PointerMoved et PointerReleased. Ceux-ci sont décrits dans l’article Gérer une entrée de pointeur sur MSDN et dans la documentation de l’API pour la classe UIElement.

L’API Pointer de la plateforme Windows universelle est destinée à unifier les entrées de la souris, de l’écran tactile et du stylet. Pour cette raison, l’événement PointerMoved est appelé quand la souris traverse un élément, même si l’utilisateur n’appuie pas sur un bouton de la souris. L’objet PointerRoutedEventArgs qui accompagne ces événements a une propriété nommée Pointer, qui a une propriété nommée IsInContact indiquant si un bouton de la souris est enfoncé ou si un doigt est en contact avec l’écran.

En outre, la plateforme Windows universelle définit deux autres événements nommés PointerEntered et PointerExited. Ils indiquent quand une souris ou un doigt se déplace d’un élément à un autre. Par exemple, considérez les deux éléments adjacents nommés A et B. Les deux éléments ont des gestionnaires installés pour les événements de pointeur. Quand un doigt appuie sur A, l’événement PointerPressed est appelé. Quand le doigt se déplace, A appelle les événements PointerMoved. Si le doigt se déplace de A à B, A appelle un événement PointerExited et B appelle un événement PointerEntered. Si le doigt est ensuite détaché de l’écran, B appelle un événement PointerReleased.

Les plateformes iOS et Android sont différentes de la plateforme Windows universelle : la vue qui reçoit d’abord l’appel à TouchesBegan ou OnTouchEvent quand un doigt touche la vue continue de recevoir toutes les activités tactiles, même si le doigt se déplace vers d’autres vues. La plateforme Windows universelle peut se comporter de façon similaire si l’application capture le pointeur : dans le gestionnaire d’événements PointerEntered, l’élément appelle CapturePointer et obtient ensuite toute l’activité tactile de ce doigt.

L’approche de la plateforme Windows universelle s’avère très pratique pour certains types d’applications, par exemple un clavier musical. Chaque touche peut gérer les événements tactiles pour cette touche, et détecter quand un doigt a glissé d’une touche à une autre en utilisant les événements PointerEntered et PointerExited.

Pour cette raison, l’effet de suivi tactile décrit dans cet article implémente l’approche UWP.

L’API Effet de suivi tactile

L’exemple contient les classes (et une énumération) qui implémentent le suivi tactile de bas niveau. Ces types appartiennent à l’espace de noms TouchTracking et commencent par le mot Touch. Le projet de bibliothèque .NET Standard TouchTrackingEffectDemos inclut l’énumération TouchActionType pour le type d’événements tactiles :

public enum TouchActionType
{
    Entered,
    Pressed,
    Moved,
    Released,
    Exited,
    Cancelled
}

Toutes les plateformes incluent également un événement qui indique que l’événement tactile a été annulé.

La classe TouchEffect de la bibliothèque .NET Standard dérive de RoutingEffect, et définit un événement nommé TouchAction et une méthode nommée OnTouchAction qui appelle l’événement TouchAction :

public class TouchEffect : RoutingEffect
{
    public event TouchActionEventHandler TouchAction;

    public TouchEffect() : base("XamarinDocs.TouchEffect")
    {
    }

    public bool Capture { set; get; }

    public void OnTouchAction(Element element, TouchActionEventArgs args)
    {
        TouchAction?.Invoke(element, args);
    }
}

Notez également la propriété Capture. Pour capturer les événements tactiles, une application doit définir cette propriété sur true avant un événement Pressed. Sinon, les événements tactiles se comportent comme ceux de la plateforme Windows universelle.

La classe TouchActionEventArgs de la bibliothèque .NET Standard contient toutes les informations qui accompagnent chaque événement :

public class TouchActionEventArgs : EventArgs
{
    public TouchActionEventArgs(long id, TouchActionType type, Point location, bool isInContact)
    {
        Id = id;
        Type = type;
        Location = location;
        IsInContact = isInContact;
    }

    public long Id { private set; get; }

    public TouchActionType Type { private set; get; }

    public Point Location { private set; get; }

    public bool IsInContact { private set; get; }
}

Une application peut utiliser la propriété Id pour suivre des doigts individuels. Notez la propriété IsInContact. Cette propriété est toujours true pour les événements Pressed et false pour les événements Released. Elle est également toujours true pour les événements Moved sur iOS et Android. La propriété IsInContact peut être false pour les événements Moved sur la plateforme Windows universelle quand le programme est en cours d’exécution sur le poste de travail et que le pointeur de la souris se déplace sans qu’un bouton soit enfoncé.

Vous pouvez utiliser la TouchEffect classe dans vos propres applications en incluant le fichier dans le projet de bibliothèque .NET Standard de la solution et en ajoutant une instance à la Effects collection de n’importe quel Xamarin.Forms élément. Attachez un gestionnaire à l’événement TouchAction pour obtenir les événements tactiles.

Pour utiliser TouchEffect dans votre propre application, vous avez également besoin des implémentations de plateforme incluses dans la solution TouchTrackingEffectDemos.

Implémentations de l’effet de suivi de tactile

Les implémentations iOS, Android et UWP de TouchEffect sont décrites ci-dessous, en commençant avec l’implémentation la plus simple (UWP) et en finissant par l’implémentation iOS, car elle est d’une plus grande complexité structurelle que les autres.

Implémentation UWP

L’implémentation UWP de TouchEffect est la plus simple. Comme d’habitude, la classe dérive de PlatformEffect et inclut deux attributs d’assembly :

[assembly: ResolutionGroupName("XamarinDocs")]
[assembly: ExportEffect(typeof(TouchTracking.UWP.TouchEffect), "TouchEffect")]

namespace TouchTracking.UWP
{
    public class TouchEffect : PlatformEffect
    {
        ...
    }
}

Le remplacement de OnAttached enregistre certaines informations sous forme de champs et attache des gestionnaires à tous les événements de pointeur :

public class TouchEffect : PlatformEffect
{
    FrameworkElement frameworkElement;
    TouchTracking.TouchEffect effect;
    Action<Element, TouchActionEventArgs> onTouchAction;

    protected override void OnAttached()
    {
        // Get the Windows FrameworkElement corresponding to the Element that the effect is attached to
        frameworkElement = Control == null ? Container : Control;

        // Get access to the TouchEffect class in the .NET Standard library
        effect = (TouchTracking.TouchEffect)Element.Effects.
                    FirstOrDefault(e => e is TouchTracking.TouchEffect);

        if (effect != null && frameworkElement != null)
        {
            // Save the method to call on touch events
            onTouchAction = effect.OnTouchAction;

            // Set event handlers on FrameworkElement
            frameworkElement.PointerEntered += OnPointerEntered;
            frameworkElement.PointerPressed += OnPointerPressed;
            frameworkElement.PointerMoved += OnPointerMoved;
            frameworkElement.PointerReleased += OnPointerReleased;
            frameworkElement.PointerExited += OnPointerExited;
            frameworkElement.PointerCanceled += OnPointerCancelled;
        }
    }
    ...
}    

Le gestionnaire OnPointerPressed appelle l’événement d’effet en appelant le champ onTouchAction de la méthode CommonHandler :

public class TouchEffect : PlatformEffect
{
    ...
    void OnPointerPressed(object sender, PointerRoutedEventArgs args)
    {
        CommonHandler(sender, TouchActionType.Pressed, args);

        // Check setting of Capture property
        if (effect.Capture)
        {
            (sender as FrameworkElement).CapturePointer(args.Pointer);
        }
    }
    ...
    void CommonHandler(object sender, TouchActionType touchActionType, PointerRoutedEventArgs args)
    {
        PointerPoint pointerPoint = args.GetCurrentPoint(sender as UIElement);
        Windows.Foundation.Point windowsPoint = pointerPoint.Position;  

        onTouchAction(Element, new TouchActionEventArgs(args.Pointer.PointerId,
                                                        touchActionType,
                                                        new Point(windowsPoint.X, windowsPoint.Y),
                                                        args.Pointer.IsInContact));
    }
}

OnPointerPressed vérifie également la valeur de la propriété Capture dans la classe d’effet de la bibliothèque .NET Standard, et appelle CapturePointer si la valeur est true.

Les autres gestionnaires d’événements UWP sont encore plus simples :

public class TouchEffect : PlatformEffect
{
    ...
    void OnPointerEntered(object sender, PointerRoutedEventArgs args)
    {
        CommonHandler(sender, TouchActionType.Entered, args);
    }
    ...
}

Implémentation Android

Les implémentations iOS et Android sont nécessairement plus complexes, car elles doivent implémenter les événements Exited et Entered quand un doigt se déplace d’un élément à un autre. Les deux implémentations sont structurées de façon similaire.

La classe TouchEffect d’Android installe un gestionnaire pour l’événement Touch :

view = Control == null ? Container : Control;
...
view.Touch += OnTouch;

La classe définit également deux dictionnaires statiques :

public class TouchEffect : PlatformEffect
{
    ...
    static Dictionary<Android.Views.View, TouchEffect> viewDictionary =
        new Dictionary<Android.Views.View, TouchEffect>();

    static Dictionary<int, TouchEffect> idToEffectDictionary =
        new Dictionary<int, TouchEffect>();
    ...

viewDictionary reçoit une nouvelle entrée chaque fois que le remplacement de OnAttached est appelé :

viewDictionary.Add(view, this);

L’entrée est supprimée du dictionnaire dans OnDetached. Chaque instance de TouchEffect est associée à une vue particulière à laquelle l’effet est attaché. Le dictionnaire statique permet à n’importe quelle instance de TouchEffect d’énumérer toutes les autres vues et leurs instances de TouchEffect correspondantes. Ceci est nécessaire pour permettre le transfert des événements d’une vue à l’autre.

Android affecte un code d’ID aux événements tactiles qui permet à une application de suivre des doigts individuels. idToEffectDictionary associe ce code d’ID à une instance de TouchEffect. Un élément est ajouté à ce dictionnaire quand le gestionnaire Touch est appelé pour un appui avec un doigt :

void OnTouch(object sender, Android.Views.View.TouchEventArgs args)
{
    ...
    switch (args.Event.ActionMasked)
    {
        case MotionEventActions.Down:
        case MotionEventActions.PointerDown:
            FireEvent(this, id, TouchActionType.Pressed, screenPointerCoords, true);

            idToEffectDictionary.Add(id, this);

            capture = libTouchEffect.Capture;
            break;

L’élément est supprimé de idToEffectDictionary quand le doigt est détaché de l’écran. La méthode FireEvent accumule simplement toutes les informations nécessaires pour appeler la méthode OnTouchAction :

void FireEvent(TouchEffect touchEffect, int id, TouchActionType actionType, Point pointerLocation, bool isInContact)
{
    // Get the method to call for firing events
    Action<Element, TouchActionEventArgs> onTouchAction = touchEffect.libTouchEffect.OnTouchAction;

    // Get the location of the pointer within the view
    touchEffect.view.GetLocationOnScreen(twoIntArray);
    double x = pointerLocation.X - twoIntArray[0];
    double y = pointerLocation.Y - twoIntArray[1];
    Point point = new Point(fromPixels(x), fromPixels(y));

    // Call the method
    onTouchAction(touchEffect.formsElement,
        new TouchActionEventArgs(id, actionType, point, isInContact));
}

Tous les autres types d’événements tactiles sont traités de deux façons différentes : si la propriété Capture est true, l’événement tactile est une traduction relativement simple en informations pour TouchEffect. Cela devient plus compliqué quand Capture est false, car les événements tactiles doivent alors être déplacés d’une vue à une autre. C’est de la responsabilité de la méthode CheckForBoundaryHop, qui est appelée lors des événements de déplacement. Cette méthode utilise les deux dictionnaires statiques. Elle énumère viewDictionary pour déterminer la vue que le doigt touche actuellement, et elle utilise idToEffectDictionary pour stocker l’instance active de TouchEffect (et par conséquent la vue active) associée à un ID spécifique :

void CheckForBoundaryHop(int id, Point pointerLocation)
{
    TouchEffect touchEffectHit = null;

    foreach (Android.Views.View view in viewDictionary.Keys)
    {
        // Get the view rectangle
        try
        {
            view.GetLocationOnScreen(twoIntArray);
        }
        catch // System.ObjectDisposedException: Cannot access a disposed object.
        {
            continue;
        }
        Rectangle viewRect = new Rectangle(twoIntArray[0], twoIntArray[1], view.Width, view.Height);

        if (viewRect.Contains(pointerLocation))
        {
            touchEffectHit = viewDictionary[view];
        }
    }

    if (touchEffectHit != idToEffectDictionary[id])
    {
        if (idToEffectDictionary[id] != null)
        {
            FireEvent(idToEffectDictionary[id], id, TouchActionType.Exited, pointerLocation, true);
        }
        if (touchEffectHit != null)
        {
            FireEvent(touchEffectHit, id, TouchActionType.Entered, pointerLocation, true);
        }
        idToEffectDictionary[id] = touchEffectHit;
    }
}

S’il y a eu un changement dans idToEffectDictionary, la méthode appelle éventuellement FireEvent pour Exited et Entered afin de passer d’une vue à l’autre. Cependant, le doigt peut avoir été déplacé dans une zone occupée par une vue sans instance de TouchEffect attachée, ou de cette zone dans une vue avec l’effet attaché.

Notez le bloc try et catch lors de l’accès à la vue. Lors d’un accès à une page suivi d’un retour à la page d’accueil, la méthode OnDetached n’est pas appelée, et les éléments restent dans viewDictionary, mais Android les considère comme supprimés.

Implémentation iOS

L’implémentation iOS est similaire à l’implémentation Android, sauf que la classe TouchEffect d’iOS doit instancier un dérivé de UIGestureRecognizer. Il s’agit d’une classe dans le projet iOS nommée TouchRecognizer. Cette classe gère deux dictionnaires statiques qui stockent les instances de TouchRecognizer :

static Dictionary<UIView, TouchRecognizer> viewDictionary =
    new Dictionary<UIView, TouchRecognizer>();

static Dictionary<long, TouchRecognizer> idToTouchDictionary =
    new Dictionary<long, TouchRecognizer>();

La plus grande partie de la structure de cette classe TouchRecognizer est similaire à la classe TouchEffect d’Android.

Important

Un grand nombre des vues dans UIKit n’ont pas la fonctionnalité tactile activée par défaut. La fonctionnalité tactile peut être activée en ajoutant view.UserInteractionEnabled = true; au remplacement OnAttached dans la classe TouchEffect du projet iOS. Cela doit se produire après l’obtention de UIView qui correspond à l’élément auquel l’effet est joint.

Utilisation de l’effet tactile

L’exemple de programme contient cinq pages qui testent l’effet de suivi tactile pour les tâches courantes.

La page BoxView Dragging vous permet d’ajouter des éléments BoxView à AbsoluteLayout, puis de les faire glisser sur l’écran. Le fichier XAML instancie deux vues Button pour l’ajout d’éléments BoxView à AbsoluteLayout et pour l’effacement de AbsoluteLayout.

La méthode dans le fichier code-behind qui ajoute un nouvel élément BoxView à AbsoluteLayout ajoute également un objet TouchEffect à BoxView et attache un gestionnaire d’événements à l’effet :

void AddBoxViewToLayout()
{
    BoxView boxView = new BoxView
    {
        WidthRequest = 100,
        HeightRequest = 100,
        Color = new Color(random.NextDouble(),
                          random.NextDouble(),
                          random.NextDouble())
    };

    TouchEffect touchEffect = new TouchEffect();
    touchEffect.TouchAction += OnTouchEffectAction;
    boxView.Effects.Add(touchEffect);
    absoluteLayout.Children.Add(boxView);
}

Le gestionnaire d’événements TouchAction traite tous les événements tactiles pour tous les éléments BoxView, mais il doit procéder avec une certaine prudence : il ne peut pas autoriser deux doigts sur un même élément BoxView, car le programme implémente seulement l’opération de glisser et il y aurait une interférence entre les deux doigts. Pour cette raison, la page définit une classe incorporée pour chaque doigt suivi :

class DragInfo
{
    public DragInfo(long id, Point pressPoint)
    {
        Id = id;
        PressPoint = pressPoint;
    }

    public long Id { private set; get; }

    public Point PressPoint { private set; get; }
}

Dictionary<BoxView, DragInfo> dragDictionary = new Dictionary<BoxView, DragInfo>();

dragDictionary contient une entrée pour chaque BoxView faisant l’objet d’une opération de glisser.

L’action tactile Pressed ajoute un élément à ce dictionnaire, et l’action Released le supprime. La logique de Pressed doit vérifier s’il existe déjà un élément dans le dictionnaire pour cet élément BoxView. Le cas échéant, cela signifie que l’élément BoxView fait déjà l’objet d’une opération de glisser et que le nouvel événement est un deuxième doigt sur ce même élément BoxView. Pour les actions Moved et Released, le gestionnaire d’événements doit vérifier si le dictionnaire a une entrée pour cet élément BoxView et que la propriété Id de l’événement tactile pour cet élément BoxView glissé correspond à celui de l’entrée du dictionnaire :

void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
    BoxView boxView = sender as BoxView;

    switch (args.Type)
    {
        case TouchActionType.Pressed:
            // Don't allow a second touch on an already touched BoxView
            if (!dragDictionary.ContainsKey(boxView))
            {
                dragDictionary.Add(boxView, new DragInfo(args.Id, args.Location));

                // Set Capture property to true
                TouchEffect touchEffect = (TouchEffect)boxView.Effects.FirstOrDefault(e => e is TouchEffect);
                touchEffect.Capture = true;
            }
            break;

        case TouchActionType.Moved:
            if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
            {
                Rectangle rect = AbsoluteLayout.GetLayoutBounds(boxView);
                Point initialLocation = dragDictionary[boxView].PressPoint;
                rect.X += args.Location.X - initialLocation.X;
                rect.Y += args.Location.Y - initialLocation.Y;
                AbsoluteLayout.SetLayoutBounds(boxView, rect);
            }
            break;

        case TouchActionType.Released:
            if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
            {
                dragDictionary.Remove(boxView);
            }
            break;
    }
}

La logique de Pressed définit la propriété Capture de l’objet TouchEffect sur true. Ceci a pour effet de délivrer tous les événements suivants pour ce doigt au même gestionnaire d’événements.

La logique de Moved déplace l’élément BoxView en modifiant la propriété attachée LayoutBounds. La propriété Location des arguments de l’événement est toujours relative à l’élément BoxView faisant l’objet d’une opération de glisser, et si l’élément BoxView est déplacé à une vitesse constante, les propriétés Location des événements consécutifs seront approximativement les mêmes. Par exemple, si un doigt appuie sur l’élément BoxView en son centre, l’action Pressed stocke une propriété PressPoint de (50, 50), qui reste la même pour les événements suivants. Si l’élément BoxView est déplacé en diagonale à une vitesse constante, les propriétés Location suivantes pendant l’action Moved peuvent avoir des valeurs (55, 55), auquel cas la logique de Moved ajoute 5 à la position horizontale et verticale de l’élément BoxView. Ceci opération déplace l’élément BoxView de façon à ce que son centre se trouve à nouveau directement sous le doigt.

Vous pouvez déplacer plusieurs éléments BoxView simultanément en utilisant des doigts différents.

Triple capture d’écran de la page BoxView Dragging

Sous-classement de la vue

Souvent, il est plus facile pour un Xamarin.Forms élément de gérer ses propres événements tactiles. La page Draggable BoxView Dragging fonctionne comme la page BoxView Dragging, mais les éléments que l’utilisateur fait glisser sont des instances d’une classe DraggableBoxView qui dérive de BoxView :

class DraggableBoxView : BoxView
{
    bool isBeingDragged;
    long touchId;
    Point pressPoint;

    public DraggableBoxView()
    {
        TouchEffect touchEffect = new TouchEffect
        {
            Capture = true
        };
        touchEffect.TouchAction += OnTouchEffectAction;
        Effects.Add(touchEffect);
    }

    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        switch (args.Type)
        {
            case TouchActionType.Pressed:
                if (!isBeingDragged)
                {
                    isBeingDragged = true;
                    touchId = args.Id;
                    pressPoint = args.Location;
                }
                break;

            case TouchActionType.Moved:
                if (isBeingDragged && touchId == args.Id)
                {
                    TranslationX += args.Location.X - pressPoint.X;
                    TranslationY += args.Location.Y - pressPoint.Y;
                }
                break;

            case TouchActionType.Released:
                if (isBeingDragged && touchId == args.Id)
                {
                    isBeingDragged = false;
                }
                break;
        }
    }
}

Le constructeur crée et attache l’élément TouchEffect et définit la propriété Capture quand cet objet est instancié pour la première fois. Aucun dictionnaire n’est nécessaire, car la classe stocke elle-même les valeurs isBeingDragged, pressPoint et touchId associées à chaque doigt. La gestion de Moved modifie les propriétés TranslationX et TranslationY : la logique fonctionne donc même si le parent de DraggableBoxView n’est pas un élément AbsoluteLayout.

Intégration à SkiaSharp

Les deux démonstrations suivantes nécessitent des graphiques, et elles utilisent pour cela SkiaSharp. Vous souhaiterez peut-être en savoir plus sur l’utilisation de SkiaSharp Xamarin.Forms avant d’étudier ces exemples. Les deux premiers articles (« SkiaSharp Drawing Basics » et « SkiaSharp Lines and Paths ») couvrent tout ce dont vous avez besoin ici.

La page Ellipse Drawing vous permet de dessiner une ellipse en faisant glisser votre doigt sur l’écran. Selon la façon dont vous déplacez votre doigt, vous pouvez dessiner l’ellipse en partant du coin supérieur gauche vers le coin inférieur droit, ou en partant de n’importe quel autre coin vers le coin opposé. L’ellipse est dessinée avec une couleur et une opacité aléatoires.

Triple capture d’écran de la page Ellipse Drawing

Si vous touchez ensuite une des ellipses, vous pouvez la faire glisser vers un autre emplacement. Ceci nécessite une technique appelée « test d’atteinte » (hit-testing), qui implique la recherche de l’objet graphique à un point donné. Les points de suspension SkiaSharp ne sont pas Xamarin.Forms des éléments, de sorte qu’ils ne peuvent pas effectuer leur propre TouchEffect traitement. Le TouchEffect doit s’appliquer à tout l’objet SKCanvasView.

Le fichier EllipseDrawPage.xaml instancie la SKCanvasView dans une Grid avec une seule cellule. L’objet TouchEffect est attaché à cette Grid :

<Grid x:Name="canvasViewGrid"
        Grid.Row="1"
        BackgroundColor="White">

    <skia:SKCanvasView x:Name="canvasView"
                        PaintSurface="OnCanvasViewPaintSurface" />
    <Grid.Effects>
        <tt:TouchEffect Capture="True"
                        TouchAction="OnTouchEffectAction" />
    </Grid.Effects>
</Grid>

Dans Android et la plateforme Windows universelle, l’élément TouchEffect peut être attaché directement à l’élément SKCanvasView, mais sur iOS, cela ne fonctionne pas. Notez que la propriété Capture est définie sur true.

Chaque ellipse rendue par SkiaSharp est représentée par un objet de type EllipseDrawingFigure :

class EllipseDrawingFigure
{
    SKPoint pt1, pt2;

    public EllipseDrawingFigure()
    {
    }

    public SKColor Color { set; get; }

    public SKPoint StartPoint
    {
        set
        {
            pt1 = value;
            MakeRectangle();
        }
    }

    public SKPoint EndPoint
    {
        set
        {
            pt2 = value;
            MakeRectangle();
        }
    }

    void MakeRectangle()
    {
        Rectangle = new SKRect(pt1.X, pt1.Y, pt2.X, pt2.Y).Standardized;
    }

    public SKRect Rectangle { set; get; }

    // For dragging operations
    public Point LastFingerLocation { set; get; }

    // For the dragging hit-test
    public bool IsInEllipse(SKPoint pt)
    {
        SKRect rect = Rectangle;

        return (Math.Pow(pt.X - rect.MidX, 2) / Math.Pow(rect.Width / 2, 2) +
                Math.Pow(pt.Y - rect.MidY, 2) / Math.Pow(rect.Height / 2, 2)) < 1;
    }
}

Les propriétés StartPoint et EndPoint sont utilisées quand le programme traite une entrée tactile ; la propriété Rectangle est utilisée pour dessiner l’ellipse. La propriété LastFingerLocation intervient quand l’ellipse est déplacée, et la méthode IsInEllipse participe au test d’atteinte. La méthode retourne true si le point est à l’intérieur de l’ellipse.

Le fichier code-behind gère trois collections :

Dictionary<long, EllipseDrawingFigure> inProgressFigures = new Dictionary<long, EllipseDrawingFigure>();
List<EllipseDrawingFigure> completedFigures = new List<EllipseDrawingFigure>();
Dictionary<long, EllipseDrawingFigure> draggingFigures = new Dictionary<long, EllipseDrawingFigure>();

Le dictionnaire draggingFigure contient un sous-ensemble de la collection completedFigures. Le gestionnaire d’événements PaintSurface de SkiaSharp rend simplement les objets de ces collections completedFigures et inProgressFigures :

SKPaint paint = new SKPaint
{
    Style = SKPaintStyle.Fill
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKCanvas canvas = args.Surface.Canvas;
    canvas.Clear();

    foreach (EllipseDrawingFigure figure in completedFigures)
    {
        paint.Color = figure.Color;
        canvas.DrawOval(figure.Rectangle, paint);
    }
    foreach (EllipseDrawingFigure figure in inProgressFigures.Values)
    {
        paint.Color = figure.Color;
        canvas.DrawOval(figure.Rectangle, paint);
    }
}

La partie la plus délicate du traitement de l’événement tactile est la gestion de Pressed. C’est là où le test d’atteinte est effectué, mais si le code détecte une ellipse sous le doigt de l’utilisateur, cette ellipse peut être déplacée seulement si elle n’est pas en cours de déplacement par un autre doigt. S’il n’existe pas d’ellipse sous le doigt de l’utilisateur, le code commence le processus de dessin d’une nouvelle ellipse :

case TouchActionType.Pressed:
    bool isDragOperation = false;

    // Loop through the completed figures
    foreach (EllipseDrawingFigure fig in completedFigures.Reverse<EllipseDrawingFigure>())
    {
        // Check if the finger is touching one of the ellipses
        if (fig.IsInEllipse(ConvertToPixel(args.Location)))
        {
            // Tentatively assume this is a dragging operation
            isDragOperation = true;

            // Loop through all the figures currently being dragged
            foreach (EllipseDrawingFigure draggedFigure in draggingFigures.Values)
            {
                // If there's a match, we'll need to dig deeper
                if (fig == draggedFigure)
                {
                    isDragOperation = false;
                    break;
                }
            }

            if (isDragOperation)
            {
                fig.LastFingerLocation = args.Location;
                draggingFigures.Add(args.Id, fig);
                break;
            }
        }
    }

    if (isDragOperation)
    {
        // Move the dragged ellipse to the end of completedFigures so it's drawn on top
        EllipseDrawingFigure fig = draggingFigures[args.Id];
        completedFigures.Remove(fig);
        completedFigures.Add(fig);
    }
    else // start making a new ellipse
    {
        // Random bytes for random color
        byte[] buffer = new byte[4];
        random.NextBytes(buffer);

        EllipseDrawingFigure figure = new EllipseDrawingFigure
        {
            Color = new SKColor(buffer[0], buffer[1], buffer[2], buffer[3]),
            StartPoint = ConvertToPixel(args.Location),
            EndPoint = ConvertToPixel(args.Location)
        };
        inProgressFigures.Add(args.Id, figure);
    }
    canvasView.InvalidateSurface();
    break;

L’autre exemple SkiaSharp est la page Finger Paint. Vous pouvez sélectionner une couleur de trait et une épaisseur de trait dans deux vues Picker, puis dessiner avec un ou plusieurs doigts :

Triple capture d’écran de la page Finger Paint

Cet exemple nécessite également une classe distincte pour représenter chaque ligne peinte à l’écran :

class FingerPaintPolyline
{
    public FingerPaintPolyline()
    {
        Path = new SKPath();
    }

    public SKPath Path { set; get; }

    public Color StrokeColor { set; get; }

    public float StrokeWidth { set; get; }
}

Un objet SKPath est utilisé pour rendre chaque ligne. Le fichier FingerPaint.xaml.cs gère deux collections de ces objets, une pour ces polylignes en cours de dessin et une autre pour les polylignes terminées :

Dictionary<long, FingerPaintPolyline> inProgressPolylines = new Dictionary<long, FingerPaintPolyline>();
List<FingerPaintPolyline> completedPolylines = new List<FingerPaintPolyline>();

Le traitement de Pressed crée une FingerPaintPolyline, appelle MoveTo sur l’objet de chemin pour stocker le point initial et ajoute cet objet au dictionnaire inProgressPolylines. Le traitement de Moved appelle LineTo sur l’objet de chemin avec la nouvelle position du doigt, et le traitement de Released transfère la polyligne terminée de inProgressPolylines vers completedPolylines. À nouveau, le code gérant le dessin de SkiaSharp est relativement simple :

SKPaint paint = new SKPaint
{
    Style = SKPaintStyle.Stroke,
    StrokeCap = SKStrokeCap.Round,
    StrokeJoin = SKStrokeJoin.Round
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKCanvas canvas = args.Surface.Canvas;
    canvas.Clear();

    foreach (FingerPaintPolyline polyline in completedPolylines)
    {
        paint.Color = polyline.StrokeColor.ToSKColor();
        paint.StrokeWidth = polyline.StrokeWidth;
        canvas.DrawPath(polyline.Path, paint);
    }

    foreach (FingerPaintPolyline polyline in inProgressPolylines.Values)
    {
        paint.Color = polyline.StrokeColor.ToSKColor();
        paint.StrokeWidth = polyline.StrokeWidth;
        canvas.DrawPath(polyline.Path, paint);
    }
}

Suivi de l’interaction tactile de vue à vue

Tous les exemples précédents ont défini la propriété Capture de TouchEffect sur true, quand TouchEffect a été créé ou quand l’événement Pressed s’est produit. Ceci garantit que le même élément reçoit tous les événements associés au doigt qui a appuyé en premier sur la vue. L’exemple final ne définit pasCapture sur true. Ceci provoque un comportement différent quand un doigt en contact avec l’écran se déplace d’un élément à un autre. L’élément déplacé par le doigt reçoit un événement avec une propriété Type définie sur TouchActionType.Exited et le deuxième élément reçoit un événement avec un Type défini sur TouchActionType.Entered.

Ce type de traitement d’événement tactile est très pratique pour un clavier musical. Une touche doit être en mesure de détecter quand l’utilisateur l’enfonce, mais également quand un doigt glisse d’une touche à une autre.

La page Silent Keyboard définit des petites classes WhiteKey et BlackKey qui dérivent de Key, qui dérive elle-même de BoxView.

La classe Key est prête à être utilisée dans un programme musical réel. Elle définit des propriétés publiques nommées IsPressed et KeyNumber, cette dernière étant destinée à être définie sur le code de la touche tel qu’il est établi par le standard MIDI. La classe Key définit également un événement nommé StatusChanged, qui est appelé quand la propriété IsPressed change.

Plusieurs doigts sont autorisés sur chaque touche. Pour cette raison, la classe Key gère un élément List des numéros d’ID d’interaction tactile de tous les doigts touchant actuellement cette touche :

List<long> ids = new List<long>();

Le gestionnaire d’événements TouchAction ajoute un ID à la liste ids pour un type d’événement Pressed et pour un type Entered, mais seulement quand la propriété IsInContact est true pour l’événement Entered. L’ID est supprimé de la List pour un événement Released ou Exited :

void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
    switch (args.Type)
    {
      case TouchActionType.Pressed:
          AddToList(args.Id);
          break;

        case TouchActionType.Entered:
            if (args.IsInContact)
            {
                AddToList(args.Id);
            }
            break;

        case TouchActionType.Moved:
            break;

        case TouchActionType.Released:
        case TouchActionType.Exited:
            RemoveFromList(args.Id);
            break;
    }
}

Les méthodes AddToList et RemoveFromList vérifient toutes deux si l’élément List a changé de vide en non vide ou inversement et, si tel est le cas, elles appellent l’événement StatusChanged.

Les différents éléments WhiteKey et BlackKey sont organisés dans le fichier XAML de la page, ce qui convient mieux quand le téléphone est tenu en mode paysage :

Triple capture d’écran de la page Silent Keyboard

Si vous balayez avec votre doigt sur les touches, vous voyez grâce à de légères modifications de couleur que les événements tactiles sont transférés d’une touche à une autre.

Résumé

Cet article a montré comment appeler des événements dans un effet, et comment écrire et utiliser un effet qui implémente un traitement de l’interaction tactile multipoint de bas niveau.