다음을 통해 공유


터치 조작

행렬 변환을 사용하여 터치 끌기, 꼬집기 및 회전 구현

모바일 디바이스와 같은 멀티 터치 환경에서 사용자는 종종 손가락을 사용하여 화면에서 개체를 조작합니다. 한 손가락 끌기 및 두 손가락 손가락 손가락 모으기와 같은 일반적인 제스처는 개체를 이동하고 크기를 조정하거나 회전할 수도 있습니다. 이러한 제스처는 일반적으로 변환 행렬을 사용하여 구현되며, 이 문서에서는 이 작업을 수행하는 방법을 보여 줍니다.

변환, 크기 조정 및 회전이 적용되는 비트맵

여기에 표시된 모든 샘플은 효과에서 Xamarin.Forms 이벤트 호출 문서에 제시된 터치 추적 효과를 사용합니다.

끌기 및 번역

매트릭스 변환의 가장 중요한 애플리케이션 중 하나는 터치 처리입니다. 단일 SKMatrix 값은 일련의 터치 작업을 통합할 수 있습니다.

한 손가락 끌기의 경우 값이 SKMatrix 변환을 수행합니다. 비트맵 끌기 페이지에서 설명합니다. XAML 파일은 에 있는 Xamarin.FormsGrid파일을 SKCanvasView 인스턴스화합니다. TouchEffect 개체가 해당 Grid컬렉션에 Effects 추가되었습니다.

<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 컬렉션SKCanvasViewEffects 직접 추가할 수 있지만 모든 플랫폼에서 작동하지는 않습니다. SKCanvasView 이 구성의 크기와 크기 Grid 가 같으므로 작업에도 연결합니다Grid.

코드 숨김 파일은 해당 생성자의 비트맵 리소스에 로드되고 처리기에 표시됩니다 PaintSurface .

public partial class BitmapDraggingPage : ContentPage
{
    // Bitmap and matrix for display
    SKBitmap bitmap;
    SKMatrix matrix = SKMatrix.MakeIdentity();
    ···

    public BitmapDraggingPage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            bitmap = SKBitmap.Decode(stream);
        }
    }
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Display the bitmap
        canvas.SetMatrix(matrix);
        canvas.DrawBitmap(bitmap, new SKPoint());
    }
}

추가 코드 SKMatrix 가 없으면 값은 항상 식별 행렬이며 비트맵 표시에는 영향을 주지 않습니다. XAML 파일에 설정된 처리기의 목표는 OnTouchEffectAction 터치 조작을 반영하도록 행렬 값을 변경하는 것입니다.

처리기는 OnTouchEffectAction 값을 SkiaSharp SKPoint 값으로 변환하여 Xamarin.FormsPoint 시작합니다. 이는 (디바이스 독립적 단위) 및 Height 속성(픽셀 단위)의 SKCanvasView 속성에 CanvasSize 따라 Width 크기를 조정하는 간단한 문제입니다.

public partial class BitmapDraggingPage : ContentPage
{
    ···
    // Touch information
    long touchId = -1;
    SKPoint previousPoint;
    ···
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point = 
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                // Find transformed bitmap rectangle
                SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
                rect = matrix.MapRect(rect);

                // Determine if the touch was within that rectangle
                if (rect.Contains(point))
                {
                    touchId = args.Id;
                    previousPoint = point;
                }
                break;

            case TouchActionType.Moved:
                if (touchId == args.Id)
                {
                    // Adjust the matrix for the new position
                    matrix.TransX += point.X - previousPoint.X;
                    matrix.TransY += point.Y - previousPoint.Y;
                    previousPoint = point;
                    canvasView.InvalidateSurface();
                }
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                touchId = -1;
                break;
        }
    }
    ···
}

손가락이 화면을 처음 터치하면 형식 TouchActionType.Pressed 의 이벤트가 발생합니다. 첫 번째 작업은 손가락이 비트맵을 터치하는지 여부를 확인하는 것입니다. 이러한 작업을 적중 테스트라고도 합니다. 이 경우 비트맵에 해당하는 값을 만들고 SKRect 행렬 변환을 적용한 다음 터치 포인트가 변환 MapRect된 사각형 내에 있는지 확인하여 적중 테스트를 수행할 수 있습니다.

touchId 경우 필드는 터치 ID로 설정되고 손가락 위치가 저장됩니다.

TouchActionType.Moved 이벤트의 경우 값의 변환 요소는 손가락의 SKMatrix 현재 위치와 손가락의 새 위치에 따라 조정됩니다. 이 새 위치는 다음 번에 저장되며 SKCanvasView 무효화됩니다.

이 프로그램을 실험할 때는 손가락이 비트맵이 표시되는 영역에 닿을 때만 비트맵을 끌 수 있습니다. 이러한 제한은 이 프로그램에 매우 중요하지는 않지만 여러 비트맵을 조작할 때 중요합니다.

꼬집기 및 크기 조정

두 손가락이 비트맵을 만질 때 어떤 일이 일어나고 싶습니까? 두 손가락이 병렬로 이동하면 비트맵이 손가락과 함께 이동하려고 할 수 있습니다. 두 손가락이 손가락 모으기 또는 스트레치 작업을 수행하는 경우 비트맵을 회전하거나(다음 섹션에서 설명) 크기를 조정할 수 있습니다. 비트맵의 크기를 조정할 때 두 손가락이 비트맵을 기준으로 동일한 위치에 다시 기본 비트맵의 크기를 적절하게 조정하는 것이 가장 좋습니다.

한 번에 두 손가락을 처리하는 것은 복잡 TouchAction 해 보이지만 처리기는 한 번에 한 손가락에 대한 정보만 받습니다. 두 손가락이 비트맵을 조작하는 경우 각 이벤트에 대해 한 손가락은 위치를 변경했지만 다른 손가락은 변경되지 않았습니다. 아래 비트맵 크기 조정 페이지 코드에서 위치를 변경하지 않은 손가락은 변환이 해당 지점을 기준으로 하기 때문에 피벗 지점이라고 합니다.

이 프로그램과 이전 프로그램의 한 가지 차이점은 여러 터치 ID를 저장해야 한다는 것입니다. 이 용도로 사전이 사용됩니다. 여기서 터치 ID는 사전 키이고 사전 값은 해당 손가락의 현재 위치입니다.

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와 터치 포인트가 사전에 추가된다는 점을 제외하고 이전 프로그램과 거의 동일합니다. ReleasedCancelled 작업은 사전 항목을 제거합니다.

그러나 작업에 대한 Moved 처리는 더 복잡합니다. 한 손가락만 관련된 경우 처리는 이전 프로그램과 매우 동일합니다. 두 개 이상의 손가락의 경우 프로그램은 이동하지 않는 손가락과 관련된 사전에서 정보를 가져와야 합니다. 이렇게 하려면 사전 키를 배열에 복사한 다음 첫 번째 키를 이동 중인 손가락의 ID와 비교합니다. 이를 통해 프로그램은 이동하지 않는 손가락에 해당하는 피벗 지점을 가져올 수 있습니다.

다음으로, 프로그램은 피벗 지점을 기준으로 새 손가락 위치의 두 벡터와 피벗 지점을 기준으로 이전 손가락 위치를 계산합니다. 이러한 벡터의 비율은 배율 인수입니다. 0으로 나누기는 가능하므로 무한 값 또는 NaN(숫자가 아님) 값에 대해 검사 합니다. 모두 잘되면 크기 조정 변환이 필드로 저장된 값과 SKMatrix 연결됩니다.

이 페이지를 실험할 때 한두 손가락으로 비트맵을 끌거나 두 손가락으로 크기를 조정할 수 있습니다. 크기 조정은 이방성이므로 수평 방향과 세로 방향으로 크기 조정이 다를 수 있습니다. 이렇게 하면 가로 세로 비율이 왜곡되지만 비트맵을 대칭 이동하여 미러 이미지를 만들 수도 있습니다. 비트맵을 0차원으로 축소할 수 있으며 사라집니다. 프로덕션 코드에서는 이를 방지해야 합니다.

두 손가락 회전

비트맵 회전 페이지를 사용하면 회전 또는 등도 크기 조정에 두 손가락을 사용할 수 있습니다. 비트맵은 항상 올바른 가로 세로 비율을 유지합니다. 회전과 이방성 배율 모두에 두 손가락을 사용하는 것은 손가락의 움직임이 두 작업 모두에서 매우 유사하기 때문에 잘 작동하지 않습니다.

이 프로그램의 첫 번째 큰 차이점은 적중 테스트 논리입니다. 이전 프로그램에서는 터치 포인트가 비트맵에 해당하는 변환된 사각형 내에 있는지 확인하는 방법을 SKRect 사용 Contains 했습니다. 그러나 사용자가 비트맵을 조작하면 비트맵이 회전될 수 있으며 SKRect 회전된 사각형을 제대로 나타낼 수 없습니다. 이 경우 적중 테스트 논리가 다소 복잡한 분석 기하 도형을 구현해야 하는 것을 두려워할 수 있습니다.

그러나 바로 가기를 사용할 수 있습니다. 변환된 사각형의 경계 내에 점이 있는지 확인하는 것은 역 변환된 점이 변환되지 않은 사각형의 경계 내에 있는지 확인하는 것과 같습니다. 이는 훨씬 더 쉬운 계산이며 논리는 편리한 Contains 방법을 계속 사용할 수 있습니다.

public partial class BitmapRotationPage : ContentPage
{
    ···
    // Touch information
    Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
    ···
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                if (!touchDictionary.ContainsKey(args.Id))
                {
                    // Invert the matrix
                    if (matrix.TryInvert(out SKMatrix inverseMatrix))
                    {
                        // Transform the point using the inverted matrix
                        SKPoint transformedPoint = inverseMatrix.MapPoint(point);

                        // Check if it's in the untransformed bitmap rectangle
                        SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);

                        if (rect.Contains(transformedPoint))
                        {
                            touchDictionary.Add(args.Id, point);
                        }
                    }
                }
                break;

            case TouchActionType.Moved:
                if (touchDictionary.ContainsKey(args.Id))
                {
                    // Single-finger drag
                    if (touchDictionary.Count == 1)
                    {
                        SKPoint prevPoint = touchDictionary[args.Id];

                        // Adjust the matrix for the new position
                        matrix.TransX += point.X - prevPoint.X;
                        matrix.TransY += point.Y - prevPoint.Y;
                        canvasView.InvalidateSurface();
                    }
                    // Double-finger rotate, scale, and drag
                    else if (touchDictionary.Count >= 2)
                    {
                        // Copy two dictionary keys into array
                        long[] keys = new long[touchDictionary.Count];
                        touchDictionary.Keys.CopyTo(keys, 0);

                        // Find index non-moving (pivot) finger
                        int pivotIndex = (keys[0] == args.Id) ? 1 : 0;

                        // Get the three points in the transform
                        SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
                        SKPoint prevPoint = touchDictionary[args.Id];
                        SKPoint newPoint = point;

                        // Calculate two vectors
                        SKPoint oldVector = prevPoint - pivotPoint;
                        SKPoint newVector = newPoint - pivotPoint;

                        // Find angles from pivot point to touch points
                        float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
                        float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);

                        // Calculate rotation matrix
                        float angle = newAngle - oldAngle;
                        SKMatrix touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);

                        // Effectively rotate the old vector
                        float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
                        oldVector.X = magnitudeRatio * newVector.X;
                        oldVector.Y = magnitudeRatio * newVector.Y;

                        // Isotropic scaling!
                        float scale = Magnitude(newVector) / Magnitude(oldVector);

                        if (!float.IsNaN(scale) && !float.IsInfinity(scale))
                        {
                            SKMatrix.PostConcat(ref touchMatrix,
                                SKMatrix.MakeScale(scale, scale, pivotPoint.X, pivotPoint.Y));

                            SKMatrix.PostConcat(ref matrix, touchMatrix);
                            canvasView.InvalidateSurface();
                        }
                    }

                    // Store the new point in the dictionary
                    touchDictionary[args.Id] = point;
                }

                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                if (touchDictionary.ContainsKey(args.Id))
                {
                    touchDictionary.Remove(args.Id);
                }
                break;
        }
    }

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

이벤트에 대한 논리는 Moved 이전 프로그램처럼 시작됩니다. 두 개의 벡터가 명명 oldVector 되고 newVector 이동 중인 손가락의 이전 점과 현재 지점 및 이동하지 않는 손가락의 피벗 지점을 기반으로 계산됩니다. 그러나 이러한 벡터의 각도가 결정되고 그 차이는 회전 각도입니다.

크기 조정도 관련될 수 있으므로 이전 벡터는 회전 각도에 따라 회전됩니다. 이제 두 벡터의 상대적 크기가 배율 인수입니다. 크기 조정이 등각형이 되도록 가로 및 세로 크기 조정에 동일한 scale 값이 사용됩니다. matrix 필드는 회전 행렬과 배율 행렬 모두에 의해 조정됩니다.

애플리케이션이 단일 비트맵(또는 다른 개체)에 대한 터치 처리를 구현해야 하는 경우 사용자 고유의 애플리케이션에 대해 이러한 세 가지 샘플의 코드를 조정할 수 있습니다. 그러나 여러 비트맵에 대한 터치 처리를 구현해야 하는 경우 다른 클래스에서 이러한 터치 작업을 캡슐화할 수 있습니다.

터치 작업 캡슐화

터치 조작 페이지는 단일 비트맵의 터치 조작을 보여 주지만 위에 표시된 논리의 대부분을 캡슐화하는 다른 여러 파일을 사용합니다. 이러한 파일 TouchManipulationMode 중 첫 번째는 표시되는 코드에 의해 구현되는 다양한 유형의 터치 조작을 나타내는 열거형입니다.

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

PanOnly 는 번역으로 구현되는 한 손가락 끌기입니다. 모든 후속 옵션에는 이동도 포함되지만 두 손가락 IsotropicScale 이 포함됩니다. 개체가 가로 및 세로 방향으로 동일하게 크기 조정되는 핀치 작업입니다. AnisotropicScale 는 같지 않은 크기 조정을 허용합니다.

ScaleRotate 손가락 크기 조정 및 회전에 대한 옵션입니다. 크기 조정은 동위원소입니다. 앞에서 멘션 이방성 배율로 두 손가락 회전을 구현하는 것은 손가락 움직임이 본질적으로 동일하기 때문에 문제가 됩니다.

ScaleDualRotate 옵션은 한 손가락 회전을 추가합니다. 한 손가락으로 개체를 끌면 개체의 중심이 끌기 벡터와 함께 표시되도록 끌기된 개체가 먼저 중심을 중심으로 회전합니다.

TouchManipulationPage.xaml 파일에는 열거형의 멤버가 TouchManipulationMode 포함된 파일이 포함됩니다Picker.

<?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 단일 셀 Grid 을 묶는 셀과 TouchEffect 연결된 셀이 있습니다.

TouchManipulationPage.xaml.cs 코드 숨김 파일에는 필드가 bitmap 있지만 형식SKBitmap이 아닙니다. 형식은 TouchManipulationBitmap (곧 볼 수 있는 클래스) 입니다.

public partial class TouchManipulationPage : ContentPage
{
    TouchManipulationBitmap bitmap;
    ...

    public TouchManipulationPage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.MountainClimbers.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            SKBitmap bitmap = SKBitmap.Decode(stream);
            this.bitmap = new TouchManipulationBitmap(bitmap);
            this.bitmap.TouchManager.Mode = TouchManipulationMode.ScaleRotate;
        }
    }
    ...
}

생성자는 포함된 리소스에서 가져온 생성자에 SKBitmap 전달하여 개체를 인스턴스화 TouchManipulationBitmap 합니다. 생성자는 개체의 TouchManager 속성 TouchManipulationBitmap 속성을 열거형의 TouchManipulationMode 멤버로 설정 Mode 하여 마무리합니다.

또한 이 SelectedIndexChanged 속성에 Picker 대한 처리기는 다음 Mode 을 설정합니다.

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 XAML 파일에서 인스턴스화된 처리기는 TouchEffect 명명 HitTest 된 다음 두 메서드를 TouchManipulationBitmap 호출합니다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 되면 터치 ID가 컬렉션에 TouchIds 추가됩니다. 이 ID는 손가락이 화면에서 들어올릴 때까지 해당 손가락에 대한 터치 이벤트 시퀀스를 나타냅니다. 여러 손가락이 비트맵을 터치하면 컬렉션에 touchIds 각 손가락에 대한 터치 ID가 포함됩니다.

TouchAction 처리기는 또한 클래스를 호출합니다ProcessTouchEvent.TouchManipulationBitmap 실제 터치 처리의 일부(전부는 아님)가 발생하는 곳입니다.

클래스는 TouchManipulationBitmap 비트맵을 렌더링하고 터치 이벤트를 처리하는 코드를 포함하는 래퍼 클래스 SKBitmap 입니다. 클래스에서 보다 일반화된 코드 TouchManipulationManager 와 함께 작동합니다(곧 표시됨).

TouchManipulationBitmap 생성자는 형식의 SKBitmap 속성과 형식 TouchManipulationManager 의 속성인 TouchManager 두 속성을 저장하고 Matrix SKMatrix인스턴스화합니다.

class TouchManipulationBitmap
{
    SKBitmap bitmap;
    ...

    public TouchManipulationBitmap(SKBitmap bitmap)
    {
        this.bitmap = bitmap;
        Matrix = SKMatrix.MakeIdentity();

        TouchManager = new TouchManipulationManager
        {
            Mode = TouchManipulationMode.ScaleRotate
        };
    }

    public TouchManipulationManager TouchManager { set; get; }

    public SKMatrix Matrix { set; get; }
    ...
}

Matrix 속성은 모든 터치 작업의 결과로 누적된 변환입니다. 보듯이 각 터치 이벤트는 행렬로 확인된 다음 속성에 저장된 Matrix 값과 SKMatrix 연결됩니다.

개체는 TouchManipulationBitmap 해당 Paint 메서드에 자신을 그립니다. 인수가 개체입니다 SKCanvas . 이미 SKCanvas 적용된 변환이 있을 수 있으므로 Paint 메서드는 비트맵과 연결된 속성을 기존 변환에 연결 Matrix 하고 완료되면 캔버스를 복원합니다.

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

이 메서드는 HitTest 사용자가 비트맵 경계 내의 한 지점에서 화면을 터치하면 반환 true 됩니다. 비트맵 회전 페이지의 앞부분에 표시된 논리를 사용합니다.

class TouchManipulationBitmap
{
    ...
    public bool HitTest(SKPoint location)
    {
        // Invert the matrix
        SKMatrix inverseMatrix;

        if (Matrix.TryInvert(out inverseMatrix))
        {
            // Transform the point using the inverted matrix
            SKPoint transformedPoint = inverseMatrix.MapPoint(location);

            // Check if it's in the untransformed bitmap rectangle
            SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
            return rect.Contains(transformedPoint);
        }
        return false;
    }
    ...
}

두 번째 public 메서드 TouchManipulationBitmap 는 .입니다 ProcessTouchEvent. 이 메서드가 호출되면 터치 이벤트가 이 특정 비트맵에 속한다는 것이 이미 설정되었습니다. 이 메서드는 단순히 각 손가락의 TouchManipulationInfo 이전 점과 새 점인 개체 사전을 기본.

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

    public SKPoint NewPoint { set; get; }
}

사전 및 ProcessTouchEvent 메서드 자체는 다음과 같습니다.

class TouchManipulationBitmap
{
    ...
    Dictionary<long, TouchManipulationInfo> touchDictionary =
        new Dictionary<long, TouchManipulationInfo>();
    ...
    public void ProcessTouchEvent(long id, TouchActionType type, SKPoint location)
    {
        switch (type)
        {
            case TouchActionType.Pressed:
                touchDictionary.Add(id, new TouchManipulationInfo
                {
                    PreviousPoint = location,
                    NewPoint = location
                });
                break;

            case TouchActionType.Moved:
                TouchManipulationInfo info = touchDictionary[id];
                info.NewPoint = location;
                Manipulate();
                info.PreviousPoint = info.NewPoint;
                break;

            case TouchActionType.Released:
                touchDictionary[id].NewPoint = location;
                Manipulate();
                touchDictionary.Remove(id);
                break;

            case TouchActionType.Cancelled:
                touchDictionary.Remove(id);
                break;
        }
    }
    ...
}

MovedReleased 이벤트에서 메서드는 .를 호출합니다Manipulate. 이때 touchDictionary 하나 이상의 TouchManipulationInfo 개체가 포함됩니다. 항목이 touchDictionary 하나 있으면 값과 NewPoint 값이 PreviousPoint 같지 않고 손가락의 움직임을 나타낼 수 있습니다. 여러 손가락이 비트맵을 터치하는 경우 사전에 둘 이상의 항목이 포함되지만 이러한 항목 중 하나만 서로 다르 PreviousPointNewPoint 값이 있습니다. 나머지는 모두 같 PreviousPointNewPoint 값이 있습니다.

이는 중요합니다. 메서드는 Manipulate 한 손가락의 움직임만 처리한다고 가정할 수 있습니다. 이 호출 시 다른 손가락은 움직이지 않으며 실제로 움직이는 경우 (가능성이 있는 것처럼) 이러한 움직임은 향후 호출 Manipulate에서 처리됩니다.

이 메서드는 Manipulate 편의를 위해 먼저 사전을 배열에 복사합니다. 처음 두 항목 이외의 항목은 무시합니다. 두 개 이상의 손가락이 비트맵을 조작하려고 하면 다른 손가락은 무시됩니다. Manipulate 는 다음의 최종 멤버입니다 TouchManipulationBitmap.

class TouchManipulationBitmap
{
    ...
    void Manipulate()
    {
        TouchManipulationInfo[] infos = new TouchManipulationInfo[touchDictionary.Count];
        touchDictionary.Values.CopyTo(infos, 0);
        SKMatrix touchMatrix = SKMatrix.MakeIdentity();

        if (infos.Length == 1)
        {
            SKPoint prevPoint = infos[0].PreviousPoint;
            SKPoint newPoint = infos[0].NewPoint;
            SKPoint pivotPoint = Matrix.MapPoint(bitmap.Width / 2, bitmap.Height / 2);

            touchMatrix = TouchManager.OneFingerManipulate(prevPoint, newPoint, pivotPoint);
        }
        else if (infos.Length >= 2)
        {
            int pivotIndex = infos[0].NewPoint == infos[0].PreviousPoint ? 0 : 1;
            SKPoint pivotPoint = infos[pivotIndex].NewPoint;
            SKPoint newPoint = infos[1 - pivotIndex].NewPoint;
            SKPoint prevPoint = infos[1 - pivotIndex].PreviousPoint;

            touchMatrix = TouchManager.TwoFingerManipulate(prevPoint, newPoint, pivotPoint);
        }

        SKMatrix matrix = Matrix;
        SKMatrix.PostConcat(ref matrix, touchMatrix);
        Matrix = matrix;
    }
}

한 손가락이 비트맵 Manipulate 을 조작하는 경우 개체의 메서드를 OneFingerManipulate TouchManipulationManager 호출합니다. 두 손가락의 경우 호출합니다 TwoFingerManipulate. 이러한 메서드에 대한 인수는 동일합니다 prevPoint . 인수와 newPoint 인수는 이동하는 손가락을 나타냅니다. pivotPoint 그러나 인수는 두 호출에 대해 다릅니다.

한 손가락 조작의 pivotPoint 경우 비트맵의 중심입니다. 이는 한 손가락 회전을 허용하기 위한 것입니다. 두 손가락 조작의 경우 이벤트는 한 손가락의 움직임만 나타내므로 pivotPoint 손가락이 움직이지 않습니다.

두 경우 TouchManipulationManager 모두 비트맵을 SKMatrix 렌더링하는 데 사용하는 현재 Matrix 속성 TouchManipulationPage 과 메서드가 연결하는 값을 반환합니다.

TouchManipulationManager 가 일반화되고 TouchManipulationMode. 사용자 고유의 애플리케이션을 변경하지 않고 이 클래스를 사용할 수 있습니다. 이 클래스는 TouchManipulationMode 형식의 단일 속성을 정의합니다.

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

그러나 이 옵션을 피하려고 할 수 있습니다 AnisotropicScale . 이 옵션을 사용하면 크기 조정 요소 중 하나가 0이 되도록 비트맵을 조작하는 것이 매우 쉽습니다. 이렇게 하면 비트맵이 보이지 않게 사라지고 절대로 돌아오지 않습니다. 이방성 크기 조정이 진정으로 필요한 경우 바람직하지 않은 결과를 방지하기 위해 논리를 향상시켜야 합니다.

TouchManipulationManager는 벡터를 사용하지만 SkiaSharp SKPoint 에는 구조가 없 SKVector 으므로 대신 사용됩니다. SKPoint 는 빼기 연산자를 지원하며 결과는 벡터로 처리될 수 있습니다. 추가해야 하는 유일한 벡터별 논리는 계산입니다.Magnitude

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

회전을 선택할 때마다 한 손가락과 두 손가락 조작 메서드가 모두 먼저 회전을 처리합니다. 회전이 감지되면 회전 구성 요소가 효과적으로 제거됩니다. 다시 기본 것은 이동 및 크기 조정으로 해석됩니다.

메서드는 OneFingerManipulate 다음과 같습니다. 한 손가락 회전을 사용하도록 설정하지 않은 경우 논리는 간단합니다. 단순히 이전 점과 새 점을 사용하여 번역에 정확하게 해당하는 벡터를 delta 생성합니다. 한 손가락 회전을 사용하도록 설정하면 메서드는 피벗 지점(비트맵의 중심)에서 이전 점 및 새 지점까지 각도를 사용하여 회전 행렬을 생성합니다.

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

    public SKMatrix OneFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
    {
        if (Mode == TouchManipulationMode.None)
        {
            return SKMatrix.MakeIdentity();
        }

        SKMatrix touchMatrix = SKMatrix.MakeIdentity();
        SKPoint delta = newPoint - prevPoint;

        if (Mode == TouchManipulationMode.ScaleDualRotate)  // One-finger rotation
        {
            SKPoint oldVector = prevPoint - pivotPoint;
            SKPoint newVector = newPoint - pivotPoint;

            // Avoid rotation if fingers are too close to center
            if (Magnitude(newVector) > 25 && Magnitude(oldVector) > 25)
            {
                float prevAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
                float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);

                // Calculate rotation matrix
                float angle = newAngle - prevAngle;
                touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);

                // Effectively rotate the old vector
                float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
                oldVector.X = magnitudeRatio * newVector.X;
                oldVector.Y = magnitudeRatio * newVector.Y;

                // Recalculate delta
                delta = newVector - oldVector;
            }
        }

        // Multiply the rotation matrix by a translation matrix
        SKMatrix.PostConcat(ref touchMatrix, SKMatrix.MakeTranslation(delta.X, delta.Y));

        return touchMatrix;
    }
    ...
}

메서드에서 TwoFingerManipulate 피벗 지점은 이 특정 터치 이벤트에서 이동하지 않는 손가락의 위치입니다. 회전은 한 손가락 회전과 매우 유사하며, 이전 점에 따라 명명 oldVector 된 벡터가 회전에 맞게 조정됩니다. 다시 기본 이동은 크기 조정으로 해석됩니다.

class TouchManipulationManager
{
    ...
    public SKMatrix TwoFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
    {
        SKMatrix touchMatrix = SKMatrix.MakeIdentity();
        SKPoint oldVector = prevPoint - pivotPoint;
        SKPoint newVector = newPoint - pivotPoint;

        if (Mode == TouchManipulationMode.ScaleRotate ||
            Mode == TouchManipulationMode.ScaleDualRotate)
        {
            // Find angles from pivot point to touch points
            float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
            float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);

            // Calculate rotation matrix
            float angle = newAngle - oldAngle;
            touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);

            // Effectively rotate the old vector
            float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
            oldVector.X = magnitudeRatio * newVector.X;
            oldVector.Y = magnitudeRatio * newVector.Y;
        }

        float scaleX = 1;
        float scaleY = 1;

        if (Mode == TouchManipulationMode.AnisotropicScale)
        {
            scaleX = newVector.X / oldVector.X;
            scaleY = newVector.Y / oldVector.Y;

        }
        else if (Mode == TouchManipulationMode.IsotropicScale ||
                 Mode == TouchManipulationMode.ScaleRotate ||
                 Mode == TouchManipulationMode.ScaleDualRotate)
        {
            scaleX = scaleY = Magnitude(newVector) / Magnitude(oldVector);
        }

        if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
            !float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
        {
            SKMatrix.PostConcat(ref touchMatrix,
                SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y));
        }

        return touchMatrix;
    }
    ...
}

이 메서드에는 명시적 번역이 없다는 것을 알 수 있습니다. 그러나 두 메서드는 모두 MakeRotation MakeScale 피벗 지점을 기반으로 하며 암시적 번역을 포함합니다. 비트맵에서 두 손가락을 사용하여 같은 방향으로 TouchManipulation 끌면 두 손가락 사이에 일련의 터치 이벤트가 교대로 표시됩니다. 각 손가락이 서로 상대적으로 움직이면 크기 조정 또는 회전 결과가 발생하지만 다른 손가락의 움직임에 의해 무효화되고 결과는 변환됩니다.

터치 조작 페이지의 유일한 재기본 부분은 코드 숨김 파일의 TouchManipulationPage 처리기입니다PaintSurface. 이렇게 하면 누적된 터치 작업을 나타내는 행렬을 적용하는 메서드TouchManipulationBitmap가 호출 Paint 됩니다.

public partial class TouchManipulationPage : ContentPage
{
    ...
    MatrixDisplay matrixDisplay = new MatrixDisplay();
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Display the bitmap
        bitmap.Paint(canvas);

        // Display the matrix in the lower-right corner
        SKSize matrixSize = matrixDisplay.Measure(bitmap.Matrix);

        matrixDisplay.Paint(canvas, bitmap.Matrix,
            new SKPoint(info.Width - matrixSize.Width,
                        info.Height - matrixSize.Height));
    }
}

PaintSurface 처리기는 누적된 터치 매트릭스를 보여 주는 MatrixDisplay 개체를 표시하여 종료합니다.

터치 조작 페이지의 삼중 스크린샷

여러 비트맵 조작

이러한 클래스 TouchManipulationBitmap TouchManipulationManager 에서 터치 처리 코드를 격리할 때의 장점 중 하나는 사용자가 여러 비트맵을 조작할 수 있는 프로그램에서 이러한 클래스를 다시 사용할 수 있다는 점입니다.

비트맵 분산 보기 페이지에서는 이 작업을 수행하는 방법을 보여 줍니다. 클래스는 형식 TouchManipulationBitmapBitmapScatterPage 필드를 정의하는 대신 비트맵 개체를 List 정의합니다.

public partial class BitmapScatterViewPage : ContentPage
{
    List<TouchManipulationBitmap> bitmapCollection =
        new List<TouchManipulationBitmap>();
    ...
    public BitmapScatterViewPage()
    {
        InitializeComponent();

        // Load in all the available bitmaps
        Assembly assembly = GetType().GetTypeInfo().Assembly;
        string[] resourceIDs = assembly.GetManifestResourceNames();
        SKPoint position = new SKPoint();

        foreach (string resourceID in resourceIDs)
        {
            if (resourceID.EndsWith(".png") ||
                resourceID.EndsWith(".jpg"))
            {
                using (Stream stream = assembly.GetManifestResourceStream(resourceID))
                {
                    SKBitmap bitmap = SKBitmap.Decode(stream);
                    bitmapCollection.Add(new TouchManipulationBitmap(bitmap)
                    {
                        Matrix = SKMatrix.MakeTranslation(position.X, position.Y),
                    });
                    position.X += 100;
                    position.Y += 100;
                }
            }
        }
    }
    ...
}

생성자는 포함된 리소스로 사용할 수 있는 모든 비트맵을 로드하고 해당 비트맵을 bitmapCollection추가합니다. 각 개체에서 Matrix 속성이 초기화되므로 각 TouchManipulationBitmap 비트맵의 왼쪽 위 모서리가 100픽셀씩 오프셋됩니다.

또한 페이지는 BitmapScatterView 여러 비트맵에 대한 터치 이벤트를 처리해야 합니다. 현재 조작된 List TouchManipulationBitmap 개체의 터치 ID를 정의하는 대신 이 프로그램에는 사전이 필요합니다.

public partial class BitmapScatterViewPage : ContentPage
{
    ...
    Dictionary<long, TouchManipulationBitmap> bitmapDictionary =
       new Dictionary<long, TouchManipulationBitmap>();
    ...
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                for (int i = bitmapCollection.Count - 1; i >= 0; i--)
                {
                    TouchManipulationBitmap bitmap = bitmapCollection[i];

                    if (bitmap.HitTest(point))
                    {
                        // Move bitmap to end of collection
                        bitmapCollection.Remove(bitmap);
                        bitmapCollection.Add(bitmap);

                        // Do the touch processing
                        bitmapDictionary.Add(args.Id, bitmap);
                        bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                        canvasView.InvalidateSurface();
                        break;
                    }
                }
                break;

            case TouchActionType.Moved:
                if (bitmapDictionary.ContainsKey(args.Id))
                {
                    TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    canvasView.InvalidateSurface();
                }
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                if (bitmapDictionary.ContainsKey(args.Id))
                {
                    TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
                    bitmap.ProcessTouchEvent(args.Id, args.Type, point);
                    bitmapDictionary.Remove(args.Id);
                    canvasView.InvalidateSurface();
                }
                break;
        }
    }
    ...
}

논리가 역방향으로 Pressed 반복되는 bitmapCollection 방식을 확인합니다. 비트맵은 종종 서로 겹칩니다. 컬렉션의 뒷부분에 있는 비트맵은 컬렉션의 앞부분에 있는 비트맵 위에 시각적으로 있습니다. 화면에서 누르는 손가락 아래에 여러 비트맵이 있는 경우 맨 위 비트맵은 해당 손가락에 의해 조작된 비트맵이어야 합니다.

또한 논리는 Pressed 다른 비트맵 더미의 맨 위로 시각적으로 이동하도록 해당 비트맵을 컬렉션의 끝으로 이동합니다.

MovedReleased 이벤트에서 TouchAction 처리기는 이전 프로그램과 마찬가지로 메서드 TouchManipulationBitmap 를 호출 ProcessingTouchEvent 합니다.

마지막으로 처리기는 PaintSurfaceTouchManipulationBitmap 개체의 메서드를 Paint 호출합니다.

public partial class BitmapScatterViewPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKCanvas canvas = args.Surface.Canvas;
        canvas.Clear();

        foreach (TouchManipulationBitmap bitmap in bitmapCollection)
        {
            bitmap.Paint(canvas);
        }
    }
}

코드는 컬렉션을 반복하고 컬렉션의 시작 부분에서 끝까지 비트맵 더미를 표시합니다.

비트맵 분산 보기 페이지의 삼중 스크린샷

한 손가락 크기 조정

크기 조정 작업에는 일반적으로 두 손가락을 사용하는 손가락 모으기 제스처가 필요합니다. 그러나 손가락이 비트맵의 모서리를 이동하도록 하여 한 손가락으로 크기 조정을 구현할 수 있습니다.

이는 한 손가락 코너 배율 페이지에서 설명합니다. 이 샘플은 클래스에서 구현된 것과는 다소 다른 형식의 크기 조정을 TouchManipulationManager 사용하므로 해당 클래스 또는 클래스를 TouchManipulationBitmap 사용하지 않습니다. 대신 모든 터치 논리는 코드 숨김 파일에 있습니다. 이는 한 번에 한 손가락만 추적하고 화면을 터치할 수 있는 보조 손가락을 무시하기 때문에 평소보다 다소 간단한 논리입니다.

SingleFingerCornerScale.xaml 페이지는 클래스를 SKCanvasView 인스턴스화하고 터치 이벤트를 추적하기 위한 개체를 TouchEffect 만듭니다.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             xmlns:tt="clr-namespace:TouchTracking"
             x:Class="SkiaSharpFormsDemos.Transforms.SingleFingerCornerScalePage"
             Title="Single Finger Corner Scale">

    <Grid BackgroundColor="White"
          Grid.Row="1">

        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface" />
        <Grid.Effects>
            <tt:TouchEffect Capture="True"
                            TouchAction="OnTouchEffectAction"   />
        </Grid.Effects>
    </Grid>
</ContentPage>

SingleFingerCornerScalePage.xaml.cs 파일은 미디어 디렉터리에서 비트맵 리소스를 로드하고 필드로 정의된 개체를 SKMatrix 사용하여 표시합니다.

public partial class SingleFingerCornerScalePage : ContentPage
{
    SKBitmap bitmap;
    SKMatrix currentMatrix = SKMatrix.MakeIdentity();
    ···

    public SingleFingerCornerScalePage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            bitmap = SKBitmap.Decode(stream);
        }
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        canvas.SetMatrix(currentMatrix);
        canvas.DrawBitmap(bitmap, 0, 0);
    }
    ···
}

SKMatrix 개체는 아래 표시된 터치 논리에 의해 수정됩니다.

코드 숨김 파일의 re기본der는 이벤트 처리기입니다TouchEffect. 먼저 손가락의 현재 위치를 값으로 SKPoint 변환합니다. Pressed 작업 유형의 경우 처리기는 다른 손가락이 화면을 건드리지 않고 손가락이 비트맵의 범위 내에 있음을 검사.

코드의 중요한 부분은 메서드에 대한 if 두 가지 호출 Math.Pow 을 포함하는 문입니다. 이 수학 검사 손가락 위치가 비트맵을 채우는 줄임표 외부에 있는지를 나타냅니다. 그렇다면 크기 조정 작업입니다. 손가락은 비트맵의 모서리 중 하나 근처에 있으며 피벗 지점은 반대쪽 모서리로 결정됩니다. 손가락이 이 줄임표 내에 있는 경우 일반적인 이동 작업입니다.

public partial class SingleFingerCornerScalePage : ContentPage
{
    SKBitmap bitmap;
    SKMatrix currentMatrix = SKMatrix.MakeIdentity();

    // Information for translating and scaling
    long? touchId = null;
    SKPoint pressedLocation;
    SKMatrix pressedMatrix;

    // Information for scaling
    bool isScaling;
    SKPoint pivotPoint;
    ···

    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        // Convert Xamarin.Forms point to pixels
        Point pt = args.Location;
        SKPoint point =
            new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                        (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));

        switch (args.Type)
        {
            case TouchActionType.Pressed:
                // Track only one finger
                if (touchId.HasValue)
                    return;

                // Check if the finger is within the boundaries of the bitmap
                SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
                rect = currentMatrix.MapRect(rect);
                if (!rect.Contains(point))
                    return;

                // First assume there will be no scaling
                isScaling = false;

                // If touch is outside interior ellipse, make this a scaling operation
                if (Math.Pow((point.X - rect.MidX) / (rect.Width / 2), 2) +
                    Math.Pow((point.Y - rect.MidY) / (rect.Height / 2), 2) > 1)
                {
                    isScaling = true;
                    float xPivot = point.X < rect.MidX ? rect.Right : rect.Left;
                    float yPivot = point.Y < rect.MidY ? rect.Bottom : rect.Top;
                    pivotPoint = new SKPoint(xPivot, yPivot);
                }

                // Common for either pan or scale
                touchId = args.Id;
                pressedLocation = point;
                pressedMatrix = currentMatrix;
                break;

            case TouchActionType.Moved:
                if (!touchId.HasValue || args.Id != touchId.Value)
                    return;

                SKMatrix matrix = SKMatrix.MakeIdentity();

                // Translating
                if (!isScaling)
                {
                    SKPoint delta = point - pressedLocation;
                    matrix = SKMatrix.MakeTranslation(delta.X, delta.Y);
                }
                // Scaling
                else
                {
                    float scaleX = (point.X - pivotPoint.X) / (pressedLocation.X - pivotPoint.X);
                    float scaleY = (point.Y - pivotPoint.Y) / (pressedLocation.Y - pivotPoint.Y);
                    matrix = SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);
                }

                // Concatenate the matrices
                SKMatrix.PreConcat(ref matrix, pressedMatrix);
                currentMatrix = matrix;
                canvasView.InvalidateSurface();
                break;

            case TouchActionType.Released:
            case TouchActionType.Cancelled:
                touchId = null;
                break;
        }
    }
}

작업 유형은 Moved 손가락이 화면을 눌렀을 때부터 이 시간까지 터치 작업에 해당하는 행렬을 계산합니다. 이 매트릭스는 손가락이 비트맵을 처음 누를 때 적용된 행렬과 연결됩니다. 크기 조정 작업은 항상 손가락이 닿은 모서리와 반대되는 모서리를 기준으로 합니다.

작거나 직사각형 비트맵의 경우 내부 타원은 비트맵의 대부분을 차지하고 비트맵의 크기를 조정하기 위해 모서리에 작은 영역을 남겨 둘 수 있습니다. 다소 다른 접근 방식을 선호할 수 있습니다. 이 경우 이 코드로 설정 isScaling true 되는 전체 if 블록을 바꿀 수 있습니다.

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

이 코드는 비트맵의 영역을 내부 다이아몬드 모양과 모서리에 있는 4개의 삼각형으로 효과적으로 나눕니다. 이렇게 하면 모서리에 있는 훨씬 더 큰 영역이 비트맵을 잡고 크기를 조정할 수 있습니다.