Манипуляции сенсорного ввода
Использование матрицных преобразований для реализации перетаскивания, перетаскивания, сцепления и поворота
В много сенсорных средах, таких как на мобильных устройствах, пользователи часто используют пальцы для управления объектами на экране. Распространенные жесты, такие как перетаскивание с одним пальцем, и двумя пальцами, могут перемещать и масштабировать объекты, или даже поворачивать их. Эти жесты обычно реализуются с помощью матриц преобразования, и в этой статье показано, как это сделать.
Все примеры, показанные здесь, используют Xamarin.Forms эффект сенсорного отслеживания, представленный в статье "Вызов событий из эффектов".
Перетаскивание и перевод
Одним из наиболее важных приложений преобразования матрицы является обработка сенсорной обработки. Одно SKMatrix
значение может консолидировать ряд операций касания.
Для перетаскивания SKMatrix
с одним пальцем значение выполняет перевод. Это показано на странице перетаскивания растрового рисунка. XAML-файл создает SKCanvasView
экземпляр в объекте Xamarin.FormsGrid
. Объект TouchEffect
был добавлен в коллекциюEffects
:Grid
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Transforms.BitmapDraggingPage"
Title="Bitmap Dragging">
<Grid BackgroundColor="White">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</ContentPage>
В теории TouchEffect
объект можно добавить непосредственно в Effects
коллекцию SKCanvasView
, но это не работает на всех платформах. Так как размер SKCanvasView
такой же, как Grid
и в этой конфигурации, присоединяя его к Grid
работе так же хорошо.
Файл программной части загружается в ресурс растрового изображения в конструкторе и отображает его в обработчике PaintSurface
:
public partial class BitmapDraggingPage : ContentPage
{
// Bitmap and matrix for display
SKBitmap bitmap;
SKMatrix matrix = SKMatrix.MakeIdentity();
···
public BitmapDraggingPage()
{
InitializeComponent();
string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
bitmap = SKBitmap.Decode(stream);
}
}
···
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Display the bitmap
canvas.SetMatrix(matrix);
canvas.DrawBitmap(bitmap, new SKPoint());
}
}
Без дальнейшего кода SKMatrix
значение всегда является матрицей идентификации, и оно не будет влиять на отображение растрового изображения. Целью обработчика OnTouchEffectAction
, заданного в XAML-файле, является изменение значения матрицы для отражения сенсорных манипуляций.
Обработчик OnTouchEffectAction
начинается с преобразования Xamarin.FormsPoint
значения в значение SkiaSharp SKPoint
. Это простой вопрос масштабирования на Width
основе свойств Height
SKCanvasView
(которые являются устройствами независимо от устройства) и CanvasSize
свойства, которые находятся в единицах пикселей:
public partial class BitmapDraggingPage : ContentPage
{
···
// Touch information
long touchId = -1;
SKPoint previousPoint;
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
// Find transformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
rect = matrix.MapRect(rect);
// Determine if the touch was within that rectangle
if (rect.Contains(point))
{
touchId = args.Id;
previousPoint = point;
}
break;
case TouchActionType.Moved:
if (touchId == args.Id)
{
// Adjust the matrix for the new position
matrix.TransX += point.X - previousPoint.X;
matrix.TransY += point.Y - previousPoint.Y;
previousPoint = point;
canvasView.InvalidateSurface();
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
touchId = -1;
break;
}
}
···
}
Когда пальцем сначала прикасается к экрану, событие типа TouchActionType.Pressed
запускается. Первая задача — определить, касается ли палец растрового изображения. Такая задача часто называется хит-тестированием. В этом случае можно выполнить тестирование попаданий, создав SKRect
значение, соответствующее растровой карте, применив к нему MapRect
преобразование матрицы, а затем определив, находится ли точка касания внутри преобразованного прямоугольника.
Если это так, touchId
поле задается идентификатором сенсорного ввода и сохраняется положение пальца.
TouchActionType.Moved
Для события факторы SKMatrix
перевода значения корректируются на основе текущего положения пальца и нового положения пальца. Эта новая позиция сохраняется в следующий раз и SKCanvasView
является недействительным.
Поэкспериментируйте с этой программой, обратите внимание, что вы можете перетаскивать растровое изображение только при касании пальца области, в которой отображается растровое изображение. Хотя это ограничение не очень важно для этой программы, она становится важной при управлении несколькими растровыми изображениями.
Закрепление и масштабирование
Что нужно сделать, когда два пальца касаются растрового изображения? Если два пальца перемещаются параллельно, то, вероятно, нужно, чтобы растровое изображение перемещалось вместе с пальцами. Если два пальца выполняют операцию сцепления или растяжения, может потребоваться повернуть растровое изображение (обсудить в следующем разделе) или масштабировать. При масштабировании растрового изображения большинство пальцев должны оставаться в одинаковых позициях относительно растрового изображения, а также для масштабирования растрового изображения соответствующим образом.
Обработка двух пальцев одновременно кажется сложной, но помните, что TouchAction
обработчик получает только информацию о одном пальце за раз. Если два пальца управляют растровым изображением, то для каждого события один палец изменил положение, но другой не изменился. В приведенном ниже коде страницы масштабирования растрового рисунка палец, который не изменил позицию, называется точкой сводной таблицы, так как преобразование относительно этой точки.
Одно из различий между этой программой и предыдущей программой заключается в том, что необходимо сохранить несколько идентификаторов касания. Словарь используется для этой цели, где сенсорный идентификатор является ключом словаря, а значение словаря — текущей позицией этого пальца:
public partial class BitmapScalingPage : ContentPage
{
···
// Touch information
Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
// Find transformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
rect = matrix.MapRect(rect);
// Determine if the touch was within that rectangle
if (rect.Contains(point) && !touchDictionary.ContainsKey(args.Id))
{
touchDictionary.Add(args.Id, point);
}
break;
case TouchActionType.Moved:
if (touchDictionary.ContainsKey(args.Id))
{
// Single-finger drag
if (touchDictionary.Count == 1)
{
SKPoint prevPoint = touchDictionary[args.Id];
// Adjust the matrix for the new position
matrix.TransX += point.X - prevPoint.X;
matrix.TransY += point.Y - prevPoint.Y;
canvasView.InvalidateSurface();
}
// Double-finger scale and drag
else if (touchDictionary.Count >= 2)
{
// Copy two dictionary keys into array
long[] keys = new long[touchDictionary.Count];
touchDictionary.Keys.CopyTo(keys, 0);
// Find index of non-moving (pivot) finger
int pivotIndex = (keys[0] == args.Id) ? 1 : 0;
// Get the three points involved in the transform
SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
SKPoint prevPoint = touchDictionary[args.Id];
SKPoint newPoint = point;
// Calculate two vectors
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
// Scaling factors are ratios of those
float scaleX = newVector.X / oldVector.X;
float scaleY = newVector.Y / oldVector.Y;
if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
!float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
{
// If something bad hasn't happened, calculate a scale and translation matrix
SKMatrix scaleMatrix =
SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);
SKMatrix.PostConcat(ref matrix, scaleMatrix);
canvasView.InvalidateSurface();
}
}
// Store the new point in the dictionary
touchDictionary[args.Id] = point;
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchDictionary.ContainsKey(args.Id))
{
touchDictionary.Remove(args.Id);
}
break;
}
}
···
}
Обработка действия почти аналогична предыдущей Pressed
программе, за исключением того, что идентификатор и точка касания добавляются в словарь. Cancelled
И Released
действия удаляют запись словаря.
Однако обработка Moved
действия более сложна. Если есть только один палец, обработка очень аналогична предыдущей программе. Для двух или нескольких пальцев программа также должна получить информацию из словаря, включающего пальцем, который не перемещается. Это делается путем копирования ключей словаря в массив, а затем сравнения первого ключа с идентификатором перемещаемого пальца. Это позволяет программе получить точку сводных данных, соответствующую пальцу, который не перемещается.
Затем программа вычисляет два вектора новой позиции пальца относительно точки сводных точек и старое положение пальца относительно точки сводных данных. Коэффициенты этих векторов являются коэффициентами масштабирования. Поскольку деление по нулю является возможностью, они должны быть проверка для бесконечных значений или naN (а не числа). Если все хорошо, преобразование масштабирования объединяется со SKMatrix
значением, сохраненным в виде поля.
Поэкспериментируя с этой страницей, вы заметите, что можно перетащить растровое изображение одним или двумя пальцами или масштабировать его двумя пальцами. Масштабирование является анизотропным, что означает, что масштабирование может отличаться в горизонтальных и вертикальных направлениях. Это искажает пропорции, но также позволяет перевернуть растровое изображение, чтобы сделать изображение зеркало. Вы также можете обнаружить, что можно уменьшить растровое изображение до нуля измерения, и он исчезает. В рабочем коде вы хотите защититься от этого.
Поворот двумя пальцами
Страница поворота растрового изображения позволяет использовать два пальца для поворота или изотропного масштабирования. Растровое изображение всегда сохраняет правильное соотношение пропорций. Использование двух пальцев для поворота и анисотропного масштабирования не работает очень хорошо, потому что движение пальцев очень похоже для обеих задач.
Первое большое различие в этой программе — логика хит-тестирования. Предыдущие программы использовали Contains
метод SKRect
определения того, находится ли точка касания внутри преобразованного прямоугольника, соответствующего растровой карте. Но по мере того как пользователь управляет растровым изображением, то растровое изображение может быть повернуто и SKRect
не может правильно представлять повернутый прямоугольник. Вы можете бояться, что логика хит-тестирования должна реализовать довольно сложную геометрию аналитики в этом случае.
Однако ярлык доступен: определение того, находится ли точка в границах преобразованного прямоугольника, совпадает с определением того, находится ли обратная преобразованная точка в границах нетрансформированного прямоугольника. Это гораздо проще вычислений, и логика может продолжать использовать удобный Contains
метод:
public partial class BitmapRotationPage : ContentPage
{
···
// Touch information
Dictionary<long, SKPoint> touchDictionary = new Dictionary<long, SKPoint>();
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
if (!touchDictionary.ContainsKey(args.Id))
{
// Invert the matrix
if (matrix.TryInvert(out SKMatrix inverseMatrix))
{
// Transform the point using the inverted matrix
SKPoint transformedPoint = inverseMatrix.MapPoint(point);
// Check if it's in the untransformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
if (rect.Contains(transformedPoint))
{
touchDictionary.Add(args.Id, point);
}
}
}
break;
case TouchActionType.Moved:
if (touchDictionary.ContainsKey(args.Id))
{
// Single-finger drag
if (touchDictionary.Count == 1)
{
SKPoint prevPoint = touchDictionary[args.Id];
// Adjust the matrix for the new position
matrix.TransX += point.X - prevPoint.X;
matrix.TransY += point.Y - prevPoint.Y;
canvasView.InvalidateSurface();
}
// Double-finger rotate, scale, and drag
else if (touchDictionary.Count >= 2)
{
// Copy two dictionary keys into array
long[] keys = new long[touchDictionary.Count];
touchDictionary.Keys.CopyTo(keys, 0);
// Find index non-moving (pivot) finger
int pivotIndex = (keys[0] == args.Id) ? 1 : 0;
// Get the three points in the transform
SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
SKPoint prevPoint = touchDictionary[args.Id];
SKPoint newPoint = point;
// Calculate two vectors
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
// Find angles from pivot point to touch points
float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
// Calculate rotation matrix
float angle = newAngle - oldAngle;
SKMatrix touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
// Effectively rotate the old vector
float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
oldVector.X = magnitudeRatio * newVector.X;
oldVector.Y = magnitudeRatio * newVector.Y;
// Isotropic scaling!
float scale = Magnitude(newVector) / Magnitude(oldVector);
if (!float.IsNaN(scale) && !float.IsInfinity(scale))
{
SKMatrix.PostConcat(ref touchMatrix,
SKMatrix.MakeScale(scale, scale, pivotPoint.X, pivotPoint.Y));
SKMatrix.PostConcat(ref matrix, touchMatrix);
canvasView.InvalidateSurface();
}
}
// Store the new point in the dictionary
touchDictionary[args.Id] = point;
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchDictionary.ContainsKey(args.Id))
{
touchDictionary.Remove(args.Id);
}
break;
}
}
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
···
}
Логика Moved
события начинается так же, как и предыдущая программа. Два вектора с именем oldVector
и newVector
вычисляются на основе предыдущей и текущей точки движущегося пальца и точки свораченного пальца. Но затем определяются угловые векторы, а разница — угол поворота.
Масштабирование также может быть задействовано, поэтому старый вектор поворачивается на основе угла поворота. Относительная величина двух векторов теперь является коэффициентом масштабирования. Обратите внимание, что одно и то же scale
значение используется для горизонтального и вертикального масштабирования, чтобы масштабирование было isotropic. Поле matrix
настраивается матрицей поворота и матрицей масштабирования.
Если приложению необходимо реализовать обработку сенсорного ввода для одного растрового изображения (или другого объекта), можно адаптировать код из этих трех примеров для собственного приложения. Но если вам нужно реализовать обработку сенсорного ввода для нескольких растровых изображений, вероятно, потребуется инкапсулировать эти операции касания в других классах.
Инкапсулирование операций касания
На странице обработки касания демонстрируется обработка касания одного растрового изображения, но использование нескольких других файлов, которые инкапсулируют большую часть логики, показанной выше. Первым из этих файлов является TouchManipulationMode
перечисление, указывающее различные типы манипуляций касанием, реализованные в коде, который вы увидите:
enum TouchManipulationMode
{
None,
PanOnly,
IsotropicScale, // includes panning
AnisotropicScale, // includes panning
ScaleRotate, // implies isotropic scaling
ScaleDualRotate // adds one-finger rotation
}
PanOnly
— это перетаскивание с одним пальцем, реализуемое с помощью перевода. Все последующие параметры также включают сдвиг, но включают два пальца: IsotropicScale
это операция сцепления, которая приводит к масштабированию объекта одинаково в горизонтальных и вертикальных направлениях. AnisotropicScale
разрешает неравное масштабирование.
Этот ScaleRotate
параметр предназначен для двух пальцев масштабирования и поворота. Масштабирование является isotropic. Как упоминание ранее, реализация двумя пальцами поворота с анизотропным масштабированием проблематична, так как движения пальцев в основном одинаковы.
Параметр ScaleDualRotate
добавляет поворот одним пальцем. Когда один палец перетаскивает объект, перетаскиваемый объект сначала поворачивается вокруг его центра, чтобы центр объекта линий вверх с вектором перетаскивания.
Файл TouchManipulationPage.xaml содержит Picker
элементы перечисления TouchManipulationMode
:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
xmlns:local="clr-namespace:SkiaSharpFormsDemos.Transforms"
x:Class="SkiaSharpFormsDemos.Transforms.TouchManipulationPage"
Title="Touch Manipulation">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Picker Title="Touch Mode"
Grid.Row="0"
SelectedIndexChanged="OnTouchModePickerSelectedIndexChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type local:TouchManipulationMode}">
<x:Static Member="local:TouchManipulationMode.None" />
<x:Static Member="local:TouchManipulationMode.PanOnly" />
<x:Static Member="local:TouchManipulationMode.IsotropicScale" />
<x:Static Member="local:TouchManipulationMode.AnisotropicScale" />
<x:Static Member="local:TouchManipulationMode.ScaleRotate" />
<x:Static Member="local:TouchManipulationMode.ScaleDualRotate" />
</x:Array>
</Picker.ItemsSource>
<Picker.SelectedIndex>
4
</Picker.SelectedIndex>
</Picker>
<Grid BackgroundColor="White"
Grid.Row="1">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</Grid>
</ContentPage>
К нижней части — это SKCanvasView
и присоединенная TouchEffect
к одной ячейке Grid
, заключающая ее.
В файле кода TouchManipulationPage.xaml.cs есть bitmap
поле, но оно не имеет типаSKBitmap
. Тип — TouchManipulationBitmap
(класс, который вы увидите в ближайшее время):
public partial class TouchManipulationPage : ContentPage
{
TouchManipulationBitmap bitmap;
...
public TouchManipulationPage()
{
InitializeComponent();
string resourceID = "SkiaSharpFormsDemos.Media.MountainClimbers.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
SKBitmap bitmap = SKBitmap.Decode(stream);
this.bitmap = new TouchManipulationBitmap(bitmap);
this.bitmap.TouchManager.Mode = TouchManipulationMode.ScaleRotate;
}
}
...
}
Конструктор создает экземпляр TouchManipulationBitmap
объекта, передавая конструктору SKBitmap
полученный из внедренного ресурса. Конструктор завершается путем задания Mode
свойства TouchManager
свойства TouchManipulationBitmap
объекта элементу TouchManipulationMode
перечисления.
Обработчик для этого свойства также задает следующее 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
экземпляра TouchEffect
в XAML-файле вызывает два метода в TouchManipulationBitmap
именованном HitTest
и ProcessTouchEvent
:
public partial class TouchManipulationPage : ContentPage
{
...
List<long> touchIds = new List<long>();
...
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
if (bitmap.HitTest(point))
{
touchIds.Add(args.Id);
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
break;
}
break;
case TouchActionType.Moved:
if (touchIds.Contains(args.Id))
{
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
canvasView.InvalidateSurface();
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchIds.Contains(args.Id))
{
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
touchIds.Remove(args.Id);
canvasView.InvalidateSurface();
}
break;
}
}
...
}
HitTest
Если метод возвращается true
— это означает, что палец коснулся экрана в области, занятой растровым изображением, то сенсорный идентификатор добавляется в коллекциюTouchIds
. Этот идентификатор представляет последовательность событий касания для этого пальца, пока палец не поднимается с экрана. Если несколько пальцев касаются растрового изображения, touchIds
коллекция содержит идентификатор касания для каждого пальца.
Обработчик TouchAction
также вызывает ProcessTouchEvent
класс в TouchManipulationBitmap
. Это место, где происходит некоторые (но не все) реальной обработки сенсорного ввода.
Класс TouchManipulationBitmap
является классом-оболочкой, который содержит код для SKBitmap
отрисовки растрового изображения и обработки событий касания. Он работает в сочетании с более обобщенным кодом в TouchManipulationManager
классе (который вы увидите в ближайшее время).
Конструктор TouchManipulationBitmap
сохраняет SKBitmap
и создает экземпляры двух свойств, TouchManager
свойства типа TouchManipulationManager
и Matrix
свойства типа SKMatrix
:
class TouchManipulationBitmap
{
SKBitmap bitmap;
...
public TouchManipulationBitmap(SKBitmap bitmap)
{
this.bitmap = bitmap;
Matrix = SKMatrix.MakeIdentity();
TouchManager = new TouchManipulationManager
{
Mode = TouchManipulationMode.ScaleRotate
};
}
public TouchManipulationManager TouchManager { set; get; }
public SKMatrix Matrix { set; get; }
...
}
Это Matrix
свойство является накопленным преобразованием, полученным из всех действий касания. Как видно, каждое событие касания разрешается в матрицу, которая затем объединяется со SKMatrix
значением, хранящимся свойством Matrix
.
Объект TouchManipulationBitmap
рисует себя в методе Paint
. Аргумент является SKCanvas
объектом. Это SKCanvas
может уже применить к нему преобразование, поэтому Paint
метод объединяет Matrix
свойство, связанное с растровым изображением к существующему преобразованию, и восстанавливает холст после завершения:
class TouchManipulationBitmap
{
...
public void Paint(SKCanvas canvas)
{
canvas.Save();
SKMatrix matrix = Matrix;
canvas.Concat(ref matrix);
canvas.DrawBitmap(bitmap, 0, 0);
canvas.Restore();
}
...
}
Метод HitTest
возвращается, если пользователь прикасается true
к экрану в точке в границах растрового изображения. Для этого используется логика, показанная ранее на странице поворота растрового рисунка:
class TouchManipulationBitmap
{
...
public bool HitTest(SKPoint location)
{
// Invert the matrix
SKMatrix inverseMatrix;
if (Matrix.TryInvert(out inverseMatrix))
{
// Transform the point using the inverted matrix
SKPoint transformedPoint = inverseMatrix.MapPoint(location);
// Check if it's in the untransformed bitmap rectangle
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
return rect.Contains(transformedPoint);
}
return false;
}
...
}
Второй открытый метод в 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;
}
}
...
}
Moved
В и Released
событиях метод вызываетсяManipulate
. В это время touchDictionary
содержит один или несколько TouchManipulationInfo
объектов. Если элемент touchDictionary
содержит один элемент, скорее всего PreviousPoint
NewPoint
, они неравны и представляют движение пальца. Если несколько пальцев касаются растрового изображения, словарь содержит несколько элементов, но только один из этих элементов имеет разные PreviousPoint
значения и NewPoint
значения. Все остальные имеют равные PreviousPoint
значения и NewPoint
значения.
Это важно: Manipulate
метод может предположить, что он обрабатывает движение только одного пальца. Во время этого вызова ни один из остальных пальцев двигается, и если они действительно движутся (как вероятно), эти движения будут обработаны в будущих вызовах Manipulate
.
Метод Manipulate
сначала копирует словарь в массив для удобства. Он игнорирует что-либо, отличное от первых двух записей. Если более двух пальцев пытаются управлять растровым изображением, остальные игнорируются. Manipulate
является окончательным членом TouchManipulationBitmap
:
class TouchManipulationBitmap
{
...
void Manipulate()
{
TouchManipulationInfo[] infos = new TouchManipulationInfo[touchDictionary.Count];
touchDictionary.Values.CopyTo(infos, 0);
SKMatrix touchMatrix = SKMatrix.MakeIdentity();
if (infos.Length == 1)
{
SKPoint prevPoint = infos[0].PreviousPoint;
SKPoint newPoint = infos[0].NewPoint;
SKPoint pivotPoint = Matrix.MapPoint(bitmap.Width / 2, bitmap.Height / 2);
touchMatrix = TouchManager.OneFingerManipulate(prevPoint, newPoint, pivotPoint);
}
else if (infos.Length >= 2)
{
int pivotIndex = infos[0].NewPoint == infos[0].PreviousPoint ? 0 : 1;
SKPoint pivotPoint = infos[pivotIndex].NewPoint;
SKPoint newPoint = infos[1 - pivotIndex].NewPoint;
SKPoint prevPoint = infos[1 - pivotIndex].PreviousPoint;
touchMatrix = TouchManager.TwoFingerManipulate(prevPoint, newPoint, pivotPoint);
}
SKMatrix matrix = Matrix;
SKMatrix.PostConcat(ref matrix, touchMatrix);
Matrix = matrix;
}
}
Если один палец управляет растровым изображением, Manipulate
вызывает OneFingerManipulate
метод TouchManipulationManager
объекта. Для двух пальцев он вызывается TwoFingerManipulate
. Аргументы этих методов одинаковы: prevPoint
аргументы newPoint
представляют пальцем, который перемещается. pivotPoint
Но аргумент отличается для двух вызовов:
Для манипуляции pivotPoint
с одним пальцем центр растрового изображения. Это позволяет вращать один пальцем. Для двухфакторной манипуляции событие указывает на движение только одного пальца, чтобы pivotPoint
это палец, который не двигался.
В обоих случаях TouchManipulationManager
возвращает SKMatrix
значение, которое метод объединяет с текущим Matrix
свойством, которое TouchManipulationPage
используется для отрисовки растрового изображения.
TouchManipulationManager
является обобщенным и не использует другие файлы, кроме TouchManipulationMode
. Вы можете использовать этот класс без изменений в собственных приложениях. Он определяет единственное свойство типа TouchManipulationMode
.
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
...
}
Тем не менее, вы, вероятно, захотите избежать AnisotropicScale
этого варианта. Это очень легко с этим параметром для управления растровым изображением, чтобы один из факторов масштабирования стал нулем. Это делает растровое изображение исчезать из зрения, никогда не возвращать. Если вам действительно нужна анисотропная масштабирование, вы хотите улучшить логику, чтобы избежать нежелательных результатов.
TouchManipulationManager
использует векторы, но так как SKVector
нет структуры в SkiaSharp, SKPoint
вместо этого используется. SKPoint
поддерживает оператор вычитания, а результат можно рассматривать как вектор. Единственной логикой, необходимой для добавления вектора, является вычисление Magnitude
:
class TouchManipulationManager
{
...
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
}
При каждом выборе поворота методы обработки с одним пальцем и двумя пальцами обрабатывают поворот первым. Если обнаружена любая смена, компонент поворота будет эффективно удален. То, что остается, интерпретируется как сдвиг и масштабирование.
OneFingerManipulate
Вот метод. Если поворот с одним пальцем не включен, логика проста— она просто использует предыдущую точку и новую точку для создания вектора delta
, соответствующего точно переводу. При включенном повороте с одним пальцем метод использует уголы с точки сводных точек (в центре растрового изображения) до предыдущей точки и новой точки для создания матрицы поворота:
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
public SKMatrix OneFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
{
if (Mode == TouchManipulationMode.None)
{
return SKMatrix.MakeIdentity();
}
SKMatrix touchMatrix = SKMatrix.MakeIdentity();
SKPoint delta = newPoint - prevPoint;
if (Mode == TouchManipulationMode.ScaleDualRotate) // One-finger rotation
{
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
// Avoid rotation if fingers are too close to center
if (Magnitude(newVector) > 25 && Magnitude(oldVector) > 25)
{
float prevAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
// Calculate rotation matrix
float angle = newAngle - prevAngle;
touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
// Effectively rotate the old vector
float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
oldVector.X = magnitudeRatio * newVector.X;
oldVector.Y = magnitudeRatio * newVector.Y;
// Recalculate delta
delta = newVector - oldVector;
}
}
// Multiply the rotation matrix by a translation matrix
SKMatrix.PostConcat(ref touchMatrix, SKMatrix.MakeTranslation(delta.X, delta.Y));
return touchMatrix;
}
...
}
В методе TwoFingerManipulate
точка сводных данных — это позиция пальца, который не перемещается в этом конкретном событии касания. Поворот очень похож на поворот одним пальцем, а затем вектор с именем oldVector
(на основе предыдущей точки) корректируется для поворота. Оставшееся движение интерпретируется как масштабирование:
class TouchManipulationManager
{
...
public SKMatrix TwoFingerManipulate(SKPoint prevPoint, SKPoint newPoint, SKPoint pivotPoint)
{
SKMatrix touchMatrix = SKMatrix.MakeIdentity();
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
if (Mode == TouchManipulationMode.ScaleRotate ||
Mode == TouchManipulationMode.ScaleDualRotate)
{
// Find angles from pivot point to touch points
float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);
// Calculate rotation matrix
float angle = newAngle - oldAngle;
touchMatrix = SKMatrix.MakeRotation(angle, pivotPoint.X, pivotPoint.Y);
// Effectively rotate the old vector
float magnitudeRatio = Magnitude(oldVector) / Magnitude(newVector);
oldVector.X = magnitudeRatio * newVector.X;
oldVector.Y = magnitudeRatio * newVector.Y;
}
float scaleX = 1;
float scaleY = 1;
if (Mode == TouchManipulationMode.AnisotropicScale)
{
scaleX = newVector.X / oldVector.X;
scaleY = newVector.Y / oldVector.Y;
}
else if (Mode == TouchManipulationMode.IsotropicScale ||
Mode == TouchManipulationMode.ScaleRotate ||
Mode == TouchManipulationMode.ScaleDualRotate)
{
scaleX = scaleY = Magnitude(newVector) / Magnitude(oldVector);
}
if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
!float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
{
SKMatrix.PostConcat(ref touchMatrix,
SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y));
}
return touchMatrix;
}
...
}
Вы заметите, что в этом методе нет явного перевода. Однако оба MakeRotation
MakeScale
метода основаны на точке сводных данных и включают неявный перевод. Если вы используете два пальца на растровом рисунке и перетаскиваете их в одном направлении, TouchManipulation
получите ряд событий касания, чередующихся между двумя пальцами. По мере того как каждый палец перемещается относительно другого, масштабирование или поворот результатов, но он отрицается движением другого пальца, и результатом является перевод.
Единственная оставшаяся часть страницы обработки сенсорного ввода является PaintSurface
обработчиком TouchManipulationPage
в файле программной части. Этот метод вызывает Paint
метод TouchManipulationBitmap
, который применяет матрицу, представляющую накопленные сенсорные действия:
public partial class TouchManipulationPage : ContentPage
{
...
MatrixDisplay matrixDisplay = new MatrixDisplay();
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Display the bitmap
bitmap.Paint(canvas);
// Display the matrix in the lower-right corner
SKSize matrixSize = matrixDisplay.Measure(bitmap.Matrix);
matrixDisplay.Paint(canvas, bitmap.Matrix,
new SKPoint(info.Width - matrixSize.Width,
info.Height - matrixSize.Height));
}
}
Обработчик PaintSurface
завершается отображением MatrixDisplay
объекта, показывающего накапливаемую матрицу сенсорного ввода:
Управление несколькими растровыми картами
Одним из преимуществ изоляции кода сенсорной обработки в таких классах, как TouchManipulationBitmap
и TouchManipulationManager
возможность повторного использования этих классов в программе, которая позволяет пользователю управлять несколькими растровыми изображениями.
На странице точечного представления растрового изображения показано, как это делается. Вместо определения поля типа TouchManipulationBitmap
BitmapScatterPage
класс определяет List
объекты растрового изображения:
public partial class BitmapScatterViewPage : ContentPage
{
List<TouchManipulationBitmap> bitmapCollection =
new List<TouchManipulationBitmap>();
...
public BitmapScatterViewPage()
{
InitializeComponent();
// Load in all the available bitmaps
Assembly assembly = GetType().GetTypeInfo().Assembly;
string[] resourceIDs = assembly.GetManifestResourceNames();
SKPoint position = new SKPoint();
foreach (string resourceID in resourceIDs)
{
if (resourceID.EndsWith(".png") ||
resourceID.EndsWith(".jpg"))
{
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
SKBitmap bitmap = SKBitmap.Decode(stream);
bitmapCollection.Add(new TouchManipulationBitmap(bitmap)
{
Matrix = SKMatrix.MakeTranslation(position.X, position.Y),
});
position.X += 100;
position.Y += 100;
}
}
}
}
...
}
Конструктор загружает все растровые изображения, доступные как внедренные ресурсы, и добавляет их в bitmapCollection
. Обратите внимание, что Matrix
свойство инициализировано для каждого объекта, поэтому верхние левого угла каждой TouchManipulationBitmap
растровой карты смещаются на 100 пикселей.
Страница BitmapScatterView
также должна обрабатывать события касания для нескольких растровых изображений. Вместо определения List
идентификаторов сенсорных идентификаторов объектов, управляемых TouchManipulationBitmap
в настоящее время, для этой программы требуется словарь:
public partial class BitmapScatterViewPage : ContentPage
{
...
Dictionary<long, TouchManipulationBitmap> bitmapDictionary =
new Dictionary<long, TouchManipulationBitmap>();
...
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
for (int i = bitmapCollection.Count - 1; i >= 0; i--)
{
TouchManipulationBitmap bitmap = bitmapCollection[i];
if (bitmap.HitTest(point))
{
// Move bitmap to end of collection
bitmapCollection.Remove(bitmap);
bitmapCollection.Add(bitmap);
// Do the touch processing
bitmapDictionary.Add(args.Id, bitmap);
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
canvasView.InvalidateSurface();
break;
}
}
break;
case TouchActionType.Moved:
if (bitmapDictionary.ContainsKey(args.Id))
{
TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
canvasView.InvalidateSurface();
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (bitmapDictionary.ContainsKey(args.Id))
{
TouchManipulationBitmap bitmap = bitmapDictionary[args.Id];
bitmap.ProcessTouchEvent(args.Id, args.Type, point);
bitmapDictionary.Remove(args.Id);
canvasView.InvalidateSurface();
}
break;
}
}
...
}
Обратите внимание, как логика Pressed
выполняется в обратном bitmapCollection
направлении. Растровые изображения часто пересекаются друг с другом. Растровые рисунки позже в коллекции визуально лежат на вершине растровых изображений ранее в коллекции. Если под пальцем есть несколько растровых изображений, которые нажимают на экране, самый верхний должен быть тот, который управляется этим пальцем.
Кроме того, обратите внимание, что логика Pressed
перемещает растровое изображение в конец коллекции, чтобы он визуально перемещается в верхнюю часть кучи других растровых изображений.
Moved
В и Released
событиях обработчик вызывает ProcessingTouchEvent
метод TouchManipulationBitmap
так же, TouchAction
как и предыдущая программа.
Наконец, PaintSurface
обработчик вызывает Paint
метод каждого TouchManipulationBitmap
объекта:
public partial class BitmapScatterViewPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKCanvas canvas = args.Surface.Canvas;
canvas.Clear();
foreach (TouchManipulationBitmap bitmap in bitmapCollection)
{
bitmap.Paint(canvas);
}
}
}
Код циклит по коллекции и отображает кучу растровых изображений с начала коллекции до конца:
Масштабирование с одним пальцем
Для операции масштабирования обычно требуется жест сцепления с двумя пальцами. Однако можно реализовать масштабирование одним пальцем, переместив угол растрового изображения.
Это показано на странице масштабирования одного угла пальца. Так как в этом примере используется несколько другой тип масштабирования, чем реализованный в TouchManipulationManager
классе, он не использует этот класс или TouchManipulationBitmap
класс. Вместо этого все логики сенсорного ввода находится в файле программной части. Это несколько проще логики, чем обычно, потому что отслеживает только один палец за раз, и просто игнорирует все вторичные пальцы, которые могут касаться экрана.
Страница SingleFingerCornerScale.xaml создает экземпляр SKCanvasView
класса и создает TouchEffect
объект для отслеживания событий касания:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Transforms.SingleFingerCornerScalePage"
Title="Single Finger Corner Scale">
<Grid BackgroundColor="White"
Grid.Row="1">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</ContentPage>
Файл SingleFingerCornerScalePage.xaml.cs загружает ресурс растрового изображения из каталога мультимедиа и отображает его с помощью объекта, определенного SKMatrix
как поле:
public partial class SingleFingerCornerScalePage : ContentPage
{
SKBitmap bitmap;
SKMatrix currentMatrix = SKMatrix.MakeIdentity();
···
public SingleFingerCornerScalePage()
{
InitializeComponent();
string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
bitmap = SKBitmap.Decode(stream);
}
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
canvas.SetMatrix(currentMatrix);
canvas.DrawBitmap(bitmap, 0, 0);
}
···
}
Этот SKMatrix
объект изменяется логикой сенсорного ввода, показанной ниже.
Оставшаяся часть файла кода находится в обработчике TouchEffect
событий. Он начинается с преобразования текущего расположения пальца в SKPoint
значение. Pressed
Для типа действия обработчик проверка, что другой палец не касается экрана, и что палец находится в пределах растрового изображения.
Важной частью кода является if
инструкция, включающая два вызова Math.Pow
метода. Эта математика проверка, если расположение пальца находится за пределами многоточия, заполняющего растровое изображение. Если да, то это операция масштабирования. Пальцем находится рядом с одним из углов растрового изображения, а точка сводных точек определяется противоположным углом. Если палец находится внутри этого многоточия, это обычная операция сдвига:
public partial class SingleFingerCornerScalePage : ContentPage
{
SKBitmap bitmap;
SKMatrix currentMatrix = SKMatrix.MakeIdentity();
// Information for translating and scaling
long? touchId = null;
SKPoint pressedLocation;
SKMatrix pressedMatrix;
// Information for scaling
bool isScaling;
SKPoint pivotPoint;
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
// Convert Xamarin.Forms point to pixels
Point pt = args.Location;
SKPoint point =
new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
switch (args.Type)
{
case TouchActionType.Pressed:
// Track only one finger
if (touchId.HasValue)
return;
// Check if the finger is within the boundaries of the bitmap
SKRect rect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
rect = currentMatrix.MapRect(rect);
if (!rect.Contains(point))
return;
// First assume there will be no scaling
isScaling = false;
// If touch is outside interior ellipse, make this a scaling operation
if (Math.Pow((point.X - rect.MidX) / (rect.Width / 2), 2) +
Math.Pow((point.Y - rect.MidY) / (rect.Height / 2), 2) > 1)
{
isScaling = true;
float xPivot = point.X < rect.MidX ? rect.Right : rect.Left;
float yPivot = point.Y < rect.MidY ? rect.Bottom : rect.Top;
pivotPoint = new SKPoint(xPivot, yPivot);
}
// Common for either pan or scale
touchId = args.Id;
pressedLocation = point;
pressedMatrix = currentMatrix;
break;
case TouchActionType.Moved:
if (!touchId.HasValue || args.Id != touchId.Value)
return;
SKMatrix matrix = SKMatrix.MakeIdentity();
// Translating
if (!isScaling)
{
SKPoint delta = point - pressedLocation;
matrix = SKMatrix.MakeTranslation(delta.X, delta.Y);
}
// Scaling
else
{
float scaleX = (point.X - pivotPoint.X) / (pressedLocation.X - pivotPoint.X);
float scaleY = (point.Y - pivotPoint.Y) / (pressedLocation.Y - pivotPoint.Y);
matrix = SKMatrix.MakeScale(scaleX, scaleY, pivotPoint.X, pivotPoint.Y);
}
// Concatenate the matrices
SKMatrix.PreConcat(ref matrix, pressedMatrix);
currentMatrix = matrix;
canvasView.InvalidateSurface();
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
touchId = null;
break;
}
}
}
Тип Moved
действия вычисляет матрицу, соответствующую сенсорному действию с момента нажатия пальца на экран до этого времени. Он объединяет матрицу с матрицей в действии в то время, когда палец сначала нажимал растровое изображение. Операция масштабирования всегда относительно угла, противоположного тому, который касался пальца.
Для небольших или продолговатых растровых изображений многоточие может занять большую часть растрового изображения и оставить крошечные области в углах, чтобы масштабировать растровое изображение. Вы можете использовать несколько другой подход, в этом случае можно заменить весь if
блок, который задает isScaling
значение true
с помощью этого кода:
float halfHeight = rect.Height / 2;
float halfWidth = rect.Width / 2;
// Top half of bitmap
if (point.Y < rect.MidY)
{
float yRelative = (point.Y - rect.Top) / halfHeight;
// Upper-left corner
if (point.X < rect.MidX - yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Right, rect.Bottom);
}
// Upper-right corner
else if (point.X > rect.MidX + yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Left, rect.Bottom);
}
}
// Bottom half of bitmap
else
{
float yRelative = (point.Y - rect.MidY) / halfHeight;
// Lower-left corner
if (point.X < rect.Left + yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Right, rect.Top);
}
// Lower-right corner
else if (point.X > rect.Right - yRelative * halfWidth)
{
isScaling = true;
pivotPoint = new SKPoint(rect.Left, rect.Top);
}
}
Этот код эффективно делит область растрового изображения на внутреннюю фигуру бриллианта и четыре треугольника в углах. Это позволяет гораздо больше областей в углах захватывать и масштабировать растровое изображение.