Manipulaciones táctiles

Descargar ejemploDescargar el ejemplo

Usar transformaciones de matriz para implementar el arrastre táctil, el pellizco y la rotación

En entornos multitáctil, como los de dispositivos móviles, los usuarios suelen usar sus dedos para manipular objetos en la pantalla. Los gestos comunes, como un arrastre de un dedo y una pellizcar de dos dedos, pueden moverse y escalar objetos, o incluso girarlos. Estos gestos se implementan generalmente mediante matrices de transformación y en este artículo se muestra cómo hacerlo.

Un mapa de bits sujeto a traducción, escalado y rotación

Todos los ejemplos que se muestran aquí usan el Xamarin.Forms efecto de seguimiento táctil presentado en el artículo Invocación de eventos de efectos.

Arrastrar y traducir

Una de las aplicaciones más importantes de las transformaciones de matriz es el procesamiento táctil. Un único SKMatrix valor puede consolidar una serie de operaciones táctiles.

Para arrastrar un solo dedo, el valor realiza la SKMatrix traducción. Esto se muestra en la página Arrastre de mapa de bits . El archivo XAML crea SKCanvasView una instancia de en un Xamarin.FormsGridobjeto . Se ha agregado un TouchEffect objeto a la Effects colección de que Grid:

<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 teoría, el TouchEffect objeto se podría agregar directamente a la Effects colección de SKCanvasView, pero eso no funciona en todas las plataformas. SKCanvasView Dado que tiene el mismo tamaño que en Grid esta configuración, adjuntarlo también a Grid funciona.

El archivo de código subyacente se carga en un recurso de mapa de bits en su constructor y lo muestra en el PaintSurface controlador:

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());
    }
}

Sin ningún código adicional, el SKMatrix valor siempre es la matriz de identificación y no tendría ningún efecto en la presentación del mapa de bits. El objetivo del controlador establecido en el archivo XAML es modificar el valor de OnTouchEffectAction matriz para reflejar las manipulaciones táctiles.

El OnTouchEffectAction controlador comienza convirtiendo el Xamarin.FormsPoint valor en un valor SkiaSharp SKPoint . Se trata de una cuestión sencilla de escalado en función de las Width propiedades y Height de SKCanvasView (que son unidades independientes del dispositivo) y la CanvasSize propiedad , que se encuentra en unidades de píxeles:

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;
        }
    }
    ···
}

Cuando un dedo toca por primera vez la pantalla, se desencadena un evento de tipo TouchActionType.Pressed . La primera tarea es determinar si el dedo está tocando el mapa de bits. Esta tarea suele denominar pruebas de posicionamiento. En este caso, las pruebas de posicionamiento se pueden realizar creando un SKRect valor correspondiente al mapa de bits, aplicando la transformación de matriz a ella con MapRecty, a continuación, determinar si el punto táctil está dentro del rectángulo transformado.

Si ese es el caso, el touchId campo se establece en el identificador táctil y se guarda la posición del dedo.

Para el TouchActionType.Moved evento, los factores de traducción del SKMatrix valor se ajustan en función de la posición actual del dedo y la nueva posición del dedo. Esa nueva posición se guarda para la próxima vez y SKCanvasView se invalida.

A medida que experimente con este programa, tenga en cuenta que solo puede arrastrar el mapa de bits cuando el dedo toca un área donde se muestra el mapa de bits. Aunque esa restricción no es muy importante para este programa, se convierte en crucial al manipular varios mapas de bits.

Reducir y escalar

¿Qué quieres pasar cuando dos dedos tocan el mapa de bits? Si los dos dedos se mueven en paralelo, es probable que desee que el mapa de bits se mueva junto con los dedos. Si los dos dedos realizan una operación de pellizcar o estirar, es posible que quiera que se gire el mapa de bits (que se describirá en la sección siguiente) o se escale. Al escalar un mapa de bits, tiene más sentido que los dos dedos permanezcan en las mismas posiciones relativas al mapa de bits y que el mapa de bits se escale según corresponda.

El control de dos dedos a la vez parece complicado, pero tenga en cuenta que el TouchAction controlador solo recibe información sobre un dedo a la vez. Si dos dedos manipulan el mapa de bits, para cada evento, un dedo ha cambiado de posición, pero el otro no ha cambiado. En el código de página Escalado de mapa de bits siguiente, el dedo que no ha cambiado de posición se denomina punto de dinamización porque la transformación es relativa a ese punto.

Una diferencia entre este programa y el programa anterior es que se deben guardar varios identificadores táctiles. Se usa un diccionario para este fin, donde el id. táctil es la clave del diccionario y el valor del diccionario es la posición actual de ese dedo:

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;
        }
    }
    ···
}

El control de la Pressed acción es casi el mismo que el programa anterior, excepto que el identificador y el punto táctil se agregan al diccionario. Las Released acciones y Cancelled quitan la entrada del diccionario.

Sin embargo, el control de la Moved acción es más complejo. Si solo hay un dedo implicado, el procesamiento es muy igual que el programa anterior. Para dos o más dedos, el programa también debe obtener información del diccionario que implica el dedo que no se mueve. Para ello, copia las claves del diccionario en una matriz y, a continuación, compara la primera clave con el identificador del dedo que se va a mover. Esto permite al programa obtener el punto de pivote correspondiente al dedo que no se mueve.

A continuación, el programa calcula dos vectores de la nueva posición del dedo en relación con el punto de pivote y la posición del dedo anterior en relación con el punto de pivote. Las relaciones de estos vectores son factores de escalado. Dado que la división por cero es una posibilidad, se deben comprobar si hay valores infinitos o valores NaN (no un número). Si todo está bien, una transformación de escalado se concatena con el SKMatrix valor guardado como campo.

A medida que experimente con esta página, observará que puede arrastrar el mapa de bits con uno o dos dedos, o escalarlo con dos dedos. El escalado es anisotrópico, lo que significa que el escalado puede ser diferente en las direcciones horizontales y verticales. Esto distorsiona la relación de aspecto, pero también permite voltear el mapa de bits para crear una imagen reflejada. También puede detectar que puede reducir el mapa de bits a una dimensión cero y desaparece. En el código de producción, querrá protegerse de esto.

Rotación de dos dedos

La página Rotación de mapa de bits le permite usar dos dedos para la rotación o el escalado isotrópico. El mapa de bits siempre conserva su relación de aspecto correcta. El uso de dos dedos para la rotación y el escalado anisotrópico no funciona muy bien porque el movimiento de los dedos es muy similar para ambas tareas.

La primera gran diferencia en este programa es la lógica de pruebas de posicionamiento. Los programas anteriores usaron el Contains método de SKRect para determinar si el punto táctil está dentro del rectángulo transformado que corresponde al mapa de bits. Pero a medida que el usuario manipula el mapa de bits, el mapa de bits puede girarse y SKRect no puede representar correctamente un rectángulo girado. Es posible que tenga miedo de que la lógica de pruebas de posicionamiento tenga que implementar geometría analítica bastante compleja en ese caso.

Sin embargo, hay disponible un acceso directo: determinar si un punto se encuentra dentro de los límites de un rectángulo transformado es el mismo que determinar si un punto transformado inverso se encuentra dentro de los límites del rectángulo sin transformar. Este es un cálculo mucho más sencillo y la lógica puede seguir usando el método práctico 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 lógica del Moved evento se inicia como el programa anterior. Dos vectores denominados oldVector y newVector se calculan en función del punto anterior y actual del dedo móvil y el punto de pivote del dedo desenmoving. Sin embargo, se determinan los ángulos de estos vectores y la diferencia es el ángulo de rotación.

El escalado también puede estar implicado, por lo que el vector antiguo se gira en función del ángulo de rotación. La magnitud relativa de los dos vectores es ahora el factor de escalado. Observe que el mismo scale valor se usa para el escalado horizontal y vertical para que el escalado sea isotrópico. El matrix campo se ajusta mediante la matriz de rotación y una matriz de escala.

Si la aplicación necesita implementar el procesamiento táctil para un solo mapa de bits (u otro objeto), puede adaptar el código de estos tres ejemplos para su propia aplicación. Pero si necesita implementar el procesamiento táctil para varios mapas de bits, probablemente querrá encapsular estas operaciones táctiles en otras clases.

Encapsular las operaciones táctiles

En la página Manipulación táctil se muestra la manipulación táctil de un solo mapa de bits, pero se usan otros archivos que encapsulan gran parte de la lógica mostrada anteriormente. El primero de estos archivos es la TouchManipulationMode enumeración, que indica los diferentes tipos de manipulación táctil implementadas por el código que verá:

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

PanOnly es un arrastre de un dedo que se implementa con traducción. Todas las opciones posteriores también incluyen movimiento panorámico, pero implican dos dedos: IsotropicScale es una operación de pellizcar que da como resultado el escalado de objetos igualmente en las direcciones horizontales y verticales. AnisotropicScale permite un escalado desigual.

La ScaleRotate opción es para el escalado y la rotación de dos dedos. El escalado es isotrópico. Como se mencionó anteriormente, la implementación de la rotación de dos dedos con el escalado anisotrópico es problemática porque los movimientos del dedo son esencialmente los mismos.

La ScaleDualRotate opción agrega rotación de un dedo. Cuando un solo dedo arrastra el objeto, el objeto arrastrado se gira primero alrededor de su centro para que el centro del objeto se alinea con el vector de arrastre.

El archivo TouchManipulationPage.xaml incluye un Picker elemento con los miembros de la TouchManipulationMode enumeración:

<?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>

Hacia la parte inferior hay un SKCanvasView elemento y un TouchEffect adjunto a la celda Grid única que lo incluye.

El archivo de código subyacente TouchManipulationPage.xaml.cs tiene un bitmap campo, pero no es de tipo SKBitmap. El tipo es TouchManipulationBitmap (una clase que verá en breve):

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;
        }
    }
    ...
}

El constructor crea una instancia de un TouchManipulationBitmap objeto , pasando al constructor un SKBitmap obtenido de un recurso incrustado. El constructor concluye estableciendo la Mode propiedad de la TouchManager propiedad del TouchManipulationBitmap objeto en un miembro de la TouchManipulationMode enumeración.

El SelectedIndexChanged controlador de también Picker establece esta Mode propiedad:

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

El TouchAction controlador de la TouchEffect instancia de en el archivo XAML llama a dos métodos en TouchManipulationBitmap denominados HitTest y 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 el HitTest método devuelve true (lo que significa que un dedo ha tocado la pantalla dentro del área ocupada por el mapa de bits), el identificador táctil se agrega a la TouchIds colección. Este identificador representa la secuencia de eventos táctiles de ese dedo hasta que el dedo se levanta desde la pantalla. Si varios dedos tocan el mapa de bits, la touchIds colección contiene un identificador táctil para cada dedo.

El TouchAction controlador también llama a la ProcessTouchEvent clase en TouchManipulationBitmap. Aquí es donde se producen algunos (pero no todos) del procesamiento táctil real.

La TouchManipulationBitmap clase es una clase contenedora para SKBitmap que contiene código para representar el mapa de bits y procesar eventos táctiles. Funciona junto con código más generalizado en una TouchManipulationManager clase (que verá en breve).

El TouchManipulationBitmap constructor guarda y SKBitmap crea una instancia de dos propiedades, la TouchManager propiedad de tipo TouchManipulationManager y la Matrix propiedad de tipo 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; }
    ...
}

Esta Matrix propiedad es la transformación acumulada resultante de toda la actividad táctil. Como verá, cada evento táctil se resuelve en una matriz, que luego se concatena con el SKMatrix valor almacenado por la Matrix propiedad .

El TouchManipulationBitmap objeto se dibuja en su Paint método . El argumento es un SKCanvas objeto . Esto SKCanvas podría tener ya aplicada una transformación, por lo que el Paint método concatena la Matrix propiedad asociada con el mapa de bits a la transformación existente y restaura el lienzo cuando haya terminado:

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

El HitTest método devuelve true si el usuario toca la pantalla en un punto dentro de los límites del mapa de bits. Esto usa la lógica mostrada anteriormente en la página Rotación de mapa de bits :

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;
    }
    ...
}

El segundo método público de TouchManipulationBitmap es ProcessTouchEvent. Cuando se llama a este método, ya se ha establecido que el evento táctil pertenece a este mapa de bits concreto. El método mantiene un diccionario de TouchManipulationInfo objetos, que es simplemente el punto anterior y el nuevo punto de cada dedo:

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

    public SKPoint NewPoint { set; get; }
}

Este es el diccionario y el ProcessTouchEvent propio método:

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;
        }
    }
    ...
}

En los Moved eventos y Released , el método llama a Manipulate. En estos momentos, contiene touchDictionary uno o varios TouchManipulationInfo objetos. touchDictionary Si contiene un elemento, es probable que los PreviousPoint valores y NewPoint sean distintos y representen el movimiento de un dedo. Si varios dedos tocan el mapa de bits, el diccionario contiene más de un elemento, pero solo uno de estos elementos tiene valores y NewPoint diferentesPreviousPoint. Todos los demás tienen valores iguales PreviousPoint y NewPoint .

Esto es importante: El Manipulate método puede suponer que está procesando el movimiento de un solo dedo. En el momento de esta llamada ninguno de los otros dedos se mueven, y si realmente se mueven (como es probable), esos movimientos se procesarán en llamadas futuras a Manipulate.

El Manipulate método copia primero el diccionario en una matriz para mayor comodidad. Omite algo distinto de las dos primeras entradas. Si hay más de dos dedos intentando manipular el mapa de bits, se omiten los demás. Manipulate es el miembro 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 dedo está manipulando el mapa de bits, Manipulate llama al OneFingerManipulate método del TouchManipulationManager objeto . Para dos dedos, llama a TwoFingerManipulate. Los argumentos de estos métodos son los mismos: los prevPoint argumentos y newPoint representan el dedo que se mueve. Pero el pivotPoint argumento es diferente para las dos llamadas:

Para la manipulación de un dedo, pivotPoint es el centro del mapa de bits. Esto es para permitir la rotación de un dedo. Para la manipulación de dos dedos, el evento indica el movimiento de un solo dedo, de modo que pivotPoint es el dedo que no se mueve.

En ambos casos, TouchManipulationManager devuelve un SKMatrix valor, que el método concatena con la propiedad actual Matrix que TouchManipulationPage usa para representar el mapa de bits.

TouchManipulationManager se generaliza y no usa ningún otro archivo excepto TouchManipulationMode. Es posible que pueda usar esta clase sin cambios en sus propias aplicaciones. Define una única propiedad de tipo TouchManipulationMode:

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

Sin embargo, probablemente querrá evitar la AnisotropicScale opción . Es muy fácil con esta opción manipular el mapa de bits para que uno de los factores de escalado se convierta en cero. Esto hace que el mapa de bits desaparezca de la vista, nunca vuelva. Si realmente necesita escalado anisotrópico, querrá mejorar la lógica para evitar resultados no deseados.

TouchManipulationManager hace uso de vectores, pero dado que no hay ninguna SKVector estructura en SkiaSharp, SKPoint se usa en su lugar. SKPoint admite el operador de resta y el resultado se puede tratar como un vector. La única lógica específica del vector que se debe agregar es un Magnitude cálculo:

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

Cada vez que se selecciona la rotación, los métodos de manipulación de un dedo y dos dedos controlan primero la rotación. Si se detecta alguna rotación, el componente de rotación se quita eficazmente. Lo que queda se interpreta como movimiento panorámico y escalado.

Este es el OneFingerManipulate método . Si no se ha habilitado la rotación de un dedo, la lógica es sencilla, simplemente usa el punto anterior y el nuevo punto para construir un vector denominado delta que corresponde precisamente a la traducción. Con la rotación de un dedo habilitada, el método usa ángulos desde el punto dinámico (el centro del mapa de bits) hasta el punto anterior y el nuevo punto para construir una matriz de rotación:

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;
    }
    ...
}

En el TwoFingerManipulate método , el punto de pivote es la posición del dedo que no se mueve en este evento táctil en particular. La rotación es muy similar a la rotación de un dedo y, a continuación, el vector denominado oldVector (basado en el punto anterior) se ajusta para la rotación. El movimiento restante se interpreta como escalado:

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;
    }
    ...
}

Observará que no hay ninguna traducción explícita en este método. Sin embargo, tanto los MakeRotation métodos como MakeScale se basan en el punto de pivote y que incluye la traducción implícita. Si usa dos dedos en el mapa de bits y los arrastra en la misma dirección, TouchManipulation obtendrá una serie de eventos táctiles que alternan entre los dos dedos. A medida que cada dedo se mueve en relación con el otro, escalado o resultados de rotación, pero se niega por el movimiento del otro dedo, y el resultado es la traducción.

La única parte restante de la página Manipulación táctil es el PaintSurface controlador en el archivo de TouchManipulationPage código subyacente. Esto llama al Paint método de TouchManipulationBitmap, que aplica la matriz que representa la actividad táctil acumulada:

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));
    }
}

El PaintSurface controlador concluye mostrando un MatrixDisplay objeto que muestra la matriz táctil acumulada:

táctil Captura de pantalla triple de la página Manipulación táctil

Manipular varios mapas de bits

Una de las ventajas de aislar el código de procesamiento táctil en clases como TouchManipulationBitmap y TouchManipulationManager es la capacidad de reutilizar estas clases en un programa que permite al usuario manipular varios mapas de bits.

La página Vista de dispersión de mapa de bits muestra cómo se hace esto. En lugar de definir un campo de tipo TouchManipulationBitmap, la BitmapScatterPage clase define un List de objetos de mapa de bits:

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;
                }
            }
        }
    }
    ...
}

El constructor carga en todos los mapas de bits disponibles como recursos incrustados y los agrega a bitmapCollection. Observe que la Matrix propiedad se inicializa en cada TouchManipulationBitmap objeto, por lo que las esquinas superior izquierda de cada mapa de bits se desplazan por 100 píxeles.

La BitmapScatterView página también debe controlar eventos táctiles para varios mapas de bits. En lugar de definir un List identificador táctil de objetos manipulados TouchManipulationBitmap actualmente, este programa requiere un diccionario:

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;
        }
    }
    ...
}

Observe cómo la Pressed lógica recorre en bucle a bitmapCollection la inversa. Los mapas de bits a menudo se superponen entre sí. Los mapas de bits más adelante en la colección se encuentran visualmente encima de los mapas de bits anteriores en la colección. Si hay varios mapas de bits bajo el dedo que presiona en la pantalla, el más alto debe ser el que manipula ese dedo.

Observe también que la lógica mueve ese Pressed mapa de bits al final de la colección para que se mueva visualmente a la parte superior de la pila de otros mapas de bits.

En los Moved eventos y Released , el TouchAction controlador llama al ProcessingTouchEvent método en TouchManipulationBitmap igual que el programa anterior.

Por último, el PaintSurface controlador llama al Paint método de cada TouchManipulationBitmap objeto:

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);
        }
    }
}

El código recorre la colección y muestra la pila de mapas de bits desde el principio de la colección hasta el final:

de bits Captura de pantalla triple de la página Vista de dispersión de mapa de bits

escalado de Single-Finger

Por lo general, una operación de escalado requiere un gesto de reducir con dos dedos. Sin embargo, es posible implementar el escalado con un solo dedo haciendo que el dedo mueva las esquinas de un mapa de bits.

Esto se muestra en la página Escala de esquina de un solo dedo . Dado que en este ejemplo se usa un tipo de escalado ligeramente diferente al implementado en la TouchManipulationManager clase , no usa esa clase ni la TouchManipulationBitmap clase . En su lugar, toda la lógica táctil está en el archivo de código subyacente. Esta es una lógica algo más sencilla de lo habitual porque realiza un seguimiento de un solo dedo a la vez y simplemente omite los dedos secundarios que puedan estar tocando la pantalla.

La página SingleFingerCornerScale.xaml crea una instancia de la SKCanvasView clase y crea un objeto para realizar el TouchEffect seguimiento de eventos táctiles:

<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>

El archivo SingleFingerCornerScalePage.xaml.cs carga un recurso de mapa de bits desde el directorio Media y lo muestra mediante un SKMatrix objeto definido como campo:

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);
    }
    ···
}

Este SKMatrix objeto se modifica mediante la lógica táctil que se muestra a continuación.

El resto del archivo de código subyacente es el TouchEffect controlador de eventos. Comienza convirtiendo la ubicación actual del dedo en un SKPoint valor. Para el Pressed tipo de acción, el controlador comprueba que ningún otro dedo está tocando la pantalla y que el dedo está dentro de los límites del mapa de bits.

La parte fundamental del código es una if instrucción que implica dos llamadas al Math.Pow método . Esta matemática comprueba si la ubicación del dedo está fuera de una elipse que rellena el mapa de bits. Si es así, se trata de una operación de escalado. El dedo está cerca de una de las esquinas del mapa de bits y se determina un punto de pivote que es la esquina opuesta. Si el dedo está dentro de esta elipse, es una operación de movimiento panorámico normal:

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;
        }
    }
}

El Moved tipo de acción calcula una matriz correspondiente a la actividad táctil desde el momento en que el dedo presionó la pantalla hasta esta vez. Concatena esa matriz con la matriz en vigor en el momento en que el dedo presionó por primera vez el mapa de bits. La operación de escalado siempre es relativa a la esquina opuesta a la que tocó el dedo.

Para mapas de bits pequeños o oblong, una elipse interior puede ocupar la mayoría del mapa de bits y dejar áreas pequeñas en las esquinas para escalar el mapa de bits. Es posible que prefiera un enfoque ligeramente diferente, en cuyo caso puede reemplazar ese bloque completo if que establece isScaling en true por este código:

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);
    }
}

Este código divide eficazmente el área del mapa de bits en una forma de diamante interior y cuatro triángulos en las esquinas. Esto permite que las áreas mucho más grandes de las esquinas grabe y escale el mapa de bits.