触摸操作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. 单指拖放两个手指 pinch 等常见笔势可以移动和缩放对象,或甚至旋转。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.

如下所示的所有示例都使用一文中介绍的 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 文件实例化SKCanvasViewXamarin.Forms 中GridThe 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. 目标OnTouchEffectActionXAML 文件中设置的处理程序是更改以反映触摸操作矩阵值。The goal of the OnTouchEffectAction handler set in the XAML file is to alter the matrix value to reflect touch manipulations.

OnTouchEffectAction处理程序首先会将转换 Xamarin.FormsPoint值到 SkiaSharpSKPoint值。The OnTouchEffectAction handler begins by converting the Xamarin.Forms Point value into a SkiaSharp SKPoint value. 这是简单的基于进行缩放,只需WidthHeight的属性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字段设置为 touch ID,并保存的手指的位置。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

你想要两根手指触摸位图时执行的操作?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.

此程序与上一个程序之间的区别在于 Id 必须保存到多个触控。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操作几乎是相同的上一个不同之处在于程序 ID 和接触点添加到字典。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. ReleasedCancelled操作删除字典条目。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. 做到这一点通过将字典密钥复制到一个数组,然后将与要移动的手指的 ID 的第一个键进行比较。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. 名为两个向量oldVectornewVector都基于上一个和计算移动的手指在当前点和不旋转手指的透视点。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. 两个向量的相对大小现在是缩放比例。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.

如果你的应用程序需要实现触摸处理单个位图 (或其他对象),你可以调整三个示例中的代码为自己的应用程序。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. 但如果您需要实现触摸处理多个位图,您可能需要封装这些接触其他类中的操作。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

触摸操作页演示触摸操作的一个位图,但使用封装许多如上所示的逻辑的其他几个文件。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字段,但它不属于类型SKBitmapThe 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名为HitTestProcessTouchEvent: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. 从屏幕的手指提起之前,此 ID 表示这根手指的触控事件的顺序。This ID represents the sequence of touch events for that finger until the finger lifts from the screen. 如果多个手指触摸位图,则touchIds集合包含与每个手指的触控 ID。If multiple fingers touch the bitmap, then the touchIds collection contains a touch ID for each finger.

TouchAction处理程序还会调用ProcessTouchEvent类中TouchManipulationBitmapThe TouchAction handler also calls the ProcessTouchEvent class in TouchManipulationBitmap. 这就是某些 (而不是所有) 的真实触控进行处理。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类型的属性TouchManipulationManagerMatrix类型的属性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属性与现有的转换,位图关联并完成后还原画布: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;
    }
    ...
}

中的第二个公共方法TouchManipulationBitmapProcessTouchEventThe 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;
        }
    }
    ...
}

在中MovedReleased事件、 方法调用ManipulateIn the Moved and Released events, the method calls Manipulate. 在这些时间touchDictionary包含一个或多个TouchManipulationInfo对象。At these times, the touchDictionary contains one or more TouchManipulationInfo objects. 如果touchDictionary包含一个项,很可能该PreviousPointNewPoint值是否不相等,并表示一个手指的移动。If the touchDictionary contains one item, it is likely that the PreviousPoint and NewPoint values are unequal and represent the movement of a finger. 如果多个手指触摸位图,字典包含多个项,但只有其中一项具有不同PreviousPointNewPoint值。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. 所有其余部分具有相等PreviousPointNewPoint值。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. 在此调用时没有其他手指正在移动,和如果他们真的在迁移 (如有可能),将在未来调用处理这些动作ManipulateAt 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. 对于两个手指,它调用TwoFingerManipulateFor two fingers, it calls TwoFingerManipulate. 这些方法的参数是相同:prevPointnewPoint参数表示正在移动的手指。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 已通用化,并不使用除以外的任何其他文件TouchManipulationModeTouchManipulationManager 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 使用的矢量,但由于没有任何SKVectorSkiaSharp 中的结构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. 但是,这两个MakeRotationMakeScale方法基于透视点,并包括隐式转换。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.

唯一的剩余部分触摸操作页面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

如隔离类中的触摸处理代码的优势之一TouchManipulationBitmapTouchManipulationManager是能够重复使用这些类允许用户用来处理多个位图的程序中。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;
                }
            }
        }
    }
    ...
}

构造函数在所有可作为嵌入的资源,位图中加载,并将它们添加到bitmapCollectionThe constructor loads in all of the bitmaps available as embedded resources, and adds them to the bitmapCollection. 请注意,Matrix属性初始化每个TouchManipulationBitmap对象,因此每个位图的左上角 x 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触摸的 Id 的当前操作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.

在中MovedReleased事件,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.

对于小型或外型位图内部椭圆可能会占用大部分位图,并保留在各个角来缩放位图的小区域。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设置的块isScalingtrue使用以下代码: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.