Manipulaciones táctiles
Usar transformaciones de matriz para implementar el arrastre táctil, el pinzamiento 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 pellizcar con dos dedos, pueden mover 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 efecto de seguimiento táctil Xamarin.Forms 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 valor SKMatrix
puede consolidar una serie de operaciones táctiles.
Para arrastrar un solo dedo, el valor SKMatrix
realiza la traducción. Esto se muestra en la página Arrastrar mapa de bits. El archivo XAML crea una instancia SKCanvasView
en un Grid
Xamarin.Forms. Se ha agregado un objeto TouchEffect
a la colección Effects
de ese 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 objeto TouchEffect
se podría agregar directamente a la colección Effects
de SKCanvasView
, pero eso no funciona en todas las plataformas. Dado que SKCanvasView
es del mismo tamaño que Grid
en esta configuración, adjuntarlo a Grid
funciona igual de bien.
El archivo de código subyacente se carga en un recurso de mapa de bits en su constructor y lo muestra en el controlador 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());
}
}
Sin ningún código adicional, el valor SKMatrix
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 OnTouchEffectAction
establecido en el archivo XAML es modificar el valor de matriz para reflejar las manipulaciones táctiles.
El controlador OnTouchEffectAction
comienza convirtiendo el valor Xamarin.FormsPoint
en un valor SKPoint
SkiaSharp. Se trata de una cuestión sencilla de escalado en función de las propiedades Width
y Height
de SKCanvasView
(que son unidades independientes del dispositivo) y la propiedad CanvasSize
, 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 prueba de posicionamiento. En este caso, las pruebas de posicionamiento se pueden realizar creando un valor SKRect
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 es así, el campo touchId
se establece en el identificador táctil y se guarda la posición del dedo.
Para el evento TouchActionType.Moved
, los factores de traducción del valor SKMatrix
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, tome nota de 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 desee que el mapa de bits se gire (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 en consecuencia.
El control de dos dedos a la vez parece complicado, pero tenga en cuenta que el controlador TouchAction
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 siguiente código de página Escalado de mapa de bits, el dedo que no ha cambiado de posición se denomina punto de pivote 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. Un diccionario se usa para este propósito, donde el identificador 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 acción Pressed
es casi el mismo que el programa anterior, excepto que el identificador y el punto táctil se agregan al diccionario. Las acciones Released
y Cancelled
quitan la entrada del diccionario.
Sin embargo, el control de la acción Moved
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 de diccionario en una matriz y, a continuación, compara la primera clave con el identificador del dedo que se mueve. 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 es un número). Si todo está bien, una transformación de escalado se concatena con el valor SKMatrix
guardado como un 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 contra esto.
Rotación de dos dedos
La página Rotación de mapa de bits le permite usar dos dedos para el escalado de giro o 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 método Contains
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 necesite 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 no transformado. 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 evento Moved
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 dinámico del dedo inmóvil. 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 se usa el mismo valor scale
para el escalado horizontal y vertical para que el escalado sea isotrópico. El campo matrix
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
La página Manipulación táctil muestra la manipulación táctil de un solo mapa de bits, pero el uso de otros archivos que encapsulan gran parte de la lógica mostrada anteriormente. El primero de estos archivos es la enumeración TouchManipulationMode
, que indica los distintos tipos de manipulación táctil implementados 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 reducción que da como resultado el escalado de objetos igualmente en las direcciones horizontales y verticales. AnisotropicScale
permite un escalado desigual.
La opción ScaleRotate
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 escala anisotrópico es problemática porque los movimientos de los dedos son esencialmente los mismos.
La opción ScaleDualRotate
agrega rotación de un dedo. Cuando un solo dedo arrastra el objeto, el objeto arrastrado se gira por primera vez alrededor de su centro para que el centro del objeto se alinea con el vector de arrastre.
El archivo TouchManipulationPage.xaml incluye un Picker
con los miembros de la enumeración 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>
Hacia la parte inferior es una SKCanvasView
y una TouchEffect
adjunta a la celda única Grid
que la incluye.
El archivo de código subyacente TouchManipulationPage.xaml.cs tiene un campo bitmap
, 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 objeto TouchManipulationBitmap
y pasa al constructor un SKBitmap
obtenido de un recurso incrustado. El constructor concluye estableciendo la propiedad Mode
de la propiedad TouchManager
del objeto TouchManipulationBitmap
en un miembro de la enumeración TouchManipulationMode
.
El controlador SelectedIndexChanged
de Picker
también establece esta propiedad 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;
}
}
...
}
El controlador TouchAction
de la instancia de TouchEffect
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 método HitTest
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 colección TouchIds
. Este identificador representa la secuencia de eventos táctiles de ese dedo hasta que el dedo se eleva desde la pantalla. Si varios dedos tocan el mapa de bits, la colección touchIds
contiene un identificador táctil para cada dedo.
El controlador TouchAction
también llama a la clase ProcessTouchEvent
en TouchManipulationBitmap
. Aquí es donde se produce una parte (no toda) del procesamiento táctil real.
La clase TouchManipulationBitmap
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 clase TouchManipulationManager
(que verá en breve).
El constructor TouchManipulationBitmap
guarda SKBitmap
y crea una instancia de dos propiedades, la propiedad TouchManager
de tipo TouchManipulationManager
y la propiedad Matrix
de tipoSKMatrix
:
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 propiedad Matrix
es la transformación acumulada resultante de toda la actividad táctil. Como verá, cada evento táctil se resuelve en una matriz, que se concatena con el valor SKMatrix
almacenado por la propiedad Matrix
.
El objeto TouchManipulationBitmap
se dibuja en su método Paint
. El argumento es un objeto SKCanvas
. Este SKCanvas
podría tener ya aplicada una transformación, por lo que el método Paint
concatena la propiedad Matrix
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 método HitTest
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 determinado. El método mantiene un diccionario de objetos TouchManipulationInfo
, 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 propio método 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;
}
}
...
}
En los eventos Moved
y Released
, el método llama a Manipulate
. En estos momentos, touchDictionary
contiene uno o varios objetos TouchManipulationInfo
. Si touchDictionary
contiene un elemento, es probable que los valores PreviousPoint
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 PreviousPoint
y NewPoint
diferentes. Todos los demás tienen valores PreviousPoint
y NewPoint
iguales.
Esto es importante: el método Manipulate
puede suponer que está procesando el movimiento de un solo dedo. En el momento de esta llamada ninguno de los otros dedos se mueve y si realmente se mueven (como es probable), esos movimientos se procesarán en futuras llamadas a Manipulate
.
El método Manipulate
copia primero el diccionario en una matriz para mayor comodidad. Omite todo lo que no sea 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 método OneFingerManipulate
del objeto TouchManipulationManager
. Para dos dedos, llama a TwoFingerManipulate
. Los argumentos de estos métodos son los mismos: los argumentos prevPoint
y newPoint
representan el dedo que se mueve. Pero el argumento pivotPoint
es diferente para las dos llamadas:
Para la manipulación con un dedo, pivotPoint
es el centro del mapa de bits. Esto es para permitir la rotación de un dedo. Para la manipulación con 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 valor SKMatrix
, que el método concatena con la propiedad Matrix
actual 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 quiera evitar la opción AnisotropicScale
. 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. Eso hace que el mapa de bits desaparezca de la vista, para nunca volver. Si realmente necesita escalado anisotrópico, querrá mejorar la lógica para evitar resultados no deseados.
TouchManipulationManager
hace uso de vectores, pero como no hay ninguna estructura SKVector
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 cálculo Magnitude
:
class TouchManipulationManager
{
...
float Magnitude(SKPoint point)
{
return (float)Math.Sqrt(Math.Pow(point.X, 2) + Math.Pow(point.Y, 2));
}
}
Cada vez que se haya seleccionado la rotación, los métodos de manipulación con un dedo y dos dedos controlan primero la rotación. Si se detecta algún giro, el componente de rotación se quita eficazmente. Lo que permanece se interpreta como movimiento panorámico y escalado.
Este es el método OneFingerManipulate
. 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 método TwoFingerManipulate
, el punto dinámico es la posición del dedo que no se mueve en este evento táctil concreto. 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 métodos MakeRotation
como MakeScale
se basan en el punto dinámico, y eso 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. Cuando cada dedo se mueve en relación con el otro, se produce una escala o rotación, pero el movimiento del otro dedo la anula y el resultado es una traslación.
La única parte restante de la página Manipulación táctil es el controlador PaintSurface
en el archivo de código subyacente TouchManipulationPage
. Esto llama al método Paint
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 controlador PaintSurface
concluye mostrando un objeto MatrixDisplay
que muestra la matriz táctil acumulada:
Manipulación de 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. En lugar de definir un campo de tipo TouchManipulationBitmap
, la clase BitmapScatterPage
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 se carga en todos los mapas de bits disponibles como recursos incrustados y los agrega a bitmapCollection
. Observe que la propiedad Matrix
se inicializa en cada objeto TouchManipulationBitmap
, por lo que las esquinas superior izquierda de cada mapa de bits se desplazan 100 píxeles.
La página BitmapScatterView
también debe controlar eventos táctiles para varios mapas de bits. En lugar de definir un List
de identificadores táctiles de TouchManipulationBitmap
manipulados 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 lógica Pressed
recorre la bitmapCollection
en sentido inverso. 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 presionan en la pantalla, el más alto debe ser el que manipula ese dedo.
Observe también que la lógica Pressed
mueve ese 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 eventos Moved
y Released
, el controlador TouchAction
llama al método ProcessingTouchEvent
en TouchManipulationBitmap
igual que el programa anterior.
Por último, el controlador PaintSurface
llama al método Paint
de cada objeto 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);
}
}
}
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 un solo dedo
Por lo general, una operación de escalado requiere un gesto de reducción mediante 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 clase TouchManipulationManager
, no usa esa clase ni la clase TouchManipulationBitmap
. 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 podrían estar tocando la pantalla.
La página SingleFingerCornerScale.xaml crea una instancia de la clase SKCanvasView
y crea un objeto TouchEffect
para realizar el 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 objeto SKMatrix
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 objeto SKMatrix
se modifica mediante la lógica táctil que se muestra a continuación.
El resto del archivo de código subyacente es el controlador TouchEffect
de eventos. Comienza convirtiendo la ubicación actual del dedo en un valor SKPoint
. Para el tipo de acción Pressed
, 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 instrucción if
que implica dos llamadas al método Math.Pow
. 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 dinámico 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 tipo de acción Moved
calcula una matriz correspondiente a la actividad táctil desde el momento en que el dedo presiona la pantalla hasta esta vez. Concatena esa matriz con la matriz en vigor en el momento en que el dedo presiona 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 grandes, una elipse interior podría 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 algo diferente, en cuyo caso puede reemplazar ese bloque if
completo 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 áreas mucho más grandes en las esquinas para agarrar y escalar el mapa de bits.