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

Загрузить образец загрузить примерDownload Sample Download the sample

Используйте матрицы преобразования для реализации перетаскивания сенсорного ввода, как уменьшение и поворотаUse matrix transforms to implement touch dragging, pinching, and rotation

В мультисенсорной средах, таких как браузеры для мобильных устройств пользователи часто используют пальцами для управления объектами на экране.In multi-touch environments such as those on mobile devices, users often use their fingers to manipulate objects on the screen. Распространенные жестов, например перетаскивание один палец и двухпальцевый можно переместить и масштабирование объектов или даже поворота.Common gestures such as a one-finger drag and a two-finger pinch can move and scale objects, or even rotate them. Эти жесты обычно реализуются с помощью матрицы преобразования, а в этой статье показано, как это сделать.These gestures are generally implemented using transform matrices, and this article shows you how to do that.

Все приведенные ниже примеры использовать эффект touch отслеживания Xamarin.Forms, представленные в этой статье вызов события из эффекты.All the samples shown here use the Xamarin.Forms touch-tracking effect presented in the article Invoking Events from Effects.

Перетаскивание и преобразованияDragging and Translation

Одним из наиболее важных приложений матрицы преобразований — это обработка сенсорного ввода.One of the most important applications of matrix transforms is touch processing. Один SKMatrix значение можно объединить ряд операций сенсорного ввода.A single SKMatrix value can consolidate a series of touch operations.

Для перетаскивания касания одним пальцем, SKMatrix значение выполняет преобразование.For single-finger dragging, the SKMatrix value performs translation. Это показано в точечного рисунка, перетащив страницы.This is demonstrated in the Bitmap Dragging page. Файл XAML создает экземпляр SKCanvasView в Xamarin.Forms Grid.The XAML file instantiates an SKCanvasView in a Xamarin.Forms Grid. Объект TouchEffect был добавлен объект Effects коллекцию, Grid:A TouchEffect object has been added to the Effects collection of that 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, но это не работает на всех платформах.In theory, the TouchEffect object could be added directly to the Effects collection of the SKCanvasView, but that doesn't work on all platforms. Так как SKCanvasView имеет тот же размер, что Grid в этой конфигурации, подключив его к Grid работает точно так же.Because the SKCanvasView is the same size as the Grid in this configuration, attaching it to the Grid works just as well.

Файл с выделенным кодом загружает в ресурса точечного рисунка в своем конструкторе и отображает его в PaintSurface обработчика:The code-behind file loads in a bitmap resource in its constructor and displays it in the PaintSurface handler:

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 значение всегда равно выявление матрицы, и она не окажет никакого влияния на отображение растрового изображения.Without any further code, the SKMatrix value is always the identify matrix, and it would have no effect on the display of the bitmap. Цель OnTouchEffectAction обработчик, задать в файле XAML заключается в изменении значение матрицы в соответствии с манипуляции сенсорного ввода.The goal of the OnTouchEffectAction handler set in the XAML file is to alter the matrix value to reflect touch manipulations.

OnTouchEffectAction Обработчик начинает путем преобразования Xamarin.Forms Point значение в SkiaSharp SKPoint значение.The OnTouchEffectAction handler begins by converting the Xamarin.Forms Point value into a SkiaSharp SKPoint value. Это сводится к простой масштабирования на основе Width и Height свойства SKCanvasView (которые являются аппаратно независимые единицы) и CanvasSize свойство, которое измеряется в пикселях:This is a simple matter of scaling based on the Width and Height properties of SKCanvasView (which are device-independent units) and the CanvasSize property, which is in units of 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;
        }
    }
    ···
}

Если сначала касании пальцем экрана, событие типа TouchActionType.Pressed запускается.When a finger first touches the screen, an event of type TouchActionType.Pressed is fired. Первым делом следует определить, если палец касается растрового изображения.The first task is to determine if the finger is touching the bitmap. Такой задачи часто называют попадания.Such a task is often called hit-testing. В этом случае попадания можно сделать, создав SKRect значение, соответствующее точечного рисунка, применением преобразования матрицы к нему с помощью MapRect, а затем решают, если точка касания находится внутри преобразованный прямоугольник.In this case, hit-testing can be accomplished by creating an SKRect value corresponding to the bitmap, applying the matrix transform to it with MapRect, and then determining if the touch point is inside the transformed rectangle.

Если это так, то touchId полю присваивается идентификатор сенсорного ввода, а положение пальца сохраняется.If that is the case, then the touchId field is set to the touch ID, and the finger position is saved.

Для TouchActionType.Moved событие, коэффициенты преобразования из SKMatrix значение корректируются на основе текущей позиции палец и новое положение пальца.For the TouchActionType.Moved event, the translation factors of the SKMatrix value are adjusted based on the current position of the finger, and the new position of the finger. Сохранение в следующий раз, новая позиция и SKCanvasView становится недействительным.That new position is saved for the next time through, and the SKCanvasView is invalidated.

Во время экспериментов с этой программой, обратите внимание, можно только перетащить точечный рисунок, когда палец касается области, где отображается растрового изображения.As you experiment with this program, take note that you can only drag the bitmap when your finger touches an area where the bitmap is displayed. Несмотря на то, что это ограничение не очень важно для этой программы, он становится ключевым, при обработке нескольких точечных рисунков.Although that restriction is not very important for this program, it becomes crucial when manipulating multiple bitmaps.

Сдвиги и масштабированиеPinching and Scaling

Выберите произойти при двух пальцев touch растрового изображения.What do you want to happen when two fingers touch the bitmap? Если двумя пальцами переместить в параллельном режиме, как правило требуется растрового изображения для перемещения, а также пальцы.If the two fingers move in parallel, then you probably want the bitmap to move along with the fingers. Если двумя пальцами выполните жест сжатия или растяжения операции, вы можете растровое изображение, чтобы вращать (об этом будет рассказано в следующем разделе) или масштабировать.If the two fingers perform a pinch or stretch operation, then you might want the bitmap to be rotated (to be discussed in the next section) or scaled. При масштабировании растрового изображения, имеет смысл большинства двумя пальцами в тех же позициях относительно точечный рисунок и точечный рисунок, который соответствующим образом масштабировать.When scaling a bitmap, it makes most sense for the two fingers to remain in the same positions relative to the bitmap, and for the bitmap to be scaled accordingly.

Обработка двух пальцев одновременно кажется сложным, но имейте в виду, TouchAction обработчик получает только сведения о одним пальцем за раз.Handling two fingers at once seems complicated, but keep in mind that the TouchAction handler only receives information about one finger at a time. Если двумя пальцами растрового изображения, для каждого события одним пальцем изменилось положение, но другой не был изменен.If two fingers are manipulating the bitmap, then for each event, one finger has changed position but the other has not changed. В масштабирование растрового изображения ниже код страницы, называется пальцу, который не был изменен позиции pivot точки, поскольку преобразование является относительно этой точки.In the Bitmap Scaling page code below, the finger that has not changed position is called the pivot point because the transform is relative to that point.

Одно различие между этой программы и предыдущей программы является, несколько штрих, который необходимо сохранить идентификаторы.One difference between this program and the previous program is that multiple touch IDs must be saved. Словарь, используемый для этой цели, где touch ID является ключом словаря, а значение словаря — текущее положение этого палец:A dictionary is used for this purpose, where the touch ID is the dictionary key and the dictionary value is the current position of that finger:

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 действии почти так же, как предыдущий за исключением случаев, идентификатор программы и точек соприкосновения добавляются в словарь.The handling of the Pressed action is almost the same as the previous program except that the ID and touch point are added to the dictionary. Released И Cancelled действия удалите записи словаря.The Released and Cancelled actions remove the dictionary entry.

Обработка Moved действие более сложен, однако.The handling for the Moved action is more complex, however. Если только одним пальцем требуется, то обработка будет очень похоже на предыдущей программы.If there's only one finger involved, then the processing is very much the same as the previous program. Для двух или более пальцами программа должна также получать информацию из словаря, включающие пальцу, который не перемещается.For two or more fingers, the program must also obtain information from the dictionary involving the finger that is not moving. Это достигается путем копирования ключи словаря в массив, а затем сравнивать первый ключ с Идентификатором перемещаемого палец.It does this by copying the dictionary keys into an array and then comparing the first key with the ID of the finger being moved. Который позволяет программе для получения точки вращения, соответствующий пальцу, который не перемещается.That allows the program to obtain the pivot point corresponding to the finger that is not moving.

Затем вычисляется двумя векторами новое положение пальца относительно точки вращения и старое положение пальца относительно точки вращения.Next, the program calculates two vectors of the new finger position relative to the pivot point, and the old finger position relative to the pivot point. Соотношение эти векторы масштабирования факторов.The ratios of these vectors are scaling factors. Так как деление на ноль вероятность того, их необходимо проверить на практически неограниченные значения или значения NaN (не число).Because division by zero is a possibility, these must be checked for infinite values or NaN (not a number) values. Если все хорошо, преобразование масштабирования объединяется с SKMatrix значение, сохраненное как поле.If all is well, a scaling transform is concatenated with the SKMatrix value saved as a field.

Во время экспериментов с этой страницей, вы заметите, что можно перетаскивать растровое изображение с одним или двумя пальцами, или его масштабирования с двумя пальцами.As you experiment with this page, you'll notice that you can drag the bitmap with one or two fingers, or scale it with two fingers. Масштабирование является анизотропная, означающее, что масштабирование может быть другим в горизонтальном и вертикальном направлениях.The scaling is anisotropic, which means that the scaling can be different in the horizontal and vertical directions. Это искажает пропорции, но также позволяет перевернуть осуществлять Зеркальное изображение точечного рисунка.This distorts the aspect ratio, but also allows you to flip the bitmap to make a mirror image. Также может оказаться, что можно сжать точечный рисунок с измерением ноль, и он исчезнет.You might also discover that you can shrink the bitmap to a zero dimension, and it disappears. В рабочем коде необходимо защититься от этого.In production code, you'll want to guard against this.

Поворот двумя пальцамиTwo-finger rotation

Поворот растрового изображения страница дает возможность использовать два пальца для поворота и масштабирования мощности.The Bitmap Rotate page allows you to use two fingers for either rotation or isotropic scaling. Точечный рисунок всегда сохраняет его правильных пропорций.The bitmap always retains its correct aspect ratio. С помощью двух пальцев, поворот и масштабирование анизотропная работает очень хорошо так, как перемещаются между пальцами очень похож на обе задачи.Using two fingers for both rotation and anisotropic scaling does not work very well because the movement of the fingers is very similar for both tasks.

Первый разница в этой программе является логику проверки нажатия.The first big difference in this program is the hit-testing logic. Предыдущей программы, использующие Contains метод SKRect для определения того, является ли сенсорной точки в преобразованный прямоугольник, соответствующий растрового изображения.The previous programs used the Contains method of SKRect to determine if the touch point is within the transformed rectangle that corresponds to the bitmap. Но как пользователь управляет точечного рисунка, растровое изображение может быть повернут, и SKRect не может представлять должным образом поворачивается прямоугольник.But as the user manipulates the bitmap, the bitmap might be rotated, and SKRect cannot properly represent a rotated rectangle. Может опасения, что логика проверки нажатия должен реализовывать довольно сложной аналитическую геометрию в этом случае.You might fear that the hit-testing logic needs to implement rather complex analytic geometry in that case.

Тем не менее ярлык доступен: Определение, находится точка в пределах преобразованный прямоугольник совпадает со значением ли обратная преобразованная точка находится внутри границы подвергнутый прямоугольника.However, a shortcut is available: Determining if a point lies within the boundaries of a transformed rectangle is the same as determining if an inverse transformed point lies within the boundaries of the untransformed rectangle. Это гораздо проще, вычисление и логику можно продолжать использовать удобный Contains метод:That's a much easier calculation, and the logic can continue to use the convenient Contains method:

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 событий начинается, как Предыдущая программа.The logic for the Moved event starts out like the previous program. Два вектора с именем oldVector и newVector вычисляются на основании предыдущей и текущей точкой движущийся палец и вращения unmoving палец.Two vectors named oldVector and newVector are calculated based on the previous and the current point of the moving finger and the pivot point of the unmoving finger. Но затем определяются углов эти векторы, и разница в угол поворота.But then angles of these vectors are determined, and the difference is the rotation angle.

Масштабирование может также принимать участие, чтобы старые векторные поворачивается на основе угол поворота.Scaling might also be involved, so the old vector is rotated based on the rotation angle. Относительный magnitude двух векторов выпущена коэффициент масштабирования.The relative magnitude of the two vectors is now the scaling factor. Обратите внимание, что же scale значение будет использоваться для горизонтального и вертикального масштабирования, масштабирование мощности.Notice that the same scale value is used for horizontal and vertical scaling so that scaling is isotropic. matrix Поля в соответствии с матрица поворота и масштабирования матрицы.The matrix field is adjusted by both the rotation matrix and a scale matrix.

Если приложению требуется реализовать touch обработки для отдельной (или другого объекта), вы можете адаптировать код из этих трех примерах для своих собственных приложений.If your application needs to implement touch processing for a single bitmap (or other object), you can adapt the code from these three samples for your own application. Но если необходимо реализовать сенсорного ввода, обработки для нескольких растровых изображений, возможно, будет необходимо инкапсулировать эти touch операции в других классах.But if you need to implement touch processing for multiple bitmaps, you'll probably want to encapsulate these touch operations in other classes.

Инкапсуляция операции сенсорного вводаEncapsulating the Touch Operations

Touch манипуляции страницы демонстрируется обработка touch одном точечном рисунке, но с использованием несколько файлов, которые инкапсулируют большую часть логики, показанный выше.The Touch Manipulation page demonstrates the touch manipulation of a single bitmap, but using several other files that encapsulate much of the logic shown above. Первый из них — TouchManipulationMode перечисления, который указывает различные типы реализации с помощью кода, будут встречаться манипуляции сенсорного ввода:The first of these files is the TouchManipulationMode enumeration, which indicates the different types of touch manipulation implemented by the code you'll be seeing:

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

PanOnly — Это один палец перетаскивания, который реализуется с помощью преобразования.PanOnly is a one-finger drag that is implemented with translation. Все последующие параметры также включают панорамирование, но включают в себя два пальца: IsotropicScale является операцией жестом сжатия, которая приводит объект масштабирование одинаково в горизонтальном и вертикальном направлениях.All the subsequent options also include panning but involve two fingers: IsotropicScale is a pinch operation that results in the object scaling equally in the horizontal and vertical directions. AnisotropicScale позволяет добавлять элементы не равны.AnisotropicScale allows unequal scaling.

ScaleRotate Параметр предназначен для двух пальцев масштабировании и повороте.The ScaleRotate option is for two-finger scaling and rotation. Масштабирование — это мощности.Scaling is isotropic. Как упоминалось ранее, представляет проблему реализации двух пальцев поворота анизотропная масштабирования, поскольку перемещений палец по сути одинаковые.As mentioned earlier, implementing two-finger rotation with anisotropic scaling is problematic because the finger movements are essentially the same.

ScaleDualRotate Параметр добавляет один пальцем.The ScaleDualRotate option adds one-finger rotation. Когда одним пальцем перетаскивает объект, перетаскиваемый объект сначала поворачивается вокруг его центра, так, чтобы центр объекта с помощью перетаскивания вектора.When a single finger drags the object, the dragged object is first rotated around its center so that the center of the object lines up with the dragging vector.

TouchManipulationPage.xaml файл включает Picker с элементами TouchManipulationMode перечисления:The TouchManipulationPage.xaml file includes a Picker with the members of the TouchManipulationMode enumeration:

<?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 , в котором он.Towards the bottom is an SKCanvasView and a TouchEffect attached to the single-cell Grid that encloses it.

TouchManipulationPage.xaml.cs имеет файл с выделенным кодом bitmap поле, но он не относится к типу SKBitmap.The TouchManipulationPage.xaml.cs code-behind file has a bitmap field but it is not of type SKBitmap. Тип — TouchManipulationBitmap (класс вскоре вы увидите):The type is TouchManipulationBitmap (a class you'll see shortly):

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 получен из внедренного ресурса.The constructor instantiates a TouchManipulationBitmap object, passing to the constructor an SKBitmap obtained from an embedded resource. Конструктор завершает, задав Mode свойство TouchManager свойство TouchManipulationBitmap , который является членом TouchManipulationMode перечисления.The constructor concludes by setting the Mode property of the TouchManager property of the TouchManipulationBitmap object to a member of the TouchManipulationMode enumeration.

SelectedIndexChanged Обработчик для Picker также задает это Mode свойство:The SelectedIndexChanged handler for the Picker also sets this Mode property:

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:The TouchAction handler of the TouchEffect instantiated in the XAML file calls two methods in TouchManipulationBitmap named HitTest and 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 — это означает, что палец коснулось экране в пределах области, растровое изображение — touch ID добавляется к TouchIds коллекции.If the HitTest method returns true — meaning that a finger has touched the screen within the area occupied by the bitmap — then the touch ID is added to the TouchIds collection. Этот идентификатор представляет последовательность событий сенсорного ввода для этого палец, пока не поднимает палец с экрана.This ID represents the sequence of touch events for that finger until the finger lifts from the screen. Если нескольких пальцев touch точечного рисунка, а затем touchIds touch ID для каждого пальца в коллекцию.If multiple fingers touch the bitmap, then the touchIds collection contains a touch ID for each finger.

TouchAction Также вызывает обработчик ProcessTouchEvent в класс TouchManipulationBitmap.The TouchAction handler also calls the ProcessTouchEvent class in TouchManipulationBitmap. Именно здесь некоторые (но не все) из реальных touch происходит обработка.This is where some (but not all) of the real touch processing occurs.

TouchManipulationBitmap Класс является оболочкой для класса для SKBitmap , содержащий код для визуализации точечного рисунка и обработки событий сенсорного ввода.The TouchManipulationBitmap class is a wrapper class for SKBitmap that contains code to render the bitmap and process touch events. Работает в сочетании с более обобщенного кода в TouchManipulationManager класс (который мы рассмотрим чуть ниже).It works in conjunction with more generalized code in a TouchManipulationManager class (which you'll see shortly).

TouchManipulationBitmap Конструктор сохраняет SKBitmap и создает два свойства TouchManager свойство типа TouchManipulationManager и Matrix свойство типа SKMatrix:The TouchManipulationBitmap constructor saves the SKBitmap and instantiates two properties, the TouchManager property of type TouchManipulationManager and the Matrix property of 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; }
    ...
}

Это Matrix свойство является накопленные преобразования, полученный в результате все действия сенсорного ввода.This Matrix property is the accumulated transform resulting from all the touch activity. Как можно будет увидеть, каждое событие сенсорного ввода разрешается в матрицу, которое затем объединяется с SKMatrix значение, хранящееся в Matrix свойство.As you'll see, each touch event is resolved into a matrix, which is then concatenated with the SKMatrix value stored by the Matrix property.

TouchManipulationBitmap Объект рисует себя в его Paint метод.The TouchManipulationBitmap object draws itself in its Paint method. Аргумент является SKCanvas объекта.The argument is an SKCanvas object. Это SKCanvas могут уже иметь преобразованием, примененным к нему, поэтому Paint метод Сцепляет Matrix свойство связанные с растровым изображением, к преобразованию существующих и восстанавливает canvas, после завершения:This SKCanvas might already have a transform applied to it, so the Paint method concatenates the Matrix property associated with the bitmap to the existing transform, and restores the canvas when it has finished:

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 Если пользователь касается экрана в момент времени в пределах границ растрового изображения.The HitTest method returns true if the user touches the screen at a point within the boundaries of the bitmap. Это используется логика, показанного на поворот растрового изображения страницы:This uses the logic shown earlier in the Bitmap Rotation page:

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

Второй открытый метод в TouchManipulationBitmap является ProcessTouchEvent.The second public method in TouchManipulationBitmap is ProcessTouchEvent. При вызове этого метода, он уже установлено событие сенсорного ввода принадлежит определенной точечного рисунка.When this method is called, it has already been established that the touch event belongs to this particular bitmap. Метод ведение словаря TouchManipulationInfo объекты, которые просто предыдущей точки и новая точка каждого пальца:The method maintains a dictionary of TouchManipulationInfo objects, which is simply the previous point and the new point of each finger:

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

    public SKPoint NewPoint { set; get; }
}

Здесь представлен словарь и ProcessTouchEvent сам метод:Here's the dictionary and the ProcessTouchEvent method itself:

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.In the Moved and Released events, the method calls Manipulate. В таких случаях touchDictionary содержит один или несколько TouchManipulationInfo объектов.At these times, the touchDictionary contains one or more TouchManipulationInfo objects. Если touchDictionary содержит один элемент, вполне вероятно, PreviousPoint и NewPoint значения не равны и представляют перемещение пальца.If the touchDictionary contains one item, it is likely that the PreviousPoint and NewPoint values are unequal and represent the movement of a finger. Если несколько пальца касаются растрового изображения, словарь содержит более одного элемента, но только один из этих элементов имеет другие PreviousPoint и NewPoint значения.If multiple fingers are touching the bitmap, the dictionary contains more than one item, but only one of these items has different PreviousPoint and NewPoint values. Все остальные, имеют PreviousPoint и NewPoint значения.All the rest have equal PreviousPoint and NewPoint values.

Это важно: Manipulate Метод можно предположить, что он обрабатывает перемещение только одним пальцем.This is important: The Manipulate method can assume that it's processing the movement of only one finger. Во время этого вызова другие пальцы нет скользящее, а если они действительно при перемещении (как, вероятнее всего), в будущих вызовах будет обрабатываться перемещения Manipulate.At the time of this call none of the other fingers are moving, and if they really are moving (as is likely), those movements will be processed in future calls to Manipulate.

Manipulate Метод сначала копирует словаря в массив для удобства.The Manipulate method first copies the dictionary to an array for convenience. Игнорирует ничего, кроме первых двух записей.It ignores anything other than the first two entries. Если более чем двумя пальцами пытаетесь управлять точечного рисунка, остальные игнорируются.If more than two fingers are attempting to manipulate the bitmap, the others are ignored. Manipulate Окончательный членом TouchManipulationBitmap:Manipulate is the final member of 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 объекта.If one finger is manipulating the bitmap, Manipulate calls the OneFingerManipulate method of the TouchManipulationManager object. Двумя пальцами, она вызывает TwoFingerManipulate.For two fingers, it calls TwoFingerManipulate. Аргументы для этих методов совпадают: prevPoint и newPoint аргументы представляют палец, которые перемещаются.The arguments to these methods are the same: the prevPoint and newPoint arguments represent the finger that is moving. Но pivotPoint аргумента отличается для двух вызовов:But the pivotPoint argument is different for the two calls:

Для манипуляции один палец pivotPoint представляет собой центр растрового изображения.For one-finger manipulation, the pivotPoint is the center of the bitmap. Это позволяет обеспечить для поворота один палец.This is to allow for one-finger rotation. Для манипуляции двумя пальцами, данное событие обозначает, перемещение только одним пальцем, чтобы pivotPoint является пальцу, который не перемещается.For two-finger manipulation, the event indicates the movement of only one finger, so that the pivotPoint is the finger that is not moving.

В обоих случаях TouchManipulationManager возвращает SKMatrix значение, которое объединяет метод с текущим Matrix свойство, TouchManipulationPage использует для визуализации точечного рисунка.In both cases, TouchManipulationManager returns an SKMatrix value, which the method concatenates with the current Matrix property that TouchManipulationPage uses to render the bitmap.

TouchManipulationManager обобщается и использует никаких других файлов, за исключением TouchManipulationMode.TouchManipulationManager is generalized and uses no other files except TouchManipulationMode. Возможно, вы сможете использовать этот класс без изменений в собственных приложениях.You might be able to use this class without change in your own applications. Он определяет единственное свойство типа TouchManipulationMode:It defines a single property of type TouchManipulationMode:

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

Тем не менее, возможно, стоит во избежание AnisotropicScale параметр.However, you'll probably want to avoid the AnisotropicScale option. Это очень просто с этот параметр, чтобы управлять растровое изображение, чтобы один из коэффициенты масштабирования становится равным нулю.It's very easy with this option to manipulate the bitmap so that one of the scaling factors becomes zero. В результате исчезают, никогда не для возврата растрового изображения.That makes the bitmap disappear from sight, never to return. Если вам действительно нужно анизотропная масштабирование, будет необходимо расширить логику, чтобы избежать нежелательных результатов.If you truly do need anisotropic scaling, you'll want to enhance the logic to avoid undesirable outcomes.

TouchManipulationManager использует векторов, но так как она не SKVector структуры в SkiaSharp, SKPoint вместо него используется.TouchManipulationManager makes use of vectors, but since there is no SKVector structure in SkiaSharp, SKPoint is used instead. SKPoint поддерживает оператор вычитания, а результат можно рассматривать как вектор.SKPoint supports the subtraction operator, and the result can be treated as a vector. Логика только конкретных вектора, необходимо было добавить Magnitude вычисления:The only vector-specific logic that needed to be added is a Magnitude calculation:

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

Каждый раз, когда был выбран поворота, оба метода один палец и двух пальцев обработку сначала обработайте поворот.Whenever rotation has been selected, both the one-finger and two-finger manipulation methods handle the rotation first. При обнаружении любого поворота координату поворота фактически удаляется.If any rotation is detected, then the rotation component is effectively removed. Что же сохраняется, интерпретируется как панорамирования и масштабирования.What remains is interpreted as panning and scaling.

Вот OneFingerManipulate метод.Here's the OneFingerManipulate method. Если один пальцем не была включена, то логика проста — он просто использует предыдущей точки и новой точки для построения вектора с именем delta , точно соответствует перевода.If one-finger rotation has not been enabled, then the logic is simple — it simply uses the previous point and new point to construct a vector named delta that corresponds precisely to translation. С поворотом один палец включен метод использует углы от точки вращения (центр растрового изображения) к предыдущей точки и новую точку для создания матрицы поворота:With one-finger rotation enabled, the method uses angles from the pivot point (the center of the bitmap) to the previous point and new point to construct a rotation matrix:

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 , вращения является положение пальца, не входящего в этом событии специфический сенсорный.In the TwoFingerManipulate method, the pivot point is the position of the finger that's not moving in this particular touch event. Поворот очень похожа на один палец поворот, и затем именованного вектора oldVector (в зависимости от предыдущей точки) настраивается для поворота.The rotation is very similar to the one-finger rotation, and then the vector named oldVector (based on the previous point) is adjusted for the rotation. Оставшиеся перемещение интерпретируется как масштабирование:The remaining movement is interpreted as scaling:

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

Вы заметите, что имеется без явного преобразования в этом методе.You'll notice there is no explicit translation in this method. Тем не менее, обе MakeRotation и MakeScale методы зависят от точки вращения и включает неявные преобразования.However, both the MakeRotation and MakeScale methods are based on the pivot point, and that includes implicit translation. Если вы используете два пальца на растровое изображение и перетащить их в одном направлении, TouchManipulation получат последовательность событий сенсорного ввода, переключаясь между двумя пальцами.If you're using two fingers on the bitmap and dragging them in the same direction, TouchManipulation will get a series of touch events alternating between the two fingers. Как и каждый палец движется по отношению к другим, масштабирования или поворота результаты, однако он сделал движение других пальцев и результат преобразования.As each finger moves relative to the other, scaling or rotation results, but it's negated by the other finger's movement, and the result is translation.

Только оставшуюся часть Touch манипуляции страница PaintSurface обработчик в TouchManipulationPage файл с выделенным кодом.The only remaining part of the Touch Manipulation page is the PaintSurface handler in the TouchManipulationPage code-behind file. Это вызывает Paint метод TouchManipulationBitmap, которому применяется матрица, представляющая действие накопленные сенсорного ввода:This calls the Paint method of the TouchManipulationBitmap, which applies the matrix representing the accumulated touch activity:

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 объекта отображаются в матрице накопленные сенсорного ввода:The PaintSurface handler concludes by displaying a MatrixDisplay object showing the accumulated touch matrix:

Управление несколькими точечных рисунковManipulating Multiple Bitmaps

Одно из преимуществ изоляции код для обработки сенсорного ввода в классах, таких как TouchManipulationBitmap и TouchManipulationManager является возможность повторного использования этих классов в программе, которая позволяет пользователю управлять несколько точечных рисунков.One of the advantages of isolating touch-processing code in classes such as TouchManipulationBitmap and TouchManipulationManager is the ability to reuse these classes in a program that allows the user to manipulate multiple bitmaps.

Представление точечной диаграммы точечного рисунка странице рассказывается, как это делается.The Bitmap Scatter View page demonstrates how this is done. Вместо того чтобы поле типа TouchManipulationBitmap, BitmapScatterPage класс определяет List растрового изображения объектов:Rather than defining a field of type TouchManipulationBitmap, the BitmapScatterPage class defines a List of bitmap objects:

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.The constructor loads in all of the bitmaps available as embedded resources, and adds them to the bitmapCollection. Обратите внимание, что Matrix свойство инициализируется на каждом TouchManipulationBitmap объекта, поэтому смещения верхнего левого угла каждого растрового изображения на 100 пикселей.Notice that the Matrix property is initialized on each TouchManipulationBitmap object, so the upper-left corners of each bitmap are offset by 100 pixels.

BitmapScatterView Страницы также необходимо обрабатывать события касания для нескольких растровых изображений.The BitmapScatterView page also needs to handle touch events for multiple bitmaps. Вместо того чтобы List точки касания управлять идентификаторы TouchManipulationBitmap этой программы требуется словарь объектов:Rather than defining a List of touch IDs of currently manipulated TouchManipulationBitmap objects, this program requires a dictionary:

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 в обратном порядке.Notice how the Pressed logic loops through the bitmapCollection in reverse. Точечные рисунки часто перекрываются друг с другом.The bitmaps often overlap each other. Точечные рисунки более поздней версии в коллекции визуально лежат одна на растровые изображения в коллекции.The bitmaps later in the collection visually lie on top of the bitmaps earlier in the collection. При наличии нескольких точечные рисунки в разделе палец, нажимает клавишу на экране, верхний должен быть тем, которое управляется этой палец.If there are multiple bitmaps under the finger that presses on the screen, the topmost one must be the one that is manipulated by that finger.

Также Обратите внимание, что Pressed помещает это растровое изображение в конец коллекции, чтобы он перемещался визуально в верхнюю часть накопление других точечных рисунков.Also notice that the Pressed logic moves that bitmap to the end of the collection so that it visually moves to the top of the pile of other bitmaps.

В Moved и Released события, TouchAction вызовов обработчика ProcessingTouchEvent метод в TouchManipulationBitmap так же, как более ранних программы.In the Moved and Released events, the TouchAction handler calls the ProcessingTouchEvent method in TouchManipulationBitmap just like the earlier program.

Наконец PaintSurface вызовов обработчика Paint метод каждого TouchManipulationBitmap объекта:Finally, the PaintSurface handler calls the Paint method of each TouchManipulationBitmap object:

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

Код выполняет цикл по коллекции и отображает накопление точечных рисунков в начале коллекции в конец:The code loops through the collection and displays the pile of bitmaps from the beginning of the collection to the end:

Масштабирование касания одним пальцемSingle-Finger Scaling

Выполняется операция масштабирования обычно требует жест жестом сжатия, с помощью двух пальцев.A scaling operation generally requires a pinch gesture using two fingers. Тем не менее это можно реализовать, масштабирование с одним пальцем благодаря наличию палец переместить углов растрового изображения.However, it's possible to implement scaling with a single finger by having the finger move the corners of a bitmap.

Это показано в одном масштабируемом углу палец страницы.This is demonstrated in the Single Finger Corner Scale page. Так как в этом примере используется несколько другой тип масштабирования, чем, реализованы в TouchManipulationManager класса, она не использует этот класс или TouchManipulationBitmap класса.Because this sample uses a somewhat different type of scaling than that implemented in the TouchManipulationManager class, it does not use that class or the TouchManipulationBitmap class. Вместо этого вся логика сенсорного ввода находится в файле кода.Instead, all the touch logic is in the code-behind file. Это довольно простой логики, чем обычно, так как он отслеживает только одним пальцем одновременно и просто игнорирует любой дополнительный пальцами, которые могут прикосновения к экрану.This is somewhat simpler logic than usual because it tracks only one finger at a time, and simply ignores any secondary fingers that might be touching the screen.

SingleFingerCornerScale.xaml создает страницу SKCanvasView класса и создает TouchEffect для отслеживания событий сенсорного экрана:The SingleFingerCornerScale.xaml page instantiates the SKCanvasView class and creates a TouchEffect object for tracking touch events:

<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 объект, определенный как поле:The SingleFingerCornerScalePage.xaml.cs file loads a bitmap resource from the Media directory and displays it using an SKMatrix object defined as a field:

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 объект изменяется с помощью сенсорного ввода логики, показано ниже.This SKMatrix object is modified by the touch logic shown below.

Остальная часть файла кода является TouchEffect обработчик событий.The remainder of the code-behind file is the TouchEffect event handler. Он начинает путем преобразования в настоящий момент палец для SKPoint значение.It begins by converting the current location of the finger to an SKPoint value. Для Pressed тип действия, обработчик проверяет, что нет другой палец касается экрана, и что палец находится в границах растрового изображения.For the Pressed action type, the handler checks that no other finger is touching the screen, and that the finger is within the bounds of the bitmap.

Является ключевой частью кода if инструкции, относящейся к два вызова Math.Pow метод.The crucial part of the code is an if statement involving two calls to the Math.Pow method. Это математические проверяет, является ли расположение палец за пределы эллипса, который заполняет растрового изображения.This math checks if the finger location is outside of an ellipse that fills the bitmap. Если Да, то это операции масштабирования.If so, then that's a scaling operation. Если палец находится рядом с одним из углов растрового изображения, а точки вращения определяется, что это противоположного угла.The finger is near one of the corners of the bitmap, and a pivot point is determined that is the opposite corner. Если точка касания находится в пределах этого эллипса, это регулярное панорамирования операции:If the finger is within this ellipse, it's a regular panning operation:

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 Тип действия вычисляет матрицу, соответствующее действие касания с момента палец нажата экрана вплоть до этого времени.The Moved action type calculates a matrix corresponding to the touch activity from the time the finger pressed the screen up to this time. Он объединяет данную матрицу с матрицы в силе во время палец первого нажатия растрового изображения.It concatenates that matrix with the matrix in effect at the time the finger first pressed the bitmap. Операции масштабирования всегда является относительно угла отличие, затронутых палец.The scaling operation is always relative to the corner opposite to the one that the finger touched.

Для небольших или прямоугольный растровых изображений эллипса внутренних может занимают большую часть точечного рисунка и оставьте tiny областей в углах масштабирование растрового изображения.For small or oblong bitmaps, an interior ellipse might occupy most of the bitmap and leave tiny areas at the corners to scale the bitmap. Может потребоваться, чтобы немного другой подход, в этом случае вы можете заменить этот всего if блок, который задает isScaling для true следующим кодом:You might prefer a somewhat different approach, in which case you can replace that entire if block that sets isScaling to true with this code:

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

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

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

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

Этот код фактически делит области растрового изображения в внутренних ромбовидной фигуры и четыре треугольника в углах.This code effectively divides the area of the bitmap into an interior diamond shape and four triangles at the corners. Это позволяет гораздо больших областях в углах взять и масштабирование растрового изображения.This allows much larger areas at the corners to grab and scale the bitmap.