Partilhar via


Manipulações de toque

Usar transformações de matriz para implementar arrastamento, pinçamento e rotação por toque

Em ambientes multitoque, como os de dispositivos móveis, os usuários costumam usar os dedos para manipular objetos na tela. Gestos comuns, como um arrastar de um dedo e uma pinça de dois dedos, podem mover e dimensionar objetos, ou até mesmo girá-los. Esses gestos geralmente são implementados usando matrizes de transformação, e este artigo mostra como fazer isso.

Um bitmap sujeito a translação, dimensionamento e rotação

Todos os exemplos mostrados aqui usam o Xamarin.Forms efeito de rastreamento de toque apresentado no artigo Invocando eventos de efeitos.

Arrastamento e Tradução

Uma das aplicações mais importantes das transformações matriciais é o processamento tátil. Um único SKMatrix valor pode consolidar uma série de operações de toque.

Para arrastar com um único dedo, o valor executa a SKMatrix tradução. Isso é demonstrado na página Arrastar bitmap. O arquivo XAML instancia um SKCanvasView em um Xamarin.FormsGridarquivo . Um TouchEffect objeto foi adicionado à Effects coleção desse 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>

Em teoria, o TouchEffect objeto poderia ser adicionado diretamente à Effects coleção do SKCanvasView, mas isso não funciona em todas as plataformas. Porque o SKCanvasView é do mesmo tamanho que o Grid nesta configuração, anexando-o Grid às obras também.

O arquivo code-behind é carregado em um recurso de bitmap em seu construtor e o exibe no PaintSurface manipulador:

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

Sem nenhum código adicional, o SKMatrix valor é sempre a matriz de identificação e não teria efeito sobre a exibição do bitmap. O objetivo do OnTouchEffectAction manipulador definido no arquivo XAML é alterar o valor da matriz para refletir manipulações de toque.

O OnTouchEffectAction manipulador começa convertendo o Xamarin.FormsPoint valor em um valor SkiaSharp SKPoint . Esta é uma questão simples de dimensionamento com base nas propriedades e Height de Width (que são unidades independentes de SKCanvasView dispositivo) e na CanvasSize propriedade, que está em unidades 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;
        }
    }
    ···
}

Quando um dedo toca pela primeira vez na tela, um evento do tipo TouchActionType.Pressed é disparado. A primeira tarefa é determinar se o dedo está tocando o bitmap. Tal tarefa é muitas vezes chamada de teste de acerto. Nesse caso, o teste de acerto pode ser realizado criando um SKRect valor correspondente ao bitmap, aplicando a transformação de matriz a ele com MapRecto e determinando se o ponto de toque está dentro do retângulo transformado.

Se esse for o caso, o touchId campo será definido como ID de toque e a posição do dedo será salva.

Para o TouchActionType.Moved evento, os fatores de translação do SKMatrix valor são ajustados com base na posição atual do dedo e na nova posição do dedo. Essa nova posição é salva para a próxima vez e a SKCanvasView é invalidada.

Ao experimentar este programa, observe que você só pode arrastar o bitmap quando o dedo toca em uma área onde o bitmap é exibido. Embora essa restrição não seja muito importante para este programa, torna-se crucial ao manipular vários bitmaps.

Pinçar e dimensionar

O que você quer que aconteça quando dois dedos tocam o bitmap? Se os dois dedos se movem em paralelo, então você provavelmente deseja que o bitmap se mova junto com os dedos. Se os dois dedos executarem uma operação de pinça ou estiramento, convém que o bitmap seja girado (a ser discutido na próxima seção) ou dimensionado. Ao dimensionar um bitmap, faz mais sentido que os dois dedos permaneçam nas mesmas posições em relação ao bitmap e que o bitmap seja dimensionado de acordo.

Lidar com dois dedos ao mesmo tempo parece complicado, mas tenha em mente que o TouchAction manipulador só recebe informações sobre um dedo de cada vez. Se dois dedos estiverem manipulando o bitmap, para cada evento, um dedo mudou de posição, mas o outro não mudou. No código da página Bitmap Scaling abaixo, o dedo que não mudou de posição é chamado de ponto de pivô porque a transformação é relativa a esse ponto.

Uma diferença entre este programa e o programa anterior é que vários IDs de toque devem ser salvos. Um dicionário é usado para essa finalidade, onde o ID de toque é a chave do dicionário e o valor do dicionário é a posição atual desse 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;
        }
    }
    ···
}

O tratamento da ação é quase o mesmo que o programa anterior, exceto que o ID e o ponto de Pressed toque são adicionados ao dicionário. As Released ações e Cancelled removem a entrada do dicionário.

O manejo para a Moved ação, no entanto, é mais complexo. Se houver apenas um dedo envolvido, o processamento é muito parecido com o programa anterior. Para dois ou mais dedos, o programa também deve obter informações do dicionário envolvendo o dedo que não está se movendo. Ele faz isso copiando as chaves do dicionário em uma matriz e, em seguida, comparando a primeira chave com o ID do dedo que está sendo movido. Isso permite que o programa obtenha o ponto de pivô correspondente ao dedo que não está se movendo.

Em seguida, o programa calcula dois vetores da nova posição do dedo em relação ao ponto de pivô e a posição antiga do dedo em relação ao ponto de pivô. As proporções desses vetores são fatores de escala. Como a divisão por zero é uma possibilidade, eles devem ser verificados para valores infinitos ou valores NaN (não um número). Se tudo estiver bem, uma transformação de dimensionamento será concatenada com o SKMatrix valor salvo como um campo.

Ao experimentar esta página, você notará que pode arrastar o bitmap com um ou dois dedos ou dimensioná-lo com dois dedos. A escala é anisotrópica, o que significa que a escala pode ser diferente nas direções horizontal e vertical. Isso distorce a proporção, mas também permite que você inverta o bitmap para criar uma imagem espelhada. Você também pode descobrir que pode reduzir o bitmap para uma dimensão zero e ele desaparece. No código de produção, você vai querer se proteger contra isso.

Rotação de dois dedos

A página Girar bitmap permite que você use dois dedos para rotação ou dimensionamento isotrópico. O bitmap sempre mantém sua proporção correta. Usar dois dedos para rotação e escala anisotrópica não funciona muito bem porque o movimento dos dedos é muito semelhante para ambas as tarefas.

A primeira grande diferença neste programa é a lógica de teste de acertos. Os programas anteriores usavam o Contains método de para determinar se o ponto de toque está dentro do retângulo transformado SKRect que corresponde ao bitmap. Mas, à medida que o usuário manipula o bitmap, o bitmap pode ser girado e SKRect não pode representar corretamente um retângulo girado. Você pode temer que a lógica de teste de acerto precise implementar geometria analítica bastante complexa nesse caso.

No entanto, um atalho está disponível: determinar se um ponto está dentro dos limites de um retângulo transformado é o mesmo que determinar se um ponto transformado inverso está dentro dos limites do retângulo não transformado. Esse é um cálculo muito mais fácil, e a lógica pode continuar a usar o método conveniente 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));
    }
    ···
}

A lógica para o Moved evento começa como o programa anterior. Dois vetores nomeados oldVector e newVector são calculados com base no ponto anterior e atual do dedo em movimento e no ponto de pivô do dedo imóvel. Mas então os ângulos desses vetores são determinados, e a diferença é o ângulo de rotação.

O dimensionamento também pode estar envolvido, de modo que o vetor antigo é girado com base no ângulo de rotação. A magnitude relativa dos dois vetores é agora o fator de escala. Observe que o mesmo scale valor é usado para dimensionamento horizontal e vertical para que o dimensionamento seja isotrópico. O matrix campo é ajustado pela matriz de rotação e por uma matriz de escala.

Se seu aplicativo precisar implementar o processamento por toque para um único bitmap (ou outro objeto), você poderá adaptar o código desses três exemplos para seu próprio aplicativo. Mas se você precisar implementar o processamento por toque para vários bitmaps, provavelmente desejará encapsular essas operações de toque em outras classes.

Encapsulando as operações de toque

A página Manipulação de toque demonstra a manipulação de toque de um único bitmap, mas usando vários outros arquivos que encapsulam grande parte da lógica mostrada acima. O primeiro desses arquivos é a TouchManipulationMode enumeração, que indica os diferentes tipos de manipulação de toque implementados pelo código que você verá:

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

PanOnly é um arrasto de um dedo que é implementado com a tradução. Todas as opções subsequentes também incluem movimento panorâmico, mas envolvem dois dedos: IsotropicScale é uma operação de pinça que resulta no dimensionamento do objeto igualmente nas direções horizontal e vertical. AnisotropicScale permite escalonamento desigual.

A ScaleRotate opção é para escalonamento e rotação com dois dedos. A escala é isotrópica. Como mencionado anteriormente, a implementação da rotação de dois dedos com escala anisotrópica é problemática porque os movimentos dos dedos são essencialmente os mesmos.

A ScaleDualRotate opção adiciona rotação de um dedo. Quando um único dedo arrasta o objeto, o objeto arrastado é primeiro girado em torno de seu centro para que o centro do objeto se alinhe com o vetor de arrastamento.

O arquivo TouchManipulationPage.xaml inclui um Picker com os TouchManipulationMode membros da enumeração:

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

Na parte inferior está um SKCanvasView e um TouchEffect ligado à célula Grid única que o encerra.

O arquivo code-behind TouchManipulationPage.xaml.cs tem um bitmap campo, mas não é do tipo SKBitmap. O tipo é TouchManipulationBitmap (uma classe que você verá em 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;
        }
    }
    ...
}

O construtor instancia um TouchManipulationBitmap objeto, passando para o construtor um SKBitmap obtido de um recurso incorporado. O construtor conclui definindo a Mode TouchManager propriedade da propriedade do TouchManipulationBitmap objeto como um membro da TouchManipulationMode enumeração.

O SelectedIndexChanged manipulador para o Picker também define esta Mode propriedade:

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

O TouchAction manipulador do TouchEffect instanciado no arquivo XAML chama dois métodos em TouchManipulationBitmap named HitTest e 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;
        }
    }
    ...
}

Se o HitTest método retornar true — o que significa que um dedo tocou na tela dentro da área ocupada pelo bitmap — o ID de toque será adicionado à TouchIds coleção. Esse ID representa a sequência de eventos de toque para esse dedo até que o dedo seja levantado da tela. Se vários dedos tocarem no bitmap, a touchIds coleção conterá uma ID de toque para cada dedo.

O TouchAction manipulador também chama a ProcessTouchEvent classe em TouchManipulationBitmap. É aqui que ocorre parte (mas não todas) do processamento de toque real.

A TouchManipulationBitmap classe é uma classe wrapper para SKBitmap que contém código para renderizar o bitmap e processar eventos de toque. Ele funciona em conjunto com código mais generalizado em uma TouchManipulationManager classe (que você verá em breve).

O TouchManipulationBitmap construtor salva o SKBitmap e instancia duas propriedades, a TouchManager propriedade de type TouchManipulationManager e a Matrix propriedade 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; }
    ...
}

Essa Matrix propriedade é a transformação acumulada resultante de toda a atividade de toque. Como você verá, cada evento de toque é resolvido em uma matriz, que é então concatenada com o SKMatrix valor armazenado pela Matrix propriedade.

O TouchManipulationBitmap objeto desenha a si mesmo em seu Paint método. O argumento é um SKCanvas objeto. Isso SKCanvas já pode ter uma transformação aplicada a ele, portanto, o Paint método concatena a Matrix propriedade associada ao bitmap à transformação existente e restaura a tela quando ela terminar:

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

O HitTest método retorna true se o usuário tocar na tela em um ponto dentro dos limites do bitmap. Isso usa a lógica mostrada anteriormente na página Rotação de 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;
    }
    ...
}

O segundo método público em TouchManipulationBitmap é ProcessTouchEvent. Quando esse método é chamado, já foi estabelecido que o evento touch pertence a esse bitmap específico. O método mantém um dicionário de TouchManipulationInfo objetos, que é simplesmente o ponto anterior e o novo ponto de cada dedo:

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

    public SKPoint NewPoint { set; get; }
}

Aqui está o dicionário e o ProcessTouchEvent método em si:

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

Moved Nos eventos e Released , o método chama Manipulate. Nessas horas, o touchDictionary contém um ou mais TouchManipulationInfo objetos. Se o touchDictionary contém um item, é provável que os PreviousPoint valores e NewPoint sejam desiguais e representem o movimento de um dedo. Se vários dedos estiverem tocando no bitmap, o dicionário conterá mais de um item, mas apenas um desses itens terá valores e PreviousPoint NewPoint diferentes. Todos os demais têm valores iguais PreviousPoint e NewPoint iguais.

Isso é importante: o Manipulate método pode assumir que está processando o movimento de apenas um dedo. No momento desta chamada, nenhum dos outros dedos está se movendo, e se eles realmente estão se movendo (como é provável), esses movimentos serão processados em chamadas futuras para Manipulate.

O Manipulate método primeiro copia o dicionário para uma matriz por conveniência. Ele ignora qualquer coisa além das duas primeiras entradas. Se mais de dois dedos estiverem tentando manipular o bitmap, os outros serão ignorados. Manipulate é o membro 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;
    }
}

Se um dedo estiver manipulando o bitmap, Manipulate chame o OneFingerManipulate método do TouchManipulationManager objeto. Para dois dedos, chama TwoFingerManipulate. Os argumentos para esses métodos são os mesmos: os prevPoint argumentos e newPoint representam o dedo que está se movendo. Mas o pivotPoint argumento é diferente para as duas chamadas:

Para manipulação com um dedo, o pivotPoint é o centro do bitmap. Isso é para permitir a rotação de um dedo. Para a manipulação com dois dedos, o evento indica o movimento de apenas um dedo, de modo que o pivotPoint dedo não está se movendo.

Em ambos os casos, TouchManipulationManager retorna um SKMatrix valor, que o método concatena com a propriedade atual Matrix que TouchManipulationPage usa para renderizar o bitmap.

TouchManipulationManager é generalizado e não usa outros arquivos, exceto TouchManipulationMode. Você pode usar essa classe sem alteração em seus próprios aplicativos. Ela define uma única propriedade do tipo TouchManipulationMode:

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

No entanto, você provavelmente vai querer evitar a AnisotropicScale opção. É muito fácil com essa opção manipular o bitmap para que um dos fatores de dimensionamento se torne zero. Isso faz com que o bitmap desapareça de vista, para nunca mais voltar. Se você realmente precisa de escalonamento anisotrópico, você vai querer melhorar a lógica para evitar resultados indesejáveis.

TouchManipulationManager faz uso de vetores, mas como não SKVector há estrutura no SkiaSharp, SKPoint é usado em vez disso. SKPoint suporta o operador de subtração, e o resultado pode ser tratado como um vetor. A única lógica específica do vetor que precisava ser adicionada é um Magnitude cálculo:

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

Sempre que a rotação foi selecionada, os métodos de manipulação com um e dois dedos manipulam a rotação primeiro. Se qualquer rotação for detectada, o componente de rotação será efetivamente removido. O que resta é interpretado como movimento panorâmico e dimensionamento.

Aqui está o OneFingerManipulate método. Se a rotação de um dedo não foi habilitada, então a lógica é simples - ela simplesmente usa o ponto anterior e o novo ponto para construir um vetor chamado delta que corresponde precisamente à tradução. Com a rotação de um dedo habilitada, o método usa ângulos do ponto dinâmico (o centro do bitmap) para o ponto anterior e novo ponto para construir uma matriz de rotação:

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

No método, o TwoFingerManipulate ponto de pivô é a posição do dedo que não está se movendo nesse evento de toque específico. A rotação é muito semelhante à rotação de um dedo, e então o vetor nomeado oldVector (com base no ponto anterior) é ajustado para a rotação. O movimento restante é interpretado como escala:

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

Você notará que não há tradução explícita nesse método. No entanto, os MakeRotation métodos e MakeScale são baseados no ponto de pivô, e isso inclui a tradução implícita. Se você estiver usando dois dedos no bitmap e arrastando-os na mesma direção, TouchManipulation obterá uma série de eventos de toque alternando entre os dois dedos. À medida que cada dedo se move em relação ao outro, a escala ou rotação resulta, mas é negada pelo movimento do outro dedo, e o resultado é a translação.

A única parte restante da página Manipulação de toque é o PaintSurface manipulador no TouchManipulationPage arquivo code-behind. Isso chama o Paint TouchManipulationBitmapmétodo do , que aplica a matriz que representa a atividade de toque 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));
    }
}

O PaintSurface manipulador conclui exibindo um MatrixDisplay objeto mostrando a matriz de toque acumulada:

Captura de tela tripla da página Manipulação de toque

Manipulando vários bitmaps

Uma das vantagens de isolar o código de processamento por toque em classes como TouchManipulationBitmap e TouchManipulationManager é a capacidade de reutilizar essas classes em um programa que permite ao usuário manipular vários bitmaps.

A página Exibição de dispersão de bitmap demonstra como isso é feito. Em vez de definir um campo de tipo TouchManipulationBitmap, a BitmapScatterPage classe define um List dos objetos 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;
                }
            }
        }
    }
    ...
}

O construtor carrega todos os bitmaps disponíveis como recursos incorporados e os adiciona ao bitmapCollection. Observe que a Matrix propriedade é inicializada em cada TouchManipulationBitmap objeto, portanto, os cantos superiores esquerdos de cada bitmap são deslocados em 100 pixels.

A BitmapScatterView página também precisa manipular eventos de toque para vários bitmaps. Em vez de definir um List dos IDs de toque de objetos manipulados TouchManipulationBitmap atualmente, este programa requer um dicionário:

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 como a Pressed lógica faz um loop inverso bitmapCollection . Os bitmaps geralmente se sobrepõem. Os bitmaps posteriores na coleção ficam visualmente sobre os bitmaps anteriores na coleção. Se houver vários bitmaps sob o dedo que pressiona na tela, o mais alto deve ser aquele que é manipulado por esse dedo.

Observe também que a Pressed lógica move esse bitmap para o final da coleção para que ele se mova visualmente para o topo da pilha de outros bitmaps.

Moved Nos eventos e Released , o TouchAction manipulador chama o ProcessingTouchEvent método como TouchManipulationBitmap o programa anterior.

Finalmente, o PaintSurface manipulador chama o 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);
        }
    }
}

O código percorre a coleção e exibe a pilha de bitmaps do início à final:

Captura de tela tripla da página Modo de Exibição de Dispersão de Bitmap

Dimensionamento com um único dedo

Uma operação de dimensionamento geralmente requer um gesto de pinça usando dois dedos. No entanto, é possível implementar o dimensionamento com um único dedo fazendo com que o dedo mova os cantos de um bitmap.

Isso é demonstrado na página Escala de canto de dedo único. Como esse exemplo usa um tipo de dimensionamento um pouco diferente daquele implementado na TouchManipulationManager classe, ele não usa essa classe ou a TouchManipulationBitmap classe. Em vez disso, toda a lógica de toque está no arquivo code-behind. Essa é uma lógica um pouco mais simples do que o normal, porque rastreia apenas um dedo de cada vez e simplesmente ignora quaisquer dedos secundários que possam estar tocando na tela.

A página SingleFingerCornerScale.xaml instancia a SKCanvasView classe e cria um TouchEffect objeto para controlar eventos de toque:

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

O arquivo SingleFingerCornerScalePage.xaml.cs carrega um recurso de bitmap do diretório Media e o exibe usando um SKMatrix objeto definido como um 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 é modificado pela lógica de toque mostrada abaixo.

O restante do arquivo code-behind é o TouchEffect manipulador de eventos. Ele começa convertendo o local atual do dedo em um SKPoint valor. Para o Pressed tipo de ação, o manipulador verifica se nenhum outro dedo está tocando na tela e se o dedo está dentro dos limites do bitmap.

A parte crucial do código é uma if instrução envolvendo duas chamadas para o Math.Pow método. Essa matemática verifica se o local do dedo está fora de uma elipse que preenche o bitmap. Se sim, então essa é uma operação de dimensionamento. O dedo está perto de um dos cantos do bitmap e um ponto de pivô é determinado que é o canto oposto. Se o dedo estiver dentro dessa elipse, é uma operação de movimento panorâmico regular:

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

O Moved tipo de ação calcula uma matriz correspondente à atividade de toque desde o momento em que o dedo pressionou a tela até esse momento. Ele concatena essa matriz com a matriz em vigor no momento em que o dedo pressionou o bitmap pela primeira vez. A operação de dimensionamento é sempre relativa ao canto oposto àquele que o dedo tocou.

Para bitmaps pequenos ou oblongos, uma elipse interna pode ocupar a maior parte do bitmap e deixar pequenas áreas nos cantos para dimensionar o bitmap. Você pode preferir uma abordagem um pouco diferente, caso em que você pode substituir todo if o bloco que define true isScaling com 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);
    }
}

Esse código efetivamente divide a área do bitmap em uma forma de diamante interior e quatro triângulos nos cantos. Isso permite que áreas muito maiores nos cantos agarrem e dimensionem o bitmap.