Поделиться через


Манипуляции сенсорного ввода

Использование матрицных преобразований для реализации перетаскивания, перетаскивания, сцепления и поворота

В много сенсорных средах, таких как на мобильных устройствах, пользователи часто используют пальцы для управления объектами на экране. Распространенные жесты, такие как перетаскивание с одним пальцем, и двумя пальцами, могут перемещать и масштабировать объекты, или даже поворачивать их. Эти жесты обычно реализуются с помощью матриц преобразования, и в этой статье показано, как это сделать.

Растровое изображение, подверженное переводу, масштабированию и повороту

Все примеры, показанные здесь, используют Xamarin.Forms эффект сенсорного отслеживания, представленный в статье "Вызов событий из эффектов".

Перетаскивание и перевод

Одним из наиболее важных приложений преобразования матрицы является обработка сенсорной обработки. Одно SKMatrix значение может консолидировать ряд операций касания.

Для перетаскивания SKMatrix с одним пальцем значение выполняет перевод. Это показано на странице перетаскивания растрового рисунка. XAML-файл создает SKCanvasView экземпляр в объекте Xamarin.FormsGrid. Объект TouchEffect был добавлен в коллекциюEffects: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>

В теории TouchEffect объект можно добавить непосредственно в Effects коллекцию SKCanvasView, но это не работает на всех платформах. Так как размер SKCanvasView такой же, как Grid и в этой конфигурации, присоединяя его к Grid работе так же хорошо.

Файл программной части загружается в ресурс растрового изображения в конструкторе и отображает его в обработчике PaintSurface :

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

Без дальнейшего кода SKMatrix значение всегда является матрицей идентификации, и оно не будет влиять на отображение растрового изображения. Целью обработчика OnTouchEffectAction , заданного в XAML-файле, является изменение значения матрицы для отражения сенсорных манипуляций.

Обработчик OnTouchEffectAction начинается с преобразования Xamarin.FormsPoint значения в значение SkiaSharp SKPoint . Это простой вопрос масштабирования на Width основе свойств HeightSKCanvasView (которые являются устройствами независимо от устройства) и CanvasSize свойства, которые находятся в единицах пикселей:

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

Когда пальцем сначала прикасается к экрану, событие типа TouchActionType.Pressed запускается. Первая задача — определить, касается ли палец растрового изображения. Такая задача часто называется хит-тестированием. В этом случае можно выполнить тестирование попаданий, создав SKRect значение, соответствующее растровой карте, применив к нему MapRectпреобразование матрицы, а затем определив, находится ли точка касания внутри преобразованного прямоугольника.

Если это так, touchId поле задается идентификатором сенсорного ввода и сохраняется положение пальца.

TouchActionType.Moved Для события факторы SKMatrix перевода значения корректируются на основе текущего положения пальца и нового положения пальца. Эта новая позиция сохраняется в следующий раз и SKCanvasView является недействительным.

Поэкспериментируйте с этой программой, обратите внимание, что вы можете перетаскивать растровое изображение только при касании пальца области, в которой отображается растровое изображение. Хотя это ограничение не очень важно для этой программы, она становится важной при управлении несколькими растровыми изображениями.

Закрепление и масштабирование

Что нужно сделать, когда два пальца касаются растрового изображения? Если два пальца перемещаются параллельно, то, вероятно, нужно, чтобы растровое изображение перемещалось вместе с пальцами. Если два пальца выполняют операцию сцепления или растяжения, может потребоваться повернуть растровое изображение (обсудить в следующем разделе) или масштабировать. При масштабировании растрового изображения большинство пальцев должны оставаться в одинаковых позициях относительно растрового изображения, а также для масштабирования растрового изображения соответствующим образом.

Обработка двух пальцев одновременно кажется сложной, но помните, что TouchAction обработчик получает только информацию о одном пальце за раз. Если два пальца управляют растровым изображением, то для каждого события один палец изменил положение, но другой не изменился. В приведенном ниже коде страницы масштабирования растрового рисунка палец, который не изменил позицию, называется точкой сводной таблицы, так как преобразование относительно этой точки.

Одно из различий между этой программой и предыдущей программой заключается в том, что необходимо сохранить несколько идентификаторов касания. Словарь используется для этой цели, где сенсорный идентификатор является ключом словаря, а значение словаря — текущей позицией этого пальца:

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

Обработка действия почти аналогична предыдущей Pressed программе, за исключением того, что идентификатор и точка касания добавляются в словарь. Cancelled И Released действия удаляют запись словаря.

Однако обработка Moved действия более сложна. Если есть только один палец, обработка очень аналогична предыдущей программе. Для двух или нескольких пальцев программа также должна получить информацию из словаря, включающего пальцем, который не перемещается. Это делается путем копирования ключей словаря в массив, а затем сравнения первого ключа с идентификатором перемещаемого пальца. Это позволяет программе получить точку сводных данных, соответствующую пальцу, который не перемещается.

Затем программа вычисляет два вектора новой позиции пальца относительно точки сводных точек и старое положение пальца относительно точки сводных данных. Коэффициенты этих векторов являются коэффициентами масштабирования. Поскольку деление по нулю является возможностью, они должны быть проверка для бесконечных значений или naN (а не числа). Если все хорошо, преобразование масштабирования объединяется со SKMatrix значением, сохраненным в виде поля.

Поэкспериментируя с этой страницей, вы заметите, что можно перетащить растровое изображение одним или двумя пальцами или масштабировать его двумя пальцами. Масштабирование является анизотропным, что означает, что масштабирование может отличаться в горизонтальных и вертикальных направлениях. Это искажает пропорции, но также позволяет перевернуть растровое изображение, чтобы сделать изображение зеркало. Вы также можете обнаружить, что можно уменьшить растровое изображение до нуля измерения, и он исчезает. В рабочем коде вы хотите защититься от этого.

Поворот двумя пальцами

Страница поворота растрового изображения позволяет использовать два пальца для поворота или изотропного масштабирования. Растровое изображение всегда сохраняет правильное соотношение пропорций. Использование двух пальцев для поворота и анисотропного масштабирования не работает очень хорошо, потому что движение пальцев очень похоже для обеих задач.

Первое большое различие в этой программе — логика хит-тестирования. Предыдущие программы использовали Contains метод SKRect определения того, находится ли точка касания внутри преобразованного прямоугольника, соответствующего растровой карте. Но по мере того как пользователь управляет растровым изображением, то растровое изображение может быть повернуто и SKRect не может правильно представлять повернутый прямоугольник. Вы можете бояться, что логика хит-тестирования должна реализовать довольно сложную геометрию аналитики в этом случае.

Однако ярлык доступен: определение того, находится ли точка в границах преобразованного прямоугольника, совпадает с определением того, находится ли обратная преобразованная точка в границах нетрансформированного прямоугольника. Это гораздо проще вычислений, и логика может продолжать использовать удобный 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));
    }
    ···
}

Логика Moved события начинается так же, как и предыдущая программа. Два вектора с именем oldVector и newVector вычисляются на основе предыдущей и текущей точки движущегося пальца и точки свораченного пальца. Но затем определяются угловые векторы, а разница — угол поворота.

Масштабирование также может быть задействовано, поэтому старый вектор поворачивается на основе угла поворота. Относительная величина двух векторов теперь является коэффициентом масштабирования. Обратите внимание, что одно и то же scale значение используется для горизонтального и вертикального масштабирования, чтобы масштабирование было isotropic. Поле matrix настраивается матрицей поворота и матрицей масштабирования.

Если приложению необходимо реализовать обработку сенсорного ввода для одного растрового изображения (или другого объекта), можно адаптировать код из этих трех примеров для собственного приложения. Но если вам нужно реализовать обработку сенсорного ввода для нескольких растровых изображений, вероятно, потребуется инкапсулировать эти операции касания в других классах.

Инкапсулирование операций касания

На странице обработки касания демонстрируется обработка касания одного растрового изображения, но использование нескольких других файлов, которые инкапсулируют большую часть логики, показанной выше. Первым из этих файлов является TouchManipulationMode перечисление, указывающее различные типы манипуляций касанием, реализованные в коде, который вы увидите:

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

PanOnly — это перетаскивание с одним пальцем, реализуемое с помощью перевода. Все последующие параметры также включают сдвиг, но включают два пальца: IsotropicScale это операция сцепления, которая приводит к масштабированию объекта одинаково в горизонтальных и вертикальных направлениях. AnisotropicScale разрешает неравное масштабирование.

Этот ScaleRotate параметр предназначен для двух пальцев масштабирования и поворота. Масштабирование является isotropic. Как упоминание ранее, реализация двумя пальцами поворота с анизотропным масштабированием проблематична, так как движения пальцев в основном одинаковы.

Параметр ScaleDualRotate добавляет поворот одним пальцем. Когда один палец перетаскивает объект, перетаскиваемый объект сначала поворачивается вокруг его центра, чтобы центр объекта линий вверх с вектором перетаскивания.

Файл TouchManipulationPage.xaml содержит Picker элементы перечисления TouchManipulationMode :

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             xmlns:tt="clr-namespace:TouchTracking"
             xmlns:local="clr-namespace:SkiaSharpFormsDemos.Transforms"
             x:Class="SkiaSharpFormsDemos.Transforms.TouchManipulationPage"
             Title="Touch Manipulation">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

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

К нижней части — это SKCanvasView и присоединенная TouchEffect к одной ячейке Grid , заключающая ее.

В файле кода TouchManipulationPage.xaml.cs есть bitmap поле, но оно не имеет типаSKBitmap. Тип — TouchManipulationBitmap (класс, который вы увидите в ближайшее время):

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

Конструктор создает экземпляр TouchManipulationBitmap объекта, передавая конструктору SKBitmap полученный из внедренного ресурса. Конструктор завершается путем задания Mode свойства TouchManager свойства TouchManipulationBitmap объекта элементу TouchManipulationMode перечисления.

Обработчик для этого свойства также задает следующее SelectedIndexChangedPickerMode :

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

Обработчик TouchAction экземпляра TouchEffect в XAML-файле вызывает два метода в TouchManipulationBitmap именованном HitTest и 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;
        }
    }
    ...
}

HitTest Если метод возвращается true — это означает, что палец коснулся экрана в области, занятой растровым изображением, то сенсорный идентификатор добавляется в коллекциюTouchIds. Этот идентификатор представляет последовательность событий касания для этого пальца, пока палец не поднимается с экрана. Если несколько пальцев касаются растрового изображения, touchIds коллекция содержит идентификатор касания для каждого пальца.

Обработчик TouchAction также вызывает ProcessTouchEvent класс в TouchManipulationBitmap. Это место, где происходит некоторые (но не все) реальной обработки сенсорного ввода.

Класс TouchManipulationBitmap является классом-оболочкой, который содержит код для SKBitmap отрисовки растрового изображения и обработки событий касания. Он работает в сочетании с более обобщенным кодом в TouchManipulationManager классе (который вы увидите в ближайшее время).

Конструктор TouchManipulationBitmap сохраняет SKBitmap и создает экземпляры двух свойств, TouchManager свойства типа TouchManipulationManager и Matrix свойства типа 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; }
    ...
}

Это Matrix свойство является накопленным преобразованием, полученным из всех действий касания. Как видно, каждое событие касания разрешается в матрицу, которая затем объединяется со SKMatrix значением, хранящимся свойством Matrix .

Объект TouchManipulationBitmap рисует себя в методе Paint . Аргумент является SKCanvas объектом. Это SKCanvas может уже применить к нему преобразование, поэтому Paint метод объединяет Matrix свойство, связанное с растровым изображением к существующему преобразованию, и восстанавливает холст после завершения:

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

Метод HitTest возвращается, если пользователь прикасается true к экрану в точке в границах растрового изображения. Для этого используется логика, показанная ранее на странице поворота растрового рисунка:

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

Второй открытый метод в TouchManipulationBitmapProcessTouchEvent. При вызове этого метода уже установлено, что событие сенсорного ввода принадлежит этому конкретному растровому рисунку. Метод поддерживает словарь TouchManipulationInfo объектов, который является просто предыдущей точкой и новой точкой каждого пальца:

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

    public SKPoint NewPoint { set; get; }
}

Вот словарь и ProcessTouchEvent сам метод:

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 В и Released событиях метод вызываетсяManipulate. В это время touchDictionary содержит один или несколько TouchManipulationInfo объектов. Если элемент touchDictionary содержит один элемент, скорее всего PreviousPointNewPoint , они неравны и представляют движение пальца. Если несколько пальцев касаются растрового изображения, словарь содержит несколько элементов, но только один из этих элементов имеет разные PreviousPoint значения и NewPoint значения. Все остальные имеют равные PreviousPoint значения и NewPoint значения.

Это важно: Manipulate метод может предположить, что он обрабатывает движение только одного пальца. Во время этого вызова ни один из остальных пальцев двигается, и если они действительно движутся (как вероятно), эти движения будут обработаны в будущих вызовах Manipulate.

Метод Manipulate сначала копирует словарь в массив для удобства. Он игнорирует что-либо, отличное от первых двух записей. Если более двух пальцев пытаются управлять растровым изображением, остальные игнорируются. Manipulate является окончательным членом 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;
    }
}

Если один палец управляет растровым изображением, Manipulate вызывает OneFingerManipulate метод TouchManipulationManager объекта. Для двух пальцев он вызывается TwoFingerManipulate. Аргументы этих методов одинаковы: prevPoint аргументы newPoint представляют пальцем, который перемещается. pivotPoint Но аргумент отличается для двух вызовов:

Для манипуляции pivotPoint с одним пальцем центр растрового изображения. Это позволяет вращать один пальцем. Для двухфакторной манипуляции событие указывает на движение только одного пальца, чтобы pivotPoint это палец, который не двигался.

В обоих случаях TouchManipulationManager возвращает SKMatrix значение, которое метод объединяет с текущим Matrix свойством, которое TouchManipulationPage используется для отрисовки растрового изображения.

TouchManipulationManager является обобщенным и не использует другие файлы, кроме TouchManipulationMode. Вы можете использовать этот класс без изменений в собственных приложениях. Он определяет единственное свойство типа TouchManipulationMode.

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

Тем не менее, вы, вероятно, захотите избежать AnisotropicScale этого варианта. Это очень легко с этим параметром для управления растровым изображением, чтобы один из факторов масштабирования стал нулем. Это делает растровое изображение исчезать из зрения, никогда не возвращать. Если вам действительно нужна анисотропная масштабирование, вы хотите улучшить логику, чтобы избежать нежелательных результатов.

TouchManipulationManager использует векторы, но так как SKVector нет структуры в SkiaSharp, SKPoint вместо этого используется. SKPoint поддерживает оператор вычитания, а результат можно рассматривать как вектор. Единственной логикой, необходимой для добавления вектора, является вычисление Magnitude :

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

При каждом выборе поворота методы обработки с одним пальцем и двумя пальцами обрабатывают поворот первым. Если обнаружена любая смена, компонент поворота будет эффективно удален. То, что остается, интерпретируется как сдвиг и масштабирование.

OneFingerManipulate Вот метод. Если поворот с одним пальцем не включен, логика проста— она просто использует предыдущую точку и новую точку для создания вектора delta , соответствующего точно переводу. При включенном повороте с одним пальцем метод использует уголы с точки сводных точек (в центре растрового изображения) до предыдущей точки и новой точки для создания матрицы поворота:

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

В методе TwoFingerManipulate точка сводных данных — это позиция пальца, который не перемещается в этом конкретном событии касания. Поворот очень похож на поворот одним пальцем, а затем вектор с именем oldVector (на основе предыдущей точки) корректируется для поворота. Оставшееся движение интерпретируется как масштабирование:

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

Вы заметите, что в этом методе нет явного перевода. Однако оба MakeRotationMakeScale метода основаны на точке сводных данных и включают неявный перевод. Если вы используете два пальца на растровом рисунке и перетаскиваете их в одном направлении, TouchManipulation получите ряд событий касания, чередующихся между двумя пальцами. По мере того как каждый палец перемещается относительно другого, масштабирование или поворот результатов, но он отрицается движением другого пальца, и результатом является перевод.

Единственная оставшаяся часть страницы обработки сенсорного ввода является PaintSurface обработчиком TouchManipulationPage в файле программной части. Этот метод вызывает Paint метод TouchManipulationBitmap, который применяет матрицу, представляющую накопленные сенсорные действия:

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

Обработчик PaintSurface завершается отображением MatrixDisplay объекта, показывающего накапливаемую матрицу сенсорного ввода:

Тройной снимок экрана страницы

Управление несколькими растровыми картами

Одним из преимуществ изоляции кода сенсорной обработки в таких классах, как TouchManipulationBitmap и TouchManipulationManager возможность повторного использования этих классов в программе, которая позволяет пользователю управлять несколькими растровыми изображениями.

На странице точечного представления растрового изображения показано, как это делается. Вместо определения поля типа TouchManipulationBitmapBitmapScatterPage класс определяет List объекты растрового изображения:

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

Конструктор загружает все растровые изображения, доступные как внедренные ресурсы, и добавляет их в bitmapCollection. Обратите внимание, что Matrix свойство инициализировано для каждого объекта, поэтому верхние левого угла каждой TouchManipulationBitmap растровой карты смещаются на 100 пикселей.

Страница BitmapScatterView также должна обрабатывать события касания для нескольких растровых изображений. Вместо определения List идентификаторов сенсорных идентификаторов объектов, управляемых TouchManipulationBitmap в настоящее время, для этой программы требуется словарь:

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

Обратите внимание, как логика Pressed выполняется в обратном bitmapCollection направлении. Растровые изображения часто пересекаются друг с другом. Растровые рисунки позже в коллекции визуально лежат на вершине растровых изображений ранее в коллекции. Если под пальцем есть несколько растровых изображений, которые нажимают на экране, самый верхний должен быть тот, который управляется этим пальцем.

Кроме того, обратите внимание, что логика Pressed перемещает растровое изображение в конец коллекции, чтобы он визуально перемещается в верхнюю часть кучи других растровых изображений.

Moved В и Released событиях обработчик вызывает ProcessingTouchEvent метод TouchManipulationBitmap так же, TouchAction как и предыдущая программа.

Наконец, PaintSurface обработчик вызывает Paint метод каждого TouchManipulationBitmap объекта:

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

Код циклит по коллекции и отображает кучу растровых изображений с начала коллекции до конца:

Тройной снимок экрана страницы точечного представления растрового изображения

Масштабирование с одним пальцем

Для операции масштабирования обычно требуется жест сцепления с двумя пальцами. Однако можно реализовать масштабирование одним пальцем, переместив угол растрового изображения.

Это показано на странице масштабирования одного угла пальца. Так как в этом примере используется несколько другой тип масштабирования, чем реализованный в TouchManipulationManager классе, он не использует этот класс или TouchManipulationBitmap класс. Вместо этого все логики сенсорного ввода находится в файле программной части. Это несколько проще логики, чем обычно, потому что отслеживает только один палец за раз, и просто игнорирует все вторичные пальцы, которые могут касаться экрана.

Страница SingleFingerCornerScale.xaml создает экземпляр SKCanvasView класса и создает TouchEffect объект для отслеживания событий касания:

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

Файл SingleFingerCornerScalePage.xaml.cs загружает ресурс растрового изображения из каталога мультимедиа и отображает его с помощью объекта, определенного SKMatrix как поле:

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

Этот SKMatrix объект изменяется логикой сенсорного ввода, показанной ниже.

Оставшаяся часть файла кода находится в обработчике TouchEffect событий. Он начинается с преобразования текущего расположения пальца в SKPoint значение. Pressed Для типа действия обработчик проверка, что другой палец не касается экрана, и что палец находится в пределах растрового изображения.

Важной частью кода является if инструкция, включающая два вызова Math.Pow метода. Эта математика проверка, если расположение пальца находится за пределами многоточия, заполняющего растровое изображение. Если да, то это операция масштабирования. Пальцем находится рядом с одним из углов растрового изображения, а точка сводных точек определяется противоположным углом. Если палец находится внутри этого многоточия, это обычная операция сдвига:

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

Тип Moved действия вычисляет матрицу, соответствующую сенсорному действию с момента нажатия пальца на экран до этого времени. Он объединяет матрицу с матрицей в действии в то время, когда палец сначала нажимал растровое изображение. Операция масштабирования всегда относительно угла, противоположного тому, который касался пальца.

Для небольших или продолговатых растровых изображений многоточие может занять большую часть растрового изображения и оставить крошечные области в углах, чтобы масштабировать растровое изображение. Вы можете использовать несколько другой подход, в этом случае можно заменить весь if блок, который задает isScaling значение true с помощью этого кода:

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

Этот код эффективно делит область растрового изображения на внутреннюю фигуру бриллианта и четыре треугольника в углах. Это позволяет гораздо больше областей в углах захватывать и масштабировать растровое изображение.