Partager via


Manipulations tactiles

Utiliser des transformations de matrice pour implémenter le glissement tactile, le pincement et la rotation

Dans des environnements tactiles multiples tels que ceux des appareils mobiles, les utilisateurs utilisent souvent leurs doigts pour manipuler des objets sur l’écran. Les mouvements courants tels qu’un glisser-doigt et un pincement à deux doigts peuvent déplacer et mettre à l’échelle des objets, ou même les faire pivoter. Ces mouvements sont généralement implémentés à l’aide de matrices de transformation, et cet article vous montre comment procéder.

Bitmap soumise à la traduction, à la mise à l’échelle et à la rotation

Tous les exemples présentés ici utilisent l’effet Xamarin.Forms de suivi tactile présenté dans l’article Appel d’événements à partir d’effets.

Glisser-déplacer et traduire

L’une des applications les plus importantes des transformations de matrice est le traitement tactile. Une valeur unique SKMatrix peut consolider une série d’opérations tactiles.

Pour faire glisser un seul doigt, la SKMatrix valeur effectue une traduction. Ceci est illustré dans la page Glisser-déplacer bitmap. Le fichier XAML instancie une instanciation SKCanvasView dans un Xamarin.FormsGridfichier . Un TouchEffect objet a été ajouté à la Effects collection de ce Gridqui suit :

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             xmlns:tt="clr-namespace:TouchTracking"
             x:Class="SkiaSharpFormsDemos.Transforms.BitmapDraggingPage"
             Title="Bitmap Dragging">
    
    <Grid BackgroundColor="White">
        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface" />
        <Grid.Effects>
            <tt:TouchEffect Capture="True"
                            TouchAction="OnTouchEffectAction" />
        </Grid.Effects>
    </Grid>
</ContentPage>

En théorie, l’objet TouchEffect peut être ajouté directement à la Effects collection du SKCanvasView, mais cela ne fonctionne pas sur toutes les plateformes. Étant donné que la SKCanvasView taille est identique à celle Grid de cette configuration, l’attachez également au Grid fonctionnement.

Le fichier code-behind se charge dans une ressource bitmap dans son constructeur et l’affiche dans le PaintSurface gestionnaire :

public partial class BitmapDraggingPage : ContentPage
{
    // Bitmap and matrix for display
    SKBitmap bitmap;
    SKMatrix matrix = SKMatrix.MakeIdentity();
    ···

    public BitmapDraggingPage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            bitmap = SKBitmap.Decode(stream);
        }
    }
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Display the bitmap
        canvas.SetMatrix(matrix);
        canvas.DrawBitmap(bitmap, new SKPoint());
    }
}

Sans code supplémentaire, la SKMatrix valeur est toujours la matrice d’identification, et elle n’aurait aucun effet sur l’affichage de la bitmap. L’objectif du gestionnaire défini dans le fichier XAML est de modifier la valeur de OnTouchEffectAction matrice pour refléter les manipulations tactiles.

Le OnTouchEffectAction gestionnaire commence par convertir la Xamarin.FormsPoint valeur en valeur SkiaSharp SKPoint . Il s’agit d’une question simple de mise à l’échelle basée sur les propriétés et les propriétés de (qui sont des unités indépendantes de SKCanvasView l’appareil Width Height) et de la CanvasSize propriété, qui est en unités de pixels :

public partial class BitmapDraggingPage : ContentPage
{
    ···
    // Touch information
    long touchId = -1;
    SKPoint previousPoint;
    ···
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point = 
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                // Find transformed bitmap rectangle
                SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
                rect = matrix.MapRect(rect);

                // Determine if the touch was within that rectangle
                if (rect.Contains(point))
                {
                    touchId = args.Id;
                    previousPoint = point;
                }
                break;

            case TouchActionType.Moved:
                if (touchId == args.Id)
                {
                    // Adjust the matrix for the new position
                    matrix.TransX += point.X - previousPoint.X;
                    matrix.TransY += point.Y - previousPoint.Y;
                    previousPoint = point;
                    canvasView.InvalidateSurface();
                }
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                touchId = -1;
                break;
        }
    }
    ···
}

Lorsqu’un doigt touche d’abord l’écran, un événement de type TouchActionType.Pressed est déclenché. La première tâche consiste à déterminer si le doigt touche la bitmap. Une telle tâche est souvent appelée test de positionnement. Dans ce cas, les tests de positionnement peuvent être effectués en créant une SKRect valeur correspondant à la bitmap, en appliquant la transformation de matrice à MapRectcelle-ci, puis en déterminant si le point tactile se trouve à l’intérieur du rectangle transformé.

Si c’est le cas, le touchId champ est défini sur l’ID tactile et la position du doigt est enregistrée.

Pour l’événement TouchActionType.Moved , les facteurs de traduction de la SKMatrix valeur sont ajustés en fonction de la position actuelle du doigt et de la nouvelle position du doigt. Cette nouvelle position est enregistrée pour la prochaine fois, et elle SKCanvasView est invalidée.

Lorsque vous expérimentez avec ce programme, notez que vous ne pouvez faire glisser la bitmap que lorsque votre doigt touche une zone où la bitmap est affichée. Bien que cette restriction ne soit pas très importante pour ce programme, elle devient cruciale lors de la manipulation de plusieurs bitmaps.

Pincement et mise à l’échelle

Que voulez-vous faire quand deux doigts touchent la bitmap ? Si les deux doigts se déplacent en parallèle, vous voulez probablement que la bitmap se déplace avec les doigts. Si les deux doigts effectuent une opération de pincement ou d’étirement, vous souhaiterez peut-être que la bitmap soit pivotée (à examiner dans la section suivante) ou mise à l’échelle. Lors de la mise à l’échelle d’une bitmap, il est judicieux que les deux doigts restent dans les mêmes positions par rapport à la bitmap, et que la bitmap soit mise à l’échelle en conséquence.

La gestion de deux doigts à la fois semble compliquée, mais gardez à l’esprit que le TouchAction gestionnaire reçoit uniquement des informations sur un doigt à la fois. Si deux doigts manipulent la bitmap, alors pour chaque événement, un doigt a changé de position, mais l’autre n’a pas changé. Dans le code de la page Mise à l’échelle bitmap ci-dessous, le doigt qui n’a pas changé de position est appelé point croisé dynamique, car la transformation est relative à ce point.

Une différence entre ce programme et le programme précédent est que plusieurs ID tactiles doivent être enregistrés. Un dictionnaire est utilisé à cet effet, où l’ID tactile est la clé de dictionnaire et la valeur du dictionnaire est la position actuelle de ce doigt :

public partial class BitmapScalingPage : ContentPage
{
    ···
    // Touch information
    Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
    ···
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                // Find transformed bitmap rectangle
                SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
                rect = matrix.MapRect(rect);

                // Determine if the touch was within that rectangle
                if (rect.Contains(point) && !touchDictionary.ContainsKey(args.Id))
                {
                    touchDictionary.Add(args.Id, point);
                }
                break;

            case TouchActionType.Moved:
                if (touchDictionary.ContainsKey(args.Id))
                {
                    // Single-finger drag
                    if (touchDictionary.Count == 1)
                    {
                        SKPoint prevPoint = touchDictionary[args.Id];

                        // Adjust the matrix for the new position
                        matrix.TransX += point.X - prevPoint.X;
                        matrix.TransY += point.Y - prevPoint.Y;
                        canvasView.InvalidateSurface();
                    }
                    // Double-finger scale and drag
                    else if (touchDictionary.Count >= 2)
                    {
                        // Copy two dictionary keys into array
                        long[] keys = new long[touchDictionary.Count];
                        touchDictionary.Keys.CopyTo(keys, 0);

                        // Find index of non-moving (pivot) finger
                        int pivotIndex = (keys[0] == args.Id) ? 1 : 0;

                        // Get the three points involved in the transform
                        SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
                        SKPoint prevPoint = touchDictionary[args.Id];
                        SKPoint newPoint = point;

                        // Calculate two vectors
                        SKPoint oldVector = prevPoint - pivotPoint;
                        SKPoint newVector = newPoint - pivotPoint;

                        // Scaling factors are ratios of those
                        float scaleX = newVector.X / oldVector.X;
                        float scaleY = newVector.Y / oldVector.Y;

                        if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
                            !float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
                        {
                            // If something bad hasn't happened, calculate a scale and translation matrix
                            SKMatrix scaleMatrix = 
                                SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);

                            SKMatrix.PostConcat(ref matrix, scaleMatrix);
                            canvasView.InvalidateSurface();
                        }
                    }

                    // Store the new point in the dictionary
                    touchDictionary[args.Id] = point;
                }

                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                if (touchDictionary.ContainsKey(args.Id))
                {
                    touchDictionary.Remove(args.Id);
                }
                break;
        }
    }
    ···
}

La gestion de l’action Pressed est presque la même que le programme précédent, sauf que l’ID et le point tactile sont ajoutés au dictionnaire. Les Released actions et Cancelled suppriment l’entrée de dictionnaire.

Toutefois, la gestion de l’action Moved est plus complexe. S’il n’y a qu’un seul doigt impliqué, le traitement est très identique au programme précédent. Pour deux doigts ou plus, le programme doit également obtenir des informations du dictionnaire impliquant le doigt qui ne se déplace pas. Pour ce faire, copiez les clés de dictionnaire dans un tableau, puis comparez la première clé à l’ID du doigt déplacé. Cela permet au programme d’obtenir le point pivot correspondant au doigt qui ne se déplace pas.

Ensuite, le programme calcule deux vecteurs de la nouvelle position du doigt par rapport au point de pivot et la position de l’ancien doigt par rapport au point de pivot. Les ratios de ces vecteurs sont des facteurs de mise à l’échelle. Étant donné que la division par zéro est une possibilité, celles-ci doivent être case activée ed pour les valeurs infinies ou les valeurs NaN (pas un nombre). Si tout est bien, une transformation de mise à l’échelle est concaténée avec la SKMatrix valeur enregistrée en tant que champ.

Lorsque vous expérimentez cette page, vous remarquerez que vous pouvez faire glisser la bitmap avec un ou deux doigts, ou la mettre à l’échelle avec deux doigts. La mise à l’échelle est anisotropique, ce qui signifie que la mise à l’échelle peut être différente dans les directions horizontales et verticales. Cela déforme les proportions, mais vous permet également de retourner la bitmap pour créer une image miroir. Vous pouvez également découvrir que vous pouvez réduire la bitmap à une dimension zéro et qu’elle disparaît. Dans le code de production, vous souhaiterez vous protéger contre cela.

Rotation à deux doigts

La page Rotation bitmap vous permet d’utiliser deux doigts pour la rotation ou la mise à l’échelle isotropique. La bitmap conserve toujours son rapport d’aspect correct. L’utilisation de deux doigts pour la rotation et la mise à l’échelle anisotropique ne fonctionne pas très bien, car le mouvement des doigts est très similaire pour les deux tâches.

La première grande différence dans ce programme est la logique de test de positionnement. Les programmes précédents ont utilisé la Contains méthode permettant de SKRect déterminer si le point tactile se trouve dans le rectangle transformé qui correspond à la bitmap. Toutefois, lorsque l’utilisateur manipule la bitmap, la bitmap peut être pivotée et SKRect ne peut pas représenter correctement un rectangle pivoté. Vous pouvez craindre que la logique de test de positionnement doit implémenter une géométrie analytique plutôt complexe dans ce cas.

Toutefois, un raccourci est disponible : déterminer si un point se trouve dans les limites d’un rectangle transformé est identique à déterminer si un point transformé inverse se trouve dans les limites du rectangle non transformé. Il s’agit d’un calcul beaucoup plus facile et la logique peut continuer à utiliser la méthode pratique Contains :

public partial class BitmapRotationPage : ContentPage
{
    ···
    // Touch information
    Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
    ···
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                if (!touchDictionary.ContainsKey(args.Id))
                {
                    // Invert the matrix
                    if (matrix.TryInvert(out SKMatrix inverseMatrix))
                    {
                        // Transform the point using the inverted matrix
                        SKPoint transformedPoint = inverseMatrix.MapPoint(point);

                        // Check if it's in the untransformed bitmap rectangle
                        SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);

                        if (rect.Contains(transformedPoint))
                        {
                            touchDictionary.Add(args.Id, point);
                        }
                    }
                }
                break;

            case TouchActionType.Moved:
                if (touchDictionary.ContainsKey(args.Id))
                {
                    // Single-finger drag
                    if (touchDictionary.Count == 1)
                    {
                        SKPoint prevPoint = touchDictionary[args.Id];

                        // Adjust the matrix for the new position
                        matrix.TransX += point.X - prevPoint.X;
                        matrix.TransY += point.Y - prevPoint.Y;
                        canvasView.InvalidateSurface();
                    }
                    // Double-finger rotate, scale, and drag
                    else if (touchDictionary.Count >= 2)
                    {
                        // Copy two dictionary keys into array
                        long[] keys = new long[touchDictionary.Count];
                        touchDictionary.Keys.CopyTo(keys, 0);

                        // Find index non-moving (pivot) finger
                        int pivotIndex = (keys[0] == args.Id) ? 1 : 0;

                        // Get the three points in the transform
                        SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
                        SKPoint prevPoint = touchDictionary[args.Id];
                        SKPoint newPoint = point;

                        // Calculate two vectors
                        SKPoint oldVector = prevPoint - pivotPoint;
                        SKPoint newVector = newPoint - pivotPoint;

                        // Find angles from pivot point to touch points
                        float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
                        float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);

                        // Calculate rotation matrix
                        float angle = newAngle - oldAngle;
                        SKMatrix touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);

                        // Effectively rotate the old vector
                        float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
                        oldVector.X = magnitudeRatio * newVector.X;
                        oldVector.Y = magnitudeRatio * newVector.Y;

                        // Isotropic scaling!
                        float scale = Magnitude(newVector) / Magnitude(oldVector);

                        if (!float.IsNaN(scale) && !float.IsInfinity(scale))
                        {
                            SKMatrix.PostConcat(ref touchMatrix,
                                SKMatrix.MakeScale(scale, scale, pivotPoint.X, pivotPoint.Y));

                            SKMatrix.PostConcat(ref matrix, touchMatrix);
                            canvasView.InvalidateSurface();
                        }
                    }

                    // Store the new point in the dictionary
                    touchDictionary[args.Id] = point;
                }

                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                if (touchDictionary.ContainsKey(args.Id))
                {
                    touchDictionary.Remove(args.Id);
                }
                break;
        }
    }

    float Magnitude(SKPoint point)
    {
        return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
    }
    ···
}

La logique de l’événement Moved commence comme le programme précédent. Deux vecteurs nommés oldVector et newVector sont calculés en fonction de l’élément précédent et du point actuel du doigt mobile et du point pivot du doigt démovible. Mais les angles de ces vecteurs sont déterminés, et la différence est l’angle de rotation.

La mise à l’échelle peut également être impliquée, de sorte que l’ancien vecteur est pivoté en fonction de l’angle de rotation. L’ampleur relative des deux vecteurs est maintenant le facteur de mise à l’échelle. Notez que la même scale valeur est utilisée pour la mise à l’échelle horizontale et verticale afin que la mise à l’échelle soit isotropique. Le matrix champ est ajusté à la fois par la matrice de rotation et par une matrice d’échelle.

Si votre application doit implémenter le traitement tactile pour une image bitmap unique (ou un autre objet), vous pouvez adapter le code à partir de ces trois exemples pour votre propre application. Mais si vous devez implémenter le traitement tactile pour plusieurs bitmaps, vous souhaiterez probablement encapsuler ces opérations tactiles dans d’autres classes.

Encapsuler les opérations tactiles

La page Manipulation tactile illustre la manipulation tactile d’une seule bitmap, mais en utilisant plusieurs autres fichiers qui encapsulent une grande partie de la logique illustrée ci-dessus. Le premier de ces fichiers est l’énumération TouchManipulationMode , qui indique les différents types de manipulation tactile implémentés par le code que vous verrez :

enum TouchManipulationMode
{
    None,
    PanOnly,
    IsotropicScale,     // includes panning
    AnisotropicScale,   // includes panning
    ScaleRotate,        // implies isotropic scaling
    ScaleDualRotate     // adds one-finger rotation
}

PanOnly est un glisser-doigt implémenté avec la traduction. Toutes les options suivantes incluent également le panoramique, mais impliquent deux doigts : IsotropicScale il s’agit d’une opération de pincement qui entraîne la mise à l’échelle de l’objet de manière égale dans les directions horizontales et verticales. AnisotropicScale permet une mise à l’échelle inégale.

L’option ScaleRotate est destinée à la mise à l’échelle et à la rotation à deux doigts. La mise à l’échelle est isotrope. Comme mentionné précédemment, l’implémentation de la rotation à deux doigts avec la mise à l’échelle anisotropique est problématique, car les mouvements des doigts sont essentiellement identiques.

L’option ScaleDualRotate ajoute une rotation d’un doigt. Lorsqu’un seul doigt fait glisser l’objet, l’objet déplacé est d’abord pivoté autour de son centre afin que le centre de l’objet s’aligne avec le vecteur de glissement.

Le fichier TouchManipulationPage.xaml inclut un Picker avec les membres de l’énumération TouchManipulationMode :

<?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:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             xmlns:tt="clr-namespace:TouchTracking"
             xmlns:local="clr-namespace:SkiaSharpFormsDemos.Transforms"
             x:Class="SkiaSharpFormsDemos.Transforms.TouchManipulationPage"
             Title="Touch Manipulation">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Picker Title="Touch Mode"
                Grid.Row="0"
                SelectedIndexChanged="OnTouchModePickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type local:TouchManipulationMode}">
                    <x:Static Member="local:TouchManipulationMode.None" />
                    <x:Static Member="local:TouchManipulationMode.PanOnly" />
                    <x:Static Member="local:TouchManipulationMode.IsotropicScale" />
                    <x:Static Member="local:TouchManipulationMode.AnisotropicScale" />
                    <x:Static Member="local:TouchManipulationMode.ScaleRotate" />
                    <x:Static Member="local:TouchManipulationMode.ScaleDualRotate" />
                </x:Array>
            </Picker.ItemsSource>
            <Picker.SelectedIndex>
                4
            </Picker.SelectedIndex>
        </Picker>
        
        <Grid BackgroundColor="White"
              Grid.Row="1">
            
            <skia:SKCanvasView x:Name="canvasView"
                               PaintSurface="OnCanvasViewPaintSurface" />
            <Grid.Effects>
                <tt:TouchEffect Capture="True"
                                TouchAction="OnTouchEffectAction" />
            </Grid.Effects>
        </Grid>
    </Grid>
</ContentPage>

Vers le bas est un SKCanvasView et un TouchEffect attaché à la cellule Grid unique qui l’entoure.

Le fichier code-behind TouchManipulationPage.xaml.cs a un bitmap champ, mais il n’est pas de type SKBitmap. Le type est TouchManipulationBitmap (une classe que vous verrez prochainement) :

public partial class TouchManipulationPage : ContentPage
{
    TouchManipulationBitmap bitmap;
    ...

    public TouchManipulationPage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.MountainClimbers.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            SKBitmap bitmap = SKBitmap.Decode(stream);
            this.bitmap = new TouchManipulationBitmap(bitmap);
            this.bitmap.TouchManager.Mode = TouchManipulationMode.ScaleRotate;
        }
    }
    ...
}

Le constructeur instancie un TouchManipulationBitmap objet, en passant au constructeur un SKBitmap élément obtenu à partir d’une ressource incorporée. Le constructeur conclut en définissant la Mode propriété de l’objet TouchManager TouchManipulationBitmap sur un membre de l’énumération TouchManipulationMode .

Le SelectedIndexChanged gestionnaire pour la Picker propriété définit également cette Mode propriété :

public partial class TouchManipulationPage : ContentPage
{
    ...
    void OnTouchModePickerSelectedIndexChanged(object sender, EventArgs args)
    {
        if (bitmap != null)
        {
            Picker picker = (Picker)sender;
            bitmap.TouchManager.Mode = (TouchManipulationMode)picker.SelectedItem;
        }
    }
    ...
}

Le TouchAction gestionnaire de l’instancié TouchEffect dans le fichier XAML appelle deux méthodes nommées HitTest TouchManipulationBitmap et ProcessTouchEvent:

public partial class TouchManipulationPage : ContentPage
{
    ...
    List<long> touchIds = new List<long>();
    ...
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                if (bitmap.HitTest(point))
                {
                    touchIds.Add(args.Id);
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    break;
                }
                break;

            case TouchActionType.Moved:
                if (touchIds.Contains(args.Id))
                {
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    canvasView.InvalidateSurface();
                }
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                if (touchIds.Contains(args.Id))
                {
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    touchIds.Remove(args.Id);
                    canvasView.InvalidateSurface();
                }
                break;
        }
    }
    ...
}

Si la HitTest méthode retourne true ( ce qui signifie qu’un doigt a touché l’écran dans la zone occupée par la bitmap), l’ID tactile est ajouté à la TouchIds collection. Cet ID représente la séquence d’événements tactiles de ce doigt jusqu’à ce que le doigt s’élève à partir de l’écran. Si plusieurs doigts touchent la bitmap, la touchIds collection contient un ID tactile pour chaque doigt.

Le TouchAction gestionnaire appelle également la ProcessTouchEvent classe dans TouchManipulationBitmap. C’est là que certains (mais pas tous) du traitement tactile réel se produisent.

La TouchManipulationBitmap classe est une classe wrapper pour SKBitmap laquelle contient du code pour afficher l’image bitmap et traiter les événements tactiles. Il fonctionne conjointement avec du code plus généralisé dans une TouchManipulationManager classe (que vous verrez sous peu).

Le TouchManipulationBitmap constructeur enregistre et SKBitmap instancie deux propriétés, la TouchManager propriété de type TouchManipulationManager et la Matrix propriété de type SKMatrix:

class TouchManipulationBitmap
{
    SKBitmap bitmap;
    ...

    public TouchManipulationBitmap(SKBitmap bitmap)
    {
        this.bitmap = bitmap;
        Matrix = SKMatrix.MakeIdentity();

        TouchManager = new TouchManipulationManager
        {
            Mode = TouchManipulationMode.ScaleRotate
        };
    }

    public TouchManipulationManager TouchManager { set; get; }

    public SKMatrix Matrix { set; get; }
    ...
}

Cette Matrix propriété est la transformation cumulée résultant de toute l’activité tactile. Comme vous le verrez, chaque événement tactile est résolu dans une matrice, qui est ensuite concaténée avec la SKMatrix valeur stockée par la Matrix propriété.

L’objet TouchManipulationBitmap se dessine dans sa Paint méthode. L’argument est un SKCanvas objet. Cela SKCanvas peut déjà avoir une transformation appliquée, de sorte que la Paint méthode concatène la Matrix propriété associée à la bitmap à la transformation existante et restaure le canevas lorsqu’elle a terminé :

class TouchManipulationBitmap
{
    ...
    public void Paint(SKCanvas canvas)
    {
        canvas.Save();
        SKMatrix matrix = Matrix;
        canvas.Concat(ref matrix);
        canvas.DrawBitmap(bitmap, 0, 0);
        canvas.Restore();
    }
    ...
}

La HitTest méthode retourne true si l’utilisateur touche l’écran à un point dans les limites de la bitmap. Cela utilise la logique illustrée précédemment dans la page Rotation bitmap :

class TouchManipulationBitmap
{
    ...
    public bool HitTest(SKPoint location)
    {
        // Invert the matrix
        SKMatrix inverseMatrix;

        if (Matrix.TryInvert(out inverseMatrix))
        {
            // Transform the point using the inverted matrix
            SKPoint transformedPoint = inverseMatrix.MapPoint(location);

            // Check if it's in the untransformed bitmap rectangle
            SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
            return rect.Contains(transformedPoint);
        }
        return false;
    }
    ...
}

La deuxième méthode publique en TouchManipulationBitmap est ProcessTouchEvent. Lorsque cette méthode est appelée, elle a déjà été établie que l’événement tactile appartient à cette bitmap particulière. La méthode conserve un dictionnaire d’objets TouchManipulationInfo , qui est simplement le point précédent et le nouveau point de chaque doigt :

class TouchManipulationInfo
{
    public SKPoint PreviousPoint { set; get; }

    public SKPoint NewPoint { set; get; }
}

Voici le dictionnaire et la ProcessTouchEvent méthode elle-même :

class TouchManipulationBitmap
{
    ...
    Dictionary<long, TouchManipulationInfo> touchDictionary =
        new Dictionary<long, TouchManipulationInfo>();
    ...
    public void ProcessTouchEvent(long id, TouchActionType type, SKPoint location)
    {
        switch (type)
        {
            case TouchActionType.Pressed:
                touchDictionary.Add(id, new TouchManipulationInfo
                {
                    PreviousPoint = location,
                    NewPoint = location
                });
                break;

            case TouchActionType.Moved:
                TouchManipulationInfo info = touchDictionary[id];
                info.NewPoint = location;
                Manipulate();
                info.PreviousPoint = info.NewPoint;
                break;

            case TouchActionType.Released:
                touchDictionary[id].NewPoint = location;
                Manipulate();
                touchDictionary.Remove(id);
                break;

            case TouchActionType.Cancelled:
                touchDictionary.Remove(id);
                break;
        }
    }
    ...
}

Dans les événements et Released les Moved événements, la méthode appelle Manipulate. À ce stade, il touchDictionary contient un ou plusieurs TouchManipulationInfo objets. Si le touchDictionary contient un élément, il est probable que les valeurs et NewPoint les PreviousPoint valeurs sont inégales et représentent le mouvement d’un doigt. Si plusieurs doigts touchent la bitmap, le dictionnaire contient plusieurs éléments, mais un seul de ces éléments a des valeurs différentesPreviousPoint.NewPoint Tous les autres ont des valeurs égales PreviousPoint et NewPoint égales.

Ceci est important : la Manipulate méthode peut supposer qu’elle traite le mouvement d’un seul doigt. Au moment de cet appel, aucun des autres doigts ne bouge, et s’ils bougent vraiment (comme c’est probable), ces mouvements seront traités dans les appels futurs à Manipulate.

La Manipulate méthode copie d’abord le dictionnaire dans un tableau pour des raisons pratiques. Il ignore tout autre chose que les deux premières entrées. Si plus de deux doigts tentent de manipuler la bitmap, les autres sont ignorés. Manipulate est le membre final de TouchManipulationBitmap:

class TouchManipulationBitmap
{
    ...
    void Manipulate()
    {
        TouchManipulationInfo[] infos = new TouchManipulationInfo[touchDictionary.Count];
        touchDictionary.Values.CopyTo(infos, 0);
        SKMatrix touchMatrix = SKMatrix.MakeIdentity();

        if (infos.Length == 1)
        {
            SKPoint prevPoint = infos[0].PreviousPoint;
            SKPoint newPoint = infos[0].NewPoint;
            SKPoint pivotPoint = Matrix.MapPoint(bitmap.Width / 2, bitmap.Height / 2);

            touchMatrix = TouchManager.OneFingerManipulate(prevPoint, newPoint, pivotPoint);
        }
        else if (infos.Length >= 2)
        {
            int pivotIndex = infos[0].NewPoint == infos[0].PreviousPoint ? 0 : 1;
            SKPoint pivotPoint = infos[pivotIndex].NewPoint;
            SKPoint newPoint = infos[1 - pivotIndex].NewPoint;
            SKPoint prevPoint = infos[1 - pivotIndex].PreviousPoint;

            touchMatrix = TouchManager.TwoFingerManipulate(prevPoint, newPoint, pivotPoint);
        }

        SKMatrix matrix = Matrix;
        SKMatrix.PostConcat(ref matrix, touchMatrix);
        Matrix = matrix;
    }
}

Si un doigt manipule la bitmap, Manipulate appelle la OneFingerManipulate méthode de l’objet TouchManipulationManager . Pour deux doigts, il appelle TwoFingerManipulate. Les arguments de ces méthodes sont identiques : les prevPoint arguments newPoint représentent le doigt qui se déplace. Mais l’argument pivotPoint est différent pour les deux appels :

Pour la manipulation d’un doigt, il pivotPoint s’agit du centre de la bitmap. Cela permet une rotation d’un doigt. Pour la manipulation à deux doigts, l’événement indique le mouvement d’un seul doigt, de sorte que le pivotPoint doigt qui ne se déplace pas.

Dans les deux cas, TouchManipulationManager retourne une SKMatrix valeur, que la méthode concatène avec la propriété actuelle Matrix qui TouchManipulationPage utilise pour afficher la bitmap.

TouchManipulationManager est généralisé et n’utilise aucun autre fichier, sauf TouchManipulationMode. Vous pouvez peut-être utiliser cette classe sans modification dans vos propres applications. Elle définit une seule propriété de type TouchManipulationMode :

class TouchManipulationManager
{
    public TouchManipulationMode Mode { set; get; }
    ...
}

Toutefois, vous voudrez probablement éviter l’option AnisotropicScale . Il est très facile avec cette option de manipuler la bitmap afin que l’un des facteurs de mise à l’échelle devienne zéro. Cela rend l’image bitmap disparaître de la vue, ne jamais retourner. Si vous avez vraiment besoin d’une mise à l’échelle anisotropique, vous devez améliorer la logique pour éviter les résultats indésirables.

TouchManipulationManager utilise des vecteurs, mais étant donné qu’il n’existe aucune SKVector structure dans SkiaSharp, SKPoint il est utilisé à la place. SKPoint prend en charge l’opérateur de soustraction, et le résultat peut être traité comme un vecteur. La seule logique spécifique au vecteur à ajouter est un Magnitude calcul :

class TouchManipulationManager
{
    ...
    float Magnitude(SKPoint point)
    {
        return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
    }
}

Chaque fois que la rotation a été sélectionnée, les méthodes de manipulation d’un doigt et de deux doigts gèrent d’abord la rotation. Si une rotation est détectée, le composant de rotation est effectivement supprimé. Ce qui reste est interprété comme un panoramique et une mise à l’échelle.

Voici la OneFingerManipulate méthode. Si la rotation d’un doigt n’a pas été activée, la logique est simple : elle utilise simplement le point précédent et le nouveau point pour construire un vecteur nommé delta qui correspond précisément à la traduction. Avec la rotation d’un doigt activée, la méthode utilise des angles du point pivot (le centre de la bitmap) au point précédent et un nouveau point pour construire une matrice de rotation :

class TouchManipulationManager
{
    public TouchManipulationMode Mode { set; get; }

    public SKMatrix OneFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
    {
        if (Mode == TouchManipulationMode.None)
        {
            return SKMatrix.MakeIdentity();
        }

        SKMatrix touchMatrix = SKMatrix.MakeIdentity();
        SKPoint delta = newPoint - prevPoint;

        if (Mode == TouchManipulationMode.ScaleDualRotate)  // One-finger rotation
        {
            SKPoint oldVector = prevPoint - pivotPoint;
            SKPoint newVector = newPoint - pivotPoint;

            // Avoid rotation if fingers are too close to center
            if (Magnitude(newVector) > 25 && Magnitude(oldVector) > 25)
            {
                float prevAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
                float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);

                // Calculate rotation matrix
                float angle = newAngle - prevAngle;
                touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);

                // Effectively rotate the old vector
                float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
                oldVector.X = magnitudeRatio * newVector.X;
                oldVector.Y = magnitudeRatio * newVector.Y;

                // Recalculate delta
                delta = newVector - oldVector;
            }
        }

        // Multiply the rotation matrix by a translation matrix
        SKMatrix.PostConcat(ref touchMatrix, SKMatrix.MakeTranslation(delta.X, delta.Y));

        return touchMatrix;
    }
    ...
}

Dans la TwoFingerManipulate méthode, le point croisé dynamique est la position du doigt qui ne se déplace pas dans cet événement tactile particulier. La rotation est très similaire à la rotation d’un doigt, puis le vecteur nommé oldVector (basé sur le point précédent) est ajusté pour la rotation. Le mouvement restant est interprété comme une mise à l’échelle :

class TouchManipulationManager
{
    ...
    public SKMatrix TwoFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
    {
        SKMatrix touchMatrix = SKMatrix.MakeIdentity();
        SKPoint oldVector = prevPoint - pivotPoint;
        SKPoint newVector = newPoint - pivotPoint;

        if (Mode == TouchManipulationMode.ScaleRotate ||
            Mode == TouchManipulationMode.ScaleDualRotate)
        {
            // Find angles from pivot point to touch points
            float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
            float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);

            // Calculate rotation matrix
            float angle = newAngle - oldAngle;
            touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);

            // Effectively rotate the old vector
            float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
            oldVector.X = magnitudeRatio * newVector.X;
            oldVector.Y = magnitudeRatio * newVector.Y;
        }

        float scaleX = 1;
        float scaleY = 1;

        if (Mode == TouchManipulationMode.AnisotropicScale)
        {
            scaleX = newVector.X / oldVector.X;
            scaleY = newVector.Y / oldVector.Y;

        }
        else if (Mode == TouchManipulationMode.IsotropicScale ||
                 Mode == TouchManipulationMode.ScaleRotate ||
                 Mode == TouchManipulationMode.ScaleDualRotate)
        {
            scaleX = scaleY = Magnitude(newVector) / Magnitude(oldVector);
        }

        if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
            !float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
        {
            SKMatrix.PostConcat(ref touchMatrix,
                SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y));
        }

        return touchMatrix;
    }
    ...
}

Vous remarquerez qu’il n’existe aucune traduction explicite dans cette méthode. Toutefois, les méthodes et MakeScale les MakeRotation méthodes sont basées sur le point croisé dynamique et incluent une traduction implicite. Si vous utilisez deux doigts sur la bitmap et que vous les faites glisser dans la même direction, TouchManipulation vous obtiendrez une série d’événements tactiles alternés entre les deux doigts. À mesure que chaque doigt se déplace par rapport à l’autre, la mise à l’échelle ou les résultats de rotation, mais il est négation par le mouvement de l’autre doigt, et le résultat est la traduction.

La seule partie restante de la page Manipulation tactile est le PaintSurface gestionnaire dans le TouchManipulationPage fichier code-behind. Cela appelle la méthode du TouchManipulationBitmap, qui applique la Paint matrice représentant l’activité tactile cumulée :

public partial class TouchManipulationPage : ContentPage
{
    ...
    MatrixDisplay matrixDisplay = new MatrixDisplay();
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Display the bitmap
        bitmap.Paint(canvas);

        // Display the matrix in the lower-right corner
        SKSize matrixSize = matrixDisplay.Measure(bitmap.Matrix);

        matrixDisplay.Paint(canvas, bitmap.Matrix,
            new SKPoint(info.Width - matrixSize.Width,
                        info.Height - matrixSize.Height));
    }
}

Le PaintSurface gestionnaire se termine par l’affichage d’un MatrixDisplay objet montrant la matrice tactile cumulée :

Capture d’écran triple de la page Manipulation tactile

Manipulation de plusieurs bitmaps

L’un des avantages de l’isolation du code de traitement tactile dans des classes telles que TouchManipulationBitmap la TouchManipulationManager possibilité de réutiliser ces classes dans un programme qui permet à l’utilisateur de manipuler plusieurs bitmaps.

La page Mode Nuages de points bitmap montre comment cela est fait. Au lieu de définir un champ de type TouchManipulationBitmap, la BitmapScatterPage classe définit un List objet bitmap :

public partial class BitmapScatterViewPage : ContentPage
{
    List<TouchManipulationBitmap> bitmapCollection =
        new List<TouchManipulationBitmap>();
    ...
    public BitmapScatterViewPage()
    {
        InitializeComponent();

        // Load in all the available bitmaps
        Assembly assembly = GetType().GetTypeInfo().Assembly;
        string[] resourceIDs = assembly.GetManifestResourceNames();
        SKPoint position = new SKPoint();

        foreach (string resourceID in resourceIDs)
        {
            if (resourceID.EndsWith(".png") ||
                resourceID.EndsWith(".jpg"))
            {
                using (Stream stream = assembly.GetManifestResourceStream(resourceID))
                {
                    SKBitmap bitmap = SKBitmap.Decode(stream);
                    bitmapCollection.Add(new TouchManipulationBitmap(bitmap)
                    {
                        Matrix = SKMatrix.MakeTranslation(position.X, position.Y),
                    });
                    position.X += 100;
                    position.Y += 100;
                }
            }
        }
    }
    ...
}

Le constructeur charge dans toutes les bitmaps disponibles en tant que ressources incorporées et les ajoute au bitmapCollection. Notez que la Matrix propriété est initialisée sur chaque TouchManipulationBitmap objet, de sorte que les angles supérieur gauche de chaque bitmap sont décalés de 100 pixels.

La BitmapScatterView page doit également gérer les événements tactiles pour plusieurs bitmaps. Au lieu de définir des List ID tactiles d’objets actuellement manipulés TouchManipulationBitmap , ce programme nécessite un dictionnaire :

public partial class BitmapScatterViewPage : ContentPage
{
    ...
    Dictionary<long, TouchManipulationBitmap> bitmapDictionary =
       new Dictionary<long, TouchManipulationBitmap>();
    ...
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                for (int i = bitmapCollection.Count - 1; i >= 0; i--)
                {
                    TouchManipulationBitmap bitmap = bitmapCollection[i];

                    if (bitmap.HitTest(point))
                    {
                        // Move bitmap to end of collection
                        bitmapCollection.Remove(bitmap);
                        bitmapCollection.Add(bitmap);

                        // Do the touch processing
                        bitmapDictionary.Add(args.Id, bitmap);
                        bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                        canvasView.InvalidateSurface();
                        break;
                    }
                }
                break;

            case TouchActionType.Moved:
                if (bitmapDictionary.ContainsKey(args.Id))
                {
                    TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    canvasView.InvalidateSurface();
                }
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                if (bitmapDictionary.ContainsKey(args.Id))
                {
                    TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    bitmapDictionary.Remove(args.Id);
                    canvasView.InvalidateSurface();
                }
                break;
        }
    }
    ...
}

Notez comment la Pressed logique effectue une boucle dans l’inverse bitmapCollection . Les bitmaps se chevauchent souvent. Les bitmaps plus loin dans la collection se trouvent visuellement sur les bitmaps antérieures de la collection. S’il existe plusieurs bitmaps sous le doigt qui appuient sur l’écran, le plus haut doit être celui qui est manipulé par ce doigt.

Notez également que la Pressed logique déplace cette bitmap à la fin de la collection afin qu’elle se déplace visuellement en haut de la pile d’autres bitmaps.

Dans les événements et Released les Moved événements, le TouchAction gestionnaire appelle la ProcessingTouchEvent méthode comme TouchManipulationBitmap le programme précédent.

Enfin, le PaintSurface gestionnaire appelle la Paint méthode de chaque TouchManipulationBitmap objet :

public partial class BitmapScatterViewPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKCanvas canvas = args.Surface.Canvas;
        canvas.Clear();

        foreach (TouchManipulationBitmap bitmap in bitmapCollection)
        {
            bitmap.Paint(canvas);
        }
    }
}

Le code effectue une boucle dans la collection et affiche la pile des bitmaps du début de la collection à la fin :

Capture d’écran triple de la page De nuages de points bitmap

Mise à l’échelle à un seul doigt

Une opération de mise à l’échelle nécessite généralement un geste de pincement à l’aide de deux doigts. Toutefois, il est possible d’implémenter la mise à l’échelle avec un seul doigt en déplaçant les angles d’une bitmap.

Ceci est illustré dans la page Mise à l’échelle d’un coin de doigt unique. Étant donné que cet exemple utilise un type de mise à l’échelle quelque peu différent de celui implémenté dans la TouchManipulationManager classe, il n’utilise pas cette classe ou la TouchManipulationBitmap classe. Au lieu de cela, toute la logique tactile se trouve dans le fichier code-behind. Il s’agit d’une logique un peu plus simple que d’habitude, car elle effectue le suivi d’un seul doigt à la fois et ignore simplement les doigts secondaires susceptibles de toucher l’écran.

La page SingleFingerCornerScale.xaml instancie la SKCanvasView classe et crée un TouchEffect objet pour le suivi des événements tactiles :

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             xmlns:tt="clr-namespace:TouchTracking"
             x:Class="SkiaSharpFormsDemos.Transforms.SingleFingerCornerScalePage"
             Title="Single Finger Corner Scale">

    <Grid BackgroundColor="White"
          Grid.Row="1">

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

Le fichier SingleFingerCornerScalePage.xaml.cs charge une ressource bitmap à partir du répertoire Media et l’affiche à l’aide d’un SKMatrix objet défini comme champ :

public partial class SingleFingerCornerScalePage : ContentPage
{
    SKBitmap bitmap;
    SKMatrix currentMatrix = SKMatrix.MakeIdentity();
    ···

    public SingleFingerCornerScalePage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            bitmap = SKBitmap.Decode(stream);
        }
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        canvas.SetMatrix(currentMatrix);
        canvas.DrawBitmap(bitmap, 0, 0);
    }
    ···
}

Cet SKMatrix objet est modifié par la logique tactile indiquée ci-dessous.

Le reste du fichier code-behind est le TouchEffect gestionnaire d’événements. Elle commence par convertir l’emplacement actuel du doigt en valeur SKPoint . Pour le Pressed type d’action, le gestionnaire case activée qu’aucun autre doigt ne touche l’écran et que le doigt se trouve dans les limites de la bitmap.

La partie cruciale du code est une if instruction impliquant deux appels à la Math.Pow méthode. Cette mathématique case activée si l’emplacement du doigt se trouve en dehors d’un ellipse qui remplit la bitmap. Dans ce cas, il s’agit d’une opération de mise à l’échelle. Le doigt se trouve à proximité de l’un des angles de la bitmap, et un point de tableau croisé dynamique est déterminé comme étant l’angle opposé. Si le doigt se trouve dans cet ellipse, il s’agit d’une opération de panoramique régulière :

public partial class SingleFingerCornerScalePage : ContentPage
{
    SKBitmap bitmap;
    SKMatrix currentMatrix = SKMatrix.MakeIdentity();

    // Information for translating and scaling
    long? touchId = null;
    SKPoint pressedLocation;
    SKMatrix pressedMatrix;

    // Information for scaling
    bool isScaling;
    SKPoint pivotPoint;
    ···

    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                // Track only one finger
                if (touchId.HasValue)
                    return;

                // Check if the finger is within the boundaries of the bitmap
                SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
                rect = currentMatrix.MapRect(rect);
                if (!rect.Contains(point))
                    return;

                // First assume there will be no scaling
                isScaling = false;

                // If touch is outside interior ellipse, make this a scaling operation
                if (Math.Pow((point.X - rect.MidX) / (rect.Width / 2), 2) +
                    Math.Pow((point.Y - rect.MidY) / (rect.Height / 2), 2) > 1)
                {
                    isScaling = true;
                    float xPivot = point.X < rect.MidX ? rect.Right : rect.Left;
                    float yPivot = point.Y < rect.MidY ? rect.Bottom : rect.Top;
                    pivotPoint = new SKPoint(xPivot, yPivot);
                }

                // Common for either pan or scale
                touchId = args.Id;
                pressedLocation = point;
                pressedMatrix = currentMatrix;
                break;

            case TouchActionType.Moved:
                if (!touchId.HasValue || args.Id != touchId.Value)
                    return;

                SKMatrix matrix = SKMatrix.MakeIdentity();

                // Translating
                if (!isScaling)
                {
                    SKPoint delta = point - pressedLocation;
                    matrix = SKMatrix.MakeTranslation(delta.X, delta.Y);
                }
                // Scaling
                else
                {
                    float scaleX = (point.X - pivotPoint.X) / (pressedLocation.X - pivotPoint.X);
                    float scaleY = (point.Y - pivotPoint.Y) / (pressedLocation.Y - pivotPoint.Y);
                    matrix = SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);
                }

                // Concatenate the matrices
                SKMatrix.PreConcat(ref matrix, pressedMatrix);
                currentMatrix = matrix;
                canvasView.InvalidateSurface();
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                touchId = null;
                break;
        }
    }
}

Le Moved type d’action calcule une matrice correspondant à l’activité tactile à partir du moment où le doigt a appuyé sur l’écran jusqu’à cette fois. Il concatène cette matrice avec la matrice en vigueur au moment où le doigt a d’abord appuyé sur la bitmap. L’opération de mise à l’échelle est toujours relative au coin opposé à celui que le doigt a touché.

Pour les images bitmap petites ou oblonges, un ellipse intérieur peut occuper la plupart de la bitmap et laisser des zones minuscules aux coins pour mettre à l’échelle la bitmap. Vous préférerez peut-être une approche quelque peu différente, auquel cas vous pouvez remplacer cet ensemble if de blocs qui se définit isScaling true par ce code :

float halfHeight = rect.Height / 2;
float halfWidth = rect.Width / 2;

// Top half of bitmap
if (point.Y < rect.MidY)
{
    float yRelative = (point.Y - rect.Top) / halfHeight;

    // Upper-left corner
    if (point.X < rect.MidX - yRelative * halfWidth)
    {
        isScaling = true;
        pivotPoint = new SKPoint(rect.Right, rect.Bottom);
    }
    // Upper-right corner
    else if (point.X > rect.MidX + yRelative * halfWidth)
    {
        isScaling = true;
        pivotPoint = new SKPoint(rect.Left, rect.Bottom);
    }
}
// Bottom half of bitmap
else
{
    float yRelative = (point.Y - rect.MidY) / halfHeight;

    // Lower-left corner
    if (point.X < rect.Left + yRelative * halfWidth)
    {
        isScaling = true;
        pivotPoint = new SKPoint(rect.Right, rect.Top);
    }
    // Lower-right corner
    else if (point.X > rect.Right - yRelative * halfWidth)
    {
        isScaling = true;
        pivotPoint = new SKPoint(rect.Left, rect.Top);
    }
}

Ce code divise efficacement la zone de la bitmap en une forme de diamant intérieur et quatre triangles aux angles. Cela permet à des zones beaucoup plus grandes aux coins de saisir et de mettre à l’échelle la bitmap.