Manipulaciones táctiles
Usar transformaciones de matriz para implementar el arrastre táctil, el pellizco y la rotación
En entornos multitáctil, como los de dispositivos móviles, los usuarios suelen usar sus dedos para manipular objetos en la pantalla. Los gestos comunes, como un arrastre de un dedo y una pellizcar de dos dedos, pueden moverse y escalar objetos, o incluso girarlos. Estos gestos se implementan generalmente mediante matrices de transformación y en este artículo se muestra cómo hacerlo.
Todos los ejemplos que se muestran aquí usan el Xamarin.Forms efecto de seguimiento táctil presentado en el artículo Invocación de eventos de efectos.
Arrastrar y traducir
Una de las aplicaciones más importantes de las transformaciones de matriz es el procesamiento táctil. Un único SKMatrix
valor puede consolidar una serie de operaciones táctiles.
Para arrastrar un solo dedo, el valor realiza la SKMatrix
traducción. Esto se muestra en la página Arrastre de mapa de bits . El archivo XAML crea SKCanvasView
una instancia de en un Xamarin.FormsGrid
objeto . Se ha agregado un TouchEffect
objeto a la Effects
colección de que 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>
En teoría, el TouchEffect
objeto se podría agregar directamente a la Effects
colección de SKCanvasView
, pero eso no funciona en todas las plataformas. SKCanvasView
Dado que tiene el mismo tamaño que en Grid
esta configuración, adjuntarlo también a Grid
funciona.
El archivo de código subyacente se carga en un recurso de mapa de bits en su constructor y lo muestra en el PaintSurface
controlador:
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());
}
}
Sin ningún código adicional, el SKMatrix
valor siempre es la matriz de identificación y no tendría ningún efecto en la presentación del mapa de bits. El objetivo del controlador establecido en el archivo XAML es modificar el valor de OnTouchEffectAction
matriz para reflejar las manipulaciones táctiles.
El OnTouchEffectAction
controlador comienza convirtiendo el Xamarin.FormsPoint
valor en un valor SkiaSharp SKPoint
. Se trata de una cuestión sencilla de escalado en función de las Width
propiedades y Height
de SKCanvasView
(que son unidades independientes del dispositivo) y la CanvasSize
propiedad , que se encuentra en unidades de píxeles:
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;
}
}
···
}
Cuando un dedo toca por primera vez la pantalla, se desencadena un evento de tipo TouchActionType.Pressed
. La primera tarea es determinar si el dedo está tocando el mapa de bits. Esta tarea suele denominar pruebas de posicionamiento. En este caso, las pruebas de posicionamiento se pueden realizar creando un SKRect
valor correspondiente al mapa de bits, aplicando la transformación de matriz a ella con MapRect
y, a continuación, determinar si el punto táctil está dentro del rectángulo transformado.
Si ese es el caso, el touchId
campo se establece en el identificador táctil y se guarda la posición del dedo.
Para el TouchActionType.Moved
evento, los factores de traducción del SKMatrix
valor se ajustan en función de la posición actual del dedo y la nueva posición del dedo. Esa nueva posición se guarda para la próxima vez y SKCanvasView
se invalida.
A medida que experimente con este programa, tenga en cuenta que solo puede arrastrar el mapa de bits cuando el dedo toca un área donde se muestra el mapa de bits. Aunque esa restricción no es muy importante para este programa, se convierte en crucial al manipular varios mapas de bits.
Reducir y escalar
¿Qué quieres pasar cuando dos dedos tocan el mapa de bits? Si los dos dedos se mueven en paralelo, es probable que desee que el mapa de bits se mueva junto con los dedos. Si los dos dedos realizan una operación de pellizcar o estirar, es posible que quiera que se gire el mapa de bits (que se describirá en la sección siguiente) o se escale. Al escalar un mapa de bits, tiene más sentido que los dos dedos permanezcan en las mismas posiciones relativas al mapa de bits y que el mapa de bits se escale según corresponda.
El control de dos dedos a la vez parece complicado, pero tenga en cuenta que el TouchAction
controlador solo recibe información sobre un dedo a la vez. Si dos dedos manipulan el mapa de bits, para cada evento, un dedo ha cambiado de posición, pero el otro no ha cambiado. En el código de página Escalado de mapa de bits siguiente, el dedo que no ha cambiado de posición se denomina punto de dinamización porque la transformación es relativa a ese punto.
Una diferencia entre este programa y el programa anterior es que se deben guardar varios identificadores táctiles. Se usa un diccionario para este fin, donde el id. táctil es la clave del diccionario y el valor del diccionario es la posición actual de ese dedo:
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;
}
}
···
}
El control de la Pressed
acción es casi el mismo que el programa anterior, excepto que el identificador y el punto táctil se agregan al diccionario. Las Released
acciones y Cancelled
quitan la entrada del diccionario.
Sin embargo, el control de la Moved
acción es más complejo. Si solo hay un dedo implicado, el procesamiento es muy igual que el programa anterior. Para dos o más dedos, el programa también debe obtener información del diccionario que implica el dedo que no se mueve. Para ello, copia las claves del diccionario en una matriz y, a continuación, compara la primera clave con el identificador del dedo que se va a mover. Esto permite al programa obtener el punto de pivote correspondiente al dedo que no se mueve.
A continuación, el programa calcula dos vectores de la nueva posición del dedo en relación con el punto de pivote y la posición del dedo anterior en relación con el punto de pivote. Las relaciones de estos vectores son factores de escalado. Dado que la división por cero es una posibilidad, se deben comprobar si hay valores infinitos o valores NaN (no un número). Si todo está bien, una transformación de escalado se concatena con el SKMatrix
valor guardado como campo.
A medida que experimente con esta página, observará que puede arrastrar el mapa de bits con uno o dos dedos, o escalarlo con dos dedos. El escalado es anisotrópico, lo que significa que el escalado puede ser diferente en las direcciones horizontales y verticales. Esto distorsiona la relación de aspecto, pero también permite voltear el mapa de bits para crear una imagen reflejada. También puede detectar que puede reducir el mapa de bits a una dimensión cero y desaparece. En el código de producción, querrá protegerse de esto.
Rotación de dos dedos
La página Rotación de mapa de bits le permite usar dos dedos para la rotación o el escalado isotrópico. El mapa de bits siempre conserva su relación de aspecto correcta. El uso de dos dedos para la rotación y el escalado anisotrópico no funciona muy bien porque el movimiento de los dedos es muy similar para ambas tareas.
La primera gran diferencia en este programa es la lógica de pruebas de posicionamiento. Los programas anteriores usaron el Contains
método de SKRect
para determinar si el punto táctil está dentro del rectángulo transformado que corresponde al mapa de bits. Pero a medida que el usuario manipula el mapa de bits, el mapa de bits puede girarse y SKRect
no puede representar correctamente un rectángulo girado. Es posible que tenga miedo de que la lógica de pruebas de posicionamiento tenga que implementar geometría analítica bastante compleja en ese caso.
Sin embargo, hay disponible un acceso directo: determinar si un punto se encuentra dentro de los límites de un rectángulo transformado es el mismo que determinar si un punto transformado inverso se encuentra dentro de los límites del rectángulo sin transformar. Este es un cálculo mucho más sencillo y la lógica puede seguir usando el método práctico 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));
}
···
}
La lógica del Moved
evento se inicia como el programa anterior. Dos vectores denominados oldVector
y newVector
se calculan en función del punto anterior y actual del dedo móvil y el punto de pivote del dedo desenmoving. Sin embargo, se determinan los ángulos de estos vectores y la diferencia es el ángulo de rotación.
El escalado también puede estar implicado, por lo que el vector antiguo se gira en función del ángulo de rotación. La magnitud relativa de los dos vectores es ahora el factor de escalado. Observe que el mismo scale
valor se usa para el escalado horizontal y vertical para que el escalado sea isotrópico. El matrix
campo se ajusta mediante la matriz de rotación y una matriz de escala.
Si la aplicación necesita implementar el procesamiento táctil para un solo mapa de bits (u otro objeto), puede adaptar el código de estos tres ejemplos para su propia aplicación. Pero si necesita implementar el procesamiento táctil para varios mapas de bits, probablemente querrá encapsular estas operaciones táctiles en otras clases.
Encapsular las operaciones táctiles
En la página Manipulación táctil se muestra la manipulación táctil de un solo mapa de bits, pero se usan otros archivos que encapsulan gran parte de la lógica mostrada anteriormente. El primero de estos archivos es la TouchManipulationMode
enumeración, que indica los diferentes tipos de manipulación táctil implementadas por el código que verá:
enum TouchManipulationMode
{
None,
PanOnly,
IsotropicScale, // includes panning
AnisotropicScale, // includes panning
ScaleRotate, // implies isotropic scaling
ScaleDualRotate // adds one-finger rotation
}
PanOnly
es un arrastre de un dedo que se implementa con traducción. Todas las opciones posteriores también incluyen movimiento panorámico, pero implican dos dedos: IsotropicScale
es una operación de pellizcar que da como resultado el escalado de objetos igualmente en las direcciones horizontales y verticales. AnisotropicScale
permite un escalado desigual.
La ScaleRotate
opción es para el escalado y la rotación de dos dedos. El escalado es isotrópico. Como se mencionó anteriormente, la implementación de la rotación de dos dedos con el escalado anisotrópico es problemática porque los movimientos del dedo son esencialmente los mismos.
La ScaleDualRotate
opción agrega rotación de un dedo. Cuando un solo dedo arrastra el objeto, el objeto arrastrado se gira primero alrededor de su centro para que el centro del objeto se alinea con el vector de arrastre.
El archivo TouchManipulationPage.xaml incluye un Picker
elemento con los miembros de la TouchManipulationMode
enumeración:
<?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>
Hacia la parte inferior hay un SKCanvasView
elemento y un TouchEffect
adjunto a la celda Grid
única que lo incluye.
El archivo de código subyacente TouchManipulationPage.xaml.cs tiene un bitmap
campo, pero no es de tipo SKBitmap
. El tipo es TouchManipulationBitmap
(una clase que verá en breve):
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;
}
}
...
}
El constructor crea una instancia de un TouchManipulationBitmap
objeto , pasando al constructor un SKBitmap
obtenido de un recurso incrustado. El constructor concluye estableciendo la Mode
propiedad de la TouchManager
propiedad del TouchManipulationBitmap
objeto en un miembro de la TouchManipulationMode
enumeración.
El SelectedIndexChanged
controlador de también Picker
establece esta Mode
propiedad:
public partial class TouchManipulationPage : ContentPage
{
...
void OnTouchModePickerSelectedIndexChanged(object sender, EventArgs args)
{
if (bitmap != null)
{
Picker picker = (Picker)sender;
bitmap.TouchManager.Mode = (TouchManipulationMode)picker.SelectedItem;
}
}
...
}
El TouchAction
controlador de la TouchEffect
instancia de en el archivo XAML llama a dos métodos en TouchManipulationBitmap
denominados HitTest
y 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;
}
}
...
}
Si el HitTest
método devuelve true
(lo que significa que un dedo ha tocado la pantalla dentro del área ocupada por el mapa de bits), el identificador táctil se agrega a la TouchIds
colección. Este identificador representa la secuencia de eventos táctiles de ese dedo hasta que el dedo se levanta desde la pantalla. Si varios dedos tocan el mapa de bits, la touchIds
colección contiene un identificador táctil para cada dedo.
El TouchAction
controlador también llama a la ProcessTouchEvent
clase en TouchManipulationBitmap
. Aquí es donde se producen algunos (pero no todos) del procesamiento táctil real.
La TouchManipulationBitmap
clase es una clase contenedora para SKBitmap
que contiene código para representar el mapa de bits y procesar eventos táctiles. Funciona junto con código más generalizado en una TouchManipulationManager
clase (que verá en breve).
El TouchManipulationBitmap
constructor guarda y SKBitmap
crea una instancia de dos propiedades, la TouchManager
propiedad de tipo TouchManipulationManager
y la Matrix
propiedad de tipo 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; }
...
}
Esta Matrix
propiedad es la transformación acumulada resultante de toda la actividad táctil. Como verá, cada evento táctil se resuelve en una matriz, que luego se concatena con el SKMatrix
valor almacenado por la Matrix
propiedad .
El TouchManipulationBitmap
objeto se dibuja en su Paint
método . El argumento es un SKCanvas
objeto . Esto SKCanvas
podría tener ya aplicada una transformación, por lo que el Paint
método concatena la Matrix
propiedad asociada con el mapa de bits a la transformación existente y restaura el lienzo cuando haya terminado:
class TouchManipulationBitmap
{
...
public void Paint(SKCanvas canvas)
{
canvas.Save();
SKMatrix matrix = Matrix;
canvas.Concat(ref matrix);
canvas.DrawBitmap(bitmap, 0, 0);
canvas.Restore();
}
...
}
El HitTest
método devuelve true
si el usuario toca la pantalla en un punto dentro de los límites del mapa de bits. Esto usa la lógica mostrada anteriormente en la página Rotación de mapa de bits :
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;
}
...
}
El segundo método público de TouchManipulationBitmap
es ProcessTouchEvent
. Cuando se llama a este método, ya se ha establecido que el evento táctil pertenece a este mapa de bits concreto. El método mantiene un diccionario de TouchManipulationInfo
objetos, que es simplemente el punto anterior y el nuevo punto de cada dedo:
class TouchManipulationInfo
{
public SKPoint PreviousPoint { set; get; }
public SKPoint NewPoint { set; get; }
}
Este es el diccionario y el ProcessTouchEvent
propio método:
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;
}
}
...
}
En los Moved
eventos y Released
, el método llama a Manipulate
. En estos momentos, contiene touchDictionary
uno o varios TouchManipulationInfo
objetos. touchDictionary
Si contiene un elemento, es probable que los PreviousPoint
valores y NewPoint
sean distintos y representen el movimiento de un dedo. Si varios dedos tocan el mapa de bits, el diccionario contiene más de un elemento, pero solo uno de estos elementos tiene valores y NewPoint
diferentesPreviousPoint
. Todos los demás tienen valores iguales PreviousPoint
y NewPoint
.
Esto es importante: El Manipulate
método puede suponer que está procesando el movimiento de un solo dedo. En el momento de esta llamada ninguno de los otros dedos se mueven, y si realmente se mueven (como es probable), esos movimientos se procesarán en llamadas futuras a Manipulate
.
El Manipulate
método copia primero el diccionario en una matriz para mayor comodidad. Omite algo distinto de las dos primeras entradas. Si hay más de dos dedos intentando manipular el mapa de bits, se omiten los demás. Manipulate
es el miembro final de 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;
}
}
Si un dedo está manipulando el mapa de bits, Manipulate
llama al OneFingerManipulate
método del TouchManipulationManager
objeto . Para dos dedos, llama a TwoFingerManipulate
. Los argumentos de estos métodos son los mismos: los prevPoint
argumentos y newPoint
representan el dedo que se mueve. Pero el pivotPoint
argumento es diferente para las dos llamadas:
Para la manipulación de un dedo, pivotPoint
es el centro del mapa de bits. Esto es para permitir la rotación de un dedo. Para la manipulación de dos dedos, el evento indica el movimiento de un solo dedo, de modo que pivotPoint
es el dedo que no se mueve.
En ambos casos, TouchManipulationManager
devuelve un SKMatrix
valor, que el método concatena con la propiedad actual Matrix
que TouchManipulationPage
usa para representar el mapa de bits.
TouchManipulationManager
se generaliza y no usa ningún otro archivo excepto TouchManipulationMode
. Es posible que pueda usar esta clase sin cambios en sus propias aplicaciones. Define una única propiedad de tipo TouchManipulationMode
:
class TouchManipulationManager
{
public TouchManipulationMode Mode { set; get; }
...
}
Sin embargo, probablemente querrá evitar la AnisotropicScale
opción . Es muy fácil con esta opción manipular el mapa de bits para que uno de los factores de escalado se convierta en cero. Esto hace que el mapa de bits desaparezca de la vista, nunca vuelva. Si realmente necesita escalado anisotrópico, querrá mejorar la lógica para evitar resultados no deseados.
TouchManipulationManager
hace uso de vectores, pero dado que no hay ninguna SKVector
estructura en SkiaSharp, SKPoint
se usa en su lugar. SKPoint
admite el operador de resta y el resultado se puede tratar como un vector. La única lógica específica del vector que se debe agregar es un Magnitude
cálculo:
class TouchManipulationManager
{
...
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
}
Cada vez que se selecciona la rotación, los métodos de manipulación de un dedo y dos dedos controlan primero la rotación. Si se detecta alguna rotación, el componente de rotación se quita eficazmente. Lo que queda se interpreta como movimiento panorámico y escalado.
Este es el OneFingerManipulate
método . Si no se ha habilitado la rotación de un dedo, la lógica es sencilla, simplemente usa el punto anterior y el nuevo punto para construir un vector denominado delta
que corresponde precisamente a la traducción. Con la rotación de un dedo habilitada, el método usa ángulos desde el punto dinámico (el centro del mapa de bits) hasta el punto anterior y el nuevo punto para construir una matriz de rotación:
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;
}
...
}
En el TwoFingerManipulate
método , el punto de pivote es la posición del dedo que no se mueve en este evento táctil en particular. La rotación es muy similar a la rotación de un dedo y, a continuación, el vector denominado oldVector
(basado en el punto anterior) se ajusta para la rotación. El movimiento restante se interpreta como escalado:
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;
}
...
}
Observará que no hay ninguna traducción explícita en este método. Sin embargo, tanto los MakeRotation
métodos como MakeScale
se basan en el punto de pivote y que incluye la traducción implícita. Si usa dos dedos en el mapa de bits y los arrastra en la misma dirección, TouchManipulation
obtendrá una serie de eventos táctiles que alternan entre los dos dedos. A medida que cada dedo se mueve en relación con el otro, escalado o resultados de rotación, pero se niega por el movimiento del otro dedo, y el resultado es la traducción.
La única parte restante de la página Manipulación táctil es el PaintSurface
controlador en el archivo de TouchManipulationPage
código subyacente. Esto llama al Paint
método de TouchManipulationBitmap
, que aplica la matriz que representa la actividad táctil acumulada:
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));
}
}
El PaintSurface
controlador concluye mostrando un MatrixDisplay
objeto que muestra la matriz táctil acumulada:
Manipular varios mapas de bits
Una de las ventajas de aislar el código de procesamiento táctil en clases como TouchManipulationBitmap
y TouchManipulationManager
es la capacidad de reutilizar estas clases en un programa que permite al usuario manipular varios mapas de bits.
La página Vista de dispersión de mapa de bits muestra cómo se hace esto. En lugar de definir un campo de tipo TouchManipulationBitmap
, la BitmapScatterPage
clase define un List
de objetos de mapa de bits:
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;
}
}
}
}
...
}
El constructor carga en todos los mapas de bits disponibles como recursos incrustados y los agrega a bitmapCollection
. Observe que la Matrix
propiedad se inicializa en cada TouchManipulationBitmap
objeto, por lo que las esquinas superior izquierda de cada mapa de bits se desplazan por 100 píxeles.
La BitmapScatterView
página también debe controlar eventos táctiles para varios mapas de bits. En lugar de definir un List
identificador táctil de objetos manipulados TouchManipulationBitmap
actualmente, este programa requiere un diccionario:
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;
}
}
...
}
Observe cómo la Pressed
lógica recorre en bucle a bitmapCollection
la inversa. Los mapas de bits a menudo se superponen entre sí. Los mapas de bits más adelante en la colección se encuentran visualmente encima de los mapas de bits anteriores en la colección. Si hay varios mapas de bits bajo el dedo que presiona en la pantalla, el más alto debe ser el que manipula ese dedo.
Observe también que la lógica mueve ese Pressed
mapa de bits al final de la colección para que se mueva visualmente a la parte superior de la pila de otros mapas de bits.
En los Moved
eventos y Released
, el TouchAction
controlador llama al ProcessingTouchEvent
método en TouchManipulationBitmap
igual que el programa anterior.
Por último, el PaintSurface
controlador llama al Paint
método de cada TouchManipulationBitmap
objeto:
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);
}
}
}
El código recorre la colección y muestra la pila de mapas de bits desde el principio de la colección hasta el final:
escalado de Single-Finger
Por lo general, una operación de escalado requiere un gesto de reducir con dos dedos. Sin embargo, es posible implementar el escalado con un solo dedo haciendo que el dedo mueva las esquinas de un mapa de bits.
Esto se muestra en la página Escala de esquina de un solo dedo . Dado que en este ejemplo se usa un tipo de escalado ligeramente diferente al implementado en la TouchManipulationManager
clase , no usa esa clase ni la TouchManipulationBitmap
clase . En su lugar, toda la lógica táctil está en el archivo de código subyacente. Esta es una lógica algo más sencilla de lo habitual porque realiza un seguimiento de un solo dedo a la vez y simplemente omite los dedos secundarios que puedan estar tocando la pantalla.
La página SingleFingerCornerScale.xaml crea una instancia de la SKCanvasView
clase y crea un objeto para realizar el TouchEffect
seguimiento de eventos táctiles:
<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>
El archivo SingleFingerCornerScalePage.xaml.cs carga un recurso de mapa de bits desde el directorio Media y lo muestra mediante un SKMatrix
objeto definido como campo:
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);
}
···
}
Este SKMatrix
objeto se modifica mediante la lógica táctil que se muestra a continuación.
El resto del archivo de código subyacente es el TouchEffect
controlador de eventos. Comienza convirtiendo la ubicación actual del dedo en un SKPoint
valor. Para el Pressed
tipo de acción, el controlador comprueba que ningún otro dedo está tocando la pantalla y que el dedo está dentro de los límites del mapa de bits.
La parte fundamental del código es una if
instrucción que implica dos llamadas al Math.Pow
método . Esta matemática comprueba si la ubicación del dedo está fuera de una elipse que rellena el mapa de bits. Si es así, se trata de una operación de escalado. El dedo está cerca de una de las esquinas del mapa de bits y se determina un punto de pivote que es la esquina opuesta. Si el dedo está dentro de esta elipse, es una operación de movimiento panorámico normal:
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;
}
}
}
El Moved
tipo de acción calcula una matriz correspondiente a la actividad táctil desde el momento en que el dedo presionó la pantalla hasta esta vez. Concatena esa matriz con la matriz en vigor en el momento en que el dedo presionó por primera vez el mapa de bits. La operación de escalado siempre es relativa a la esquina opuesta a la que tocó el dedo.
Para mapas de bits pequeños o oblong, una elipse interior puede ocupar la mayoría del mapa de bits y dejar áreas pequeñas en las esquinas para escalar el mapa de bits. Es posible que prefiera un enfoque ligeramente diferente, en cuyo caso puede reemplazar ese bloque completo if
que establece isScaling
en true
por este código:
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);
}
}
Este código divide eficazmente el área del mapa de bits en una forma de diamante interior y cuatro triángulos en las esquinas. Esto permite que las áreas mucho más grandes de las esquinas grabe y escale el mapa de bits.