Вызов событий из эффекта
Эффект может определять вызов события, сигнализирующего об изменениях в базовом собственном представлении. В этой статье описываются реализация низкоуровневого отслеживания мультисенсорного ввода, а также создание событий, сигнализирующих о прикосновениях.
Описываемый в этой статье эффект предоставляет доступ к низкоуровневым событиям прикосновения. Эти низкоуровневые события недоступны через существующие классы GestureRecognizer
, однако важны для некоторых типов приложений. Например, приложение для рисования пальцами должно отслеживать движение отдельных пальцев по экрану. Клавиатура музыкального приложения должна обнаруживать, когда пользователь касается отдельных клавиш и отпускает их, а также скольжение пальца между клавишами при исполнении глиссандо.
Этот эффект идеально подходит для отслеживания мультисенсорного ввода, поскольку он может присоединяться к любому элементу Xamarin.Forms.
События прикосновения платформы
В iOS, Android и на универсальной платформе Windows предусмотрен низкоуровневый API, с помощью которого приложения могут обнаруживать операции сенсорного ввода. Для всех этих платформ существует три базовых типа событий прикосновения:
- Pressed — палец касается экрана;
- Moved — палец, касающийся экрана, перемещается;
- Released — палец перестает касаться экрана.
В среде мультисенсорного ввода несколько пальцев могут одновременно касаться экрана. На различных платформах используются идентификаторы (ID), с помощью которых приложения различают пальцы.
Класс UIView
в iOS определяет три переопределяемых метода (TouchesBegan
, TouchesMoved
и TouchesEnded
), которые соответствуют этим трем базовым событиям. Использование этих методов описывается в статье Отслеживание мультисенсорного ввода. Тем не менее для работы с этими методами в программе iOS не требуется переопределять класс, производный от UIView
. Объект UIGestureRecognizer
в iOS также определяет эти три метода. Экземпляр класса, унаследованного от UIGestureRecognizer
, можно присоединить к любому объекту UIView
.
Класс View
в Android определяет переопределяемый метод OnTouchEvent
, который обрабатывает все операции сенсорного ввода. Тип действия прикосновения определяется с помощью членов перечисления Down
, PointerDown
, Move
, Up
и PointerUp
, как описывается в статье Отслеживание мультисенсорного ввода. Объект View
в Android также определяет событие Touch
, что позволяет присоединять обработчик событий к любому объекту View
.
На универсальной платформе Windows (UWP) класс UIElement
определяет события PointerPressed
, PointerMoved
и PointerReleased
. Они описываются в статье Обработка ввода указателя на сайте MSDN, а также в документации по API для класса UIElement
.
API Pointer
на универсальной платформе Windows обеспечивает унифицированную обработку ввода с помощью мыши, прикосновений и пера. По этой причине событие PointerMoved
вызывается при наведении указателя мыши на элемент, даже если кнопка мыши при этом не нажата. Объект PointerRoutedEventArgs
, сопровождающий эти события, содержит свойство Pointer
со свойством IsInContact
, которое указывает, была ли нажата кнопка мыши или было ли прикосновения пальца к экрану.
Кроме того, на универсальной платформе Windows определяются события PointerEntered
и PointerExited
. Они указывают на перемещение указателя мыши или пальца с одного элемента на другой. Допустим, у вас есть два соседних элемента A и B, для каждого из которых установлены обработчики событий указателя. Когда палец касается элемента A, вызывается событие PointerPressed
. При перемещении пальца элемент A вызывает событие PointerMoved
. Если палец перемещается с элемента A на элемент B, элемент A вызывает событие PointerExited
, а элемент B вызывает событие PointerEntered
. Если после этого пользователь отпустит палец, элемент B вызовет событие PointerReleased
.
На платформах iOS и Android такое поведение реализовано иначе, чем на UWP. Представление, которое первым получает вызов TouchesBegan
или OnTouchEvent
при касании пальца, продолжает принимать все действия прикосновения, даже если палец перемещается в другие представления. На платформе UWP может быть реализовано аналогичное поведение, если приложение захватывает указатель. В этом случае в обработчике событий PointerEntered
элемент вызывает CapturePointer
и затем принимает все действия прикосновения от этого пальца.
Применяемый на платформе UWP подход эффективен для некоторых типов приложений, например в клавиатурах музыкальных приложений. Каждая клавиша может обрабатывать собственные события прикосновения, а также обнаруживать перемещение пальца между клавишами с помощью событий PointerEntered
и PointerExited
.
Поэтому в эффекте отслеживания сенсорного ввода, который описывается в этой статье, реализуется подход, принятый на универсальной платформе Windows (UWP).
API эффекта отслеживания сенсорного ввода
Пример содержит классы (и перечисление), реализующие низкоуровневое отслеживание сенсорного ввода. Эти типы принадлежат пространству имен TouchTracking
и начинаются со слова Touch
. Проект библиотеки .NET Standard TouchTrackingEffectDemos включает перечисление TouchActionType
для типа событий прикосновения:
public enum TouchActionType
{
Entered,
Pressed,
Moved,
Released,
Exited,
Cancelled
}
На всех платформах также реализовано событие, указывающее на отмену события прикосновения.
Класс TouchEffect
в библиотеке .NET Standard является производным от RoutingEffect
и определяет событие TouchAction
, а также метод OnTouchAction
, который вызывает событие TouchAction
:
public class TouchEffect : RoutingEffect
{
public event TouchActionEventHandler TouchAction;
public TouchEffect() : base("XamarinDocs.TouchEffect")
{
}
public bool Capture { set; get; }
public void OnTouchAction(Element element, TouchActionEventArgs args)
{
TouchAction?.Invoke(element, args);
}
}
Также обратите внимание на свойство Capture
. Для захвата событий прикосновения приложение должно присваивать этому свойству значение true
до вызова события Pressed
. В противном случае поведение событий прикосновения будет таким же, как на универсальной платформе Windows.
Класс TouchActionEventArgs
в библиотеке .NET Standard содержит всю информацию, сопровождающую каждое событие:
public class TouchActionEventArgs : EventArgs
{
public TouchActionEventArgs(long id, TouchActionType type, Point location, bool isInContact)
{
Id = id;
Type = type;
Location = location;
IsInContact = isInContact;
}
public long Id { private set; get; }
public TouchActionType Type { private set; get; }
public Point Location { private set; get; }
public bool IsInContact { private set; get; }
}
Для отслеживания отдельных пальцев в приложении может использоваться свойство Id
. Обратите внимание на свойство IsInContact
. Оно всегда имеет значение true
для событий Pressed
и false
для событий Released
. Кроме того, оно всегда имеет значение true
для событий Moved
на платформах iOS и Android. На универсальной платформе Windows свойство IsInContact
может иметь значение false
для событий Moved
в тех случаях, когда программа выполняется на настольном ПК и указатель мыши перемещается без нажатия кнопки.
Вы можете использовать класс TouchEffect
в собственных приложениях. Для этого необходимо включить этот файл в проект библиотеки .NET Standard для решения и добавить экземпляр в коллекцию Effects
любого элемента Xamarin.Forms. Чтобы получать события ввода, присоедините обработчик к событию TouchAction
.
Если вы хотите использовать класс TouchEffect
в собственном приложении, вам также потребуются реализации для платформ, включенные в решение TouchTrackingEffectDemos.
Реализации эффекта отслеживания сенсорного ввода
Реализации класса TouchEffect
для платформ iOS, Android и UWP описываются далее, начиная с самой простой (UWP) и заканчивая структурно самой сложной реализацией для iOS.
Реализация для универсальной платформы Windows
Реализация класса TouchEffect
для универсальной платформы Windows является самой простой. Как правило, этот класс является производным от класса PlatformEffect
и включает два атрибута сборки:
[assembly: ResolutionGroupName("XamarinDocs")]
[assembly: ExportEffect(typeof(TouchTracking.UWP.TouchEffect), "TouchEffect")]
namespace TouchTracking.UWP
{
public class TouchEffect : PlatformEffect
{
...
}
}
Переопределение OnAttached
сохраняет некоторые данные в виде полей и присоединяет обработчики ко всем событиям указателя:
public class TouchEffect : PlatformEffect
{
FrameworkElement frameworkElement;
TouchTracking.TouchEffect effect;
Action<Element, TouchActionEventArgs> onTouchAction;
protected override void OnAttached()
{
// Get the Windows FrameworkElement corresponding to the Element that the effect is attached to
frameworkElement = Control == null ? Container : Control;
// Get access to the TouchEffect class in the .NET Standard library
effect = (TouchTracking.TouchEffect)Element.Effects.
FirstOrDefault(e => e is TouchTracking.TouchEffect);
if (effect != null && frameworkElement != null)
{
// Save the method to call on touch events
onTouchAction = effect.OnTouchAction;
// Set event handlers on FrameworkElement
frameworkElement.PointerEntered += OnPointerEntered;
frameworkElement.PointerPressed += OnPointerPressed;
frameworkElement.PointerMoved += OnPointerMoved;
frameworkElement.PointerReleased += OnPointerReleased;
frameworkElement.PointerExited += OnPointerExited;
frameworkElement.PointerCanceled += OnPointerCancelled;
}
}
...
}
Обработчик OnPointerPressed
вызывает событие эффекта посредством вызова поля onTouchAction
в методе CommonHandler
:
public class TouchEffect : PlatformEffect
{
...
void OnPointerPressed(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Pressed, args);
// Check setting of Capture property
if (effect.Capture)
{
(sender as FrameworkElement).CapturePointer(args.Pointer);
}
}
...
void CommonHandler(object sender, TouchActionType touchActionType, PointerRoutedEventArgs args)
{
PointerPoint pointerPoint = args.GetCurrentPoint(sender as UIElement);
Windows.Foundation.Point windowsPoint = pointerPoint.Position;
onTouchAction(Element, new TouchActionEventArgs(args.Pointer.PointerId,
touchActionType,
new Point(windowsPoint.X, windowsPoint.Y),
args.Pointer.IsInContact));
}
}
Обработчик OnPointerPressed
также проверяет значение свойства Capture
в классе эффекта в библиотеке .NET Standard и вызывает метод CapturePointer
, если это свойство имеет значение true
.
Другой обработчик событий UWP реализован еще проще:
public class TouchEffect : PlatformEffect
{
...
void OnPointerEntered(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Entered, args);
}
...
}
Реализация для Android
Реализации этого класса для Android и iOS вынужденно усложняются, поскольку в этих случаях необходимо реализовать события Exited
и Entered
, соответствующие перемещению пальца с одного элемента на другой. Обе реализации имеют схожую структуру.
Класс TouchEffect
в Android устанавливает обработчик для события Touch
:
view = Control == null ? Container : Control;
...
view.Touch += OnTouch;
Этот класс также определяет два статических словаря:
public class TouchEffect : PlatformEffect
{
...
static Dictionary<Android.Views.View, TouchEffect> viewDictionary =
new Dictionary<Android.Views.View, TouchEffect>();
static Dictionary<int, TouchEffect> idToEffectDictionary =
new Dictionary<int, TouchEffect>();
...
Объект viewDictionary
получает новую запись каждый раз при вызове переопределения OnAttached
:
viewDictionary.Add(view, this);
Запись удаляется из словаря в OnDetached
. Каждый экземпляр TouchEffect
связан с конкретным представлением, к которому подключен эффект. Используя статический словарь, любой экземпляр TouchEffect
может перечислять все остальные представления и соответствующие им экземпляры TouchEffect
. Это необходимо для передачи событий от одного представления другому.
Android назначает событиям прикосновения идентификаторы, благодаря которым приложение может отслеживать отдельные пальцы. idToEffectDictionary
связывает этот идентификатор с экземпляром TouchEffect
. Элемент добавляется в этот словарь при вызове обработчика Touch
для нажатия пальцем:
void OnTouch(object sender, Android.Views.View.TouchEventArgs args)
{
...
switch (args.Event.ActionMasked)
{
case MotionEventActions.Down:
case MotionEventActions.PointerDown:
FireEvent(this, id, TouchActionType.Pressed, screenPointerCoords, true);
idToEffectDictionary.Add(id, this);
capture = libTouchEffect.Capture;
break;
Элемент удаляется из словаря idToEffectDictionary
, когда палец перестает касаться экрана. Метод FireEvent
просто накапливает всю информацию, необходимую для вызова метода OnTouchAction
:
void FireEvent(TouchEffect touchEffect, int id, TouchActionType actionType, Point pointerLocation, bool isInContact)
{
// Get the method to call for firing events
Action<Element, TouchActionEventArgs> onTouchAction = touchEffect.libTouchEffect.OnTouchAction;
// Get the location of the pointer within the view
touchEffect.view.GetLocationOnScreen(twoIntArray);
double x = pointerLocation.X - twoIntArray[0];
double y = pointerLocation.Y - twoIntArray[1];
Point point = new Point(fromPixels(x), fromPixels(y));
// Call the method
onTouchAction(touchEffect.formsElement,
new TouchActionEventArgs(id, actionType, point, isInContact));
}
Все остальные типы прикосновений обрабатываются двумя разными способами: если свойство Capture
имеет значение true
, событие прикосновения просто получает информацию TouchEffect
. Если свойство Capture
имеет значение false
, процесс усложняется, поскольку события прикосновения могут перемещаться из одного представления в другое. За обработку таких событий отвечает метод CheckForBoundaryHop
, который вызывается во время событий перемещения. Этот метод использует оба статических словаря. Он выполняет перечисление словаря viewDictionary
, чтобы определить представление, которого палец касается в текущий момент. Затем он сохраняет в словарь idToEffectDictionary
текущий экземпляр TouchEffect
(и соответственно текущее представление), связанный с конкретным идентификатором:
void CheckForBoundaryHop(int id, Point pointerLocation)
{
TouchEffect touchEffectHit = null;
foreach (Android.Views.View view in viewDictionary.Keys)
{
// Get the view rectangle
try
{
view.GetLocationOnScreen(twoIntArray);
}
catch // System.ObjectDisposedException: Cannot access a disposed object.
{
continue;
}
Rectangle viewRect = new Rectangle(twoIntArray[0], twoIntArray[1], view.Width, view.Height);
if (viewRect.Contains(pointerLocation))
{
touchEffectHit = viewDictionary[view];
}
}
if (touchEffectHit != idToEffectDictionary[id])
{
if (idToEffectDictionary[id] != null)
{
FireEvent(idToEffectDictionary[id], id, TouchActionType.Exited, pointerLocation, true);
}
if (touchEffectHit != null)
{
FireEvent(touchEffectHit, id, TouchActionType.Entered, pointerLocation, true);
}
idToEffectDictionary[id] = touchEffectHit;
}
}
Если словарь idToEffectDictionary
был изменен, этот метод может вызвать FireEvent
для событий Exited
и Entered
, чтобы выполнить передачу от одного представления другому. Тем не менее палец может перемещаться в область представления без присоединенного объекта TouchEffect
, а также из такой области в представление с присоединенным эффектом.
Обратите внимание на блок try
и catch
при доступе к представлению. Если был выполнен переход на страницу с последующим возвратом на домашнюю страницу, метод OnDetached
не вызывается и элементы остаются в словаре viewDictionary
, хотя Android рассматривает их как ликвидированные.
Реализация для iOS
Реализация для iOS схожа с реализацией для Android, однако в ней класс TouchEffect
должен создавать экземпляр, производный от UIGestureRecognizer
. Это класс из проекта iOS TouchRecognizer
. В нем содержатся два статических словаря, в которых хранятся экземпляры TouchRecognizer
:
static Dictionary<UIView, TouchRecognizer> viewDictionary =
new Dictionary<UIView, TouchRecognizer>();
static Dictionary<long, TouchRecognizer> idToTouchDictionary =
new Dictionary<long, TouchRecognizer>();
Структура класса TouchRecognizer
во многом схожа с классом TouchEffect
в Android.
Внимание
Во многих представлениях в UIKit
поддержка сенсорного ввода не включена по умолчанию. Ее можно включить, добавив view.UserInteractionEnabled = true;
в переопределение OnAttached
в классе TouchEffect
в проекте iOS. Это должно произойти после получения UIView
(соответствует элементу, к которому присоединен эффект).
Применение эффекта сенсорного ввода на практике
Пример программы содержит пять страниц, которые проверяют эффект сенсорного отслеживания для распространенных задач.
На странице перетаскивания элементов BoxView вы можете добавить элементы BoxView
в объект AbsoluteLayout
и затем перетаскивать их по экрану. В файле XAML создаются экземпляры двух представлений Button
для добавления элементов BoxView
в объект AbsoluteLayout
и очистки объекта AbsoluteLayout
.
Метод в файле кода программной части, который добавляет новый элемент BoxView
в объект AbsoluteLayout
, также добавляет объект TouchEffect
в элемент BoxView
и присоединяет к эффекту обработчик событий:
void AddBoxViewToLayout()
{
BoxView boxView = new BoxView
{
WidthRequest = 100,
HeightRequest = 100,
Color = new Color(random.NextDouble(),
random.NextDouble(),
random.NextDouble())
};
TouchEffect touchEffect = new TouchEffect();
touchEffect.TouchAction += OnTouchEffectAction;
boxView.Effects.Add(touchEffect);
absoluteLayout.Children.Add(boxView);
}
Обработчик события TouchAction
обрабатывает все события прикосновения для всех элементов BoxView
. Тем не менее необходимо обратить внимание, что он не поддерживает обработку прикосновения двух пальцев к одному элементу BoxView
, поскольку эта программа поддерживает только операцию перетаскивания, при которой пальцы будут мешать друг другу. По этой причине на странице определяется встроенный класс для каждого отслеживаемого на текущий момент пальца:
class DragInfo
{
public DragInfo(long id, Point pressPoint)
{
Id = id;
PressPoint = pressPoint;
}
public long Id { private set; get; }
public Point PressPoint { private set; get; }
}
Dictionary<BoxView, DragInfo> dragDictionary = new Dictionary<BoxView, DragInfo>();
Словарь dragDictionary
содержит записи для каждого перетаскиваемого элемента BoxView
.
При обнаружении действия прикосновения Pressed
элемент добавляется в этот словарь, а в ответ на действие Released
удаляется из него. В логике действия Pressed
должна выполняться проверка на наличие в словаре элемента, соответствующего такому элементу BoxView
. Если такой элемент присутствует в словаре, значит, этот элемент BoxView
уже перетаскивается и новое событие относится к прикосновению второго пальца к этому же элементу BoxView
. Для действий Moved
и Released
обработчик событий должен проверять наличие в словаре записи для такого элемента BoxView
, а также соответствие свойства Id
прикосновения к перетаскиваемому элементу BoxView
, аналогичному свойству записи в словаре:
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
BoxView boxView = sender as BoxView;
switch (args.Type)
{
case TouchActionType.Pressed:
// Don't allow a second touch on an already touched BoxView
if (!dragDictionary.ContainsKey(boxView))
{
dragDictionary.Add(boxView, new DragInfo(args.Id, args.Location));
// Set Capture property to true
TouchEffect touchEffect = (TouchEffect)boxView.Effects.FirstOrDefault(e => e is TouchEffect);
touchEffect.Capture = true;
}
break;
case TouchActionType.Moved:
if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
{
Rectangle rect = AbsoluteLayout.GetLayoutBounds(boxView);
Point initialLocation = dragDictionary[boxView].PressPoint;
rect.X += args.Location.X - initialLocation.X;
rect.Y += args.Location.Y - initialLocation.Y;
AbsoluteLayout.SetLayoutBounds(boxView, rect);
}
break;
case TouchActionType.Released:
if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
{
dragDictionary.Remove(boxView);
}
break;
}
}
В логике действия Pressed
свойству Capture
объекта TouchEffect
присваивается значение true
. Это влияет на доставку всех последующих событий для этого пальца в тот же обработчик событий.
В логике действия Moved
элемент BoxView
перемещается посредством изменения присоединенного свойства LayoutBounds
. Свойство Location
аргументов события всегда задается относительно перетаскиваемого элемента BoxView
. Если такой элемент BoxView
перетаскивается с постоянной скоростью, свойства Location
последующих событий будут иметь примерно одинаковые значения. Например, если коснуться пальцем в центре элемента BoxView
, действие Pressed
сохранит значение свойства PressPoint
(50, 50), которое будет оставаться постоянным для последующих событий. Если элемент BoxView
перетаскивается по диагонали с постоянной скоростью, последующие свойства Location
во время выполнения действия Moved
могут иметь значения (55, 55). Это означает, что в логике действия Moved
добавляется 5 единиц к горизонтальной и вертикальной позиции элемента BoxView
. При этом элемент BoxView
перемещается таким образом, чтобы его центр снова оказался непосредственно в точке прикосновения пальца.
Вы можете одновременно перетаскивать несколько элементов BoxView
разными пальцами.
Создание подкласса представления
Для элемента Xamarin.Forms зачастую проще обрабатывать собственные события прикосновения. Страница перетаскивания перетаскиваемых элементов BoxView функционирует аналогично странице перетаскивания элементов BoxView, однако на ней перетаскиваемые пользователем элементы являются экземплярами класса DraggableBoxView
, производного от BoxView
:
class DraggableBoxView : BoxView
{
bool isBeingDragged;
long touchId;
Point pressPoint;
public DraggableBoxView()
{
TouchEffect touchEffect = new TouchEffect
{
Capture = true
};
touchEffect.TouchAction += OnTouchEffectAction;
Effects.Add(touchEffect);
}
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
switch (args.Type)
{
case TouchActionType.Pressed:
if (!isBeingDragged)
{
isBeingDragged = true;
touchId = args.Id;
pressPoint = args.Location;
}
break;
case TouchActionType.Moved:
if (isBeingDragged && touchId == args.Id)
{
TranslationX += args.Location.X - pressPoint.X;
TranslationY += args.Location.Y - pressPoint.Y;
}
break;
case TouchActionType.Released:
if (isBeingDragged && touchId == args.Id)
{
isBeingDragged = false;
}
break;
}
}
}
Конструктор создает и присоединяет объект TouchEffect
, после чего задает свойство Capture
при создании первого экземпляра этого объекта. В этом случае словарь не требуется, поскольку в классе уже хранятся значения isBeingDragged
, pressPoint
и touchId
, связанные с каждым пальцем. При обработке действия Moved
изменяются свойства TranslationX
и TranslationY
, благодаря чему логика будет работать даже в том случае, если родительским для класса DraggableBoxView
не является класс AbsoluteLayout
.
Интеграция с SkiaSharp
В следующих двух демонстрациях используется графика, для работы с которой применяется SkiaSharp. Прежде чем изучать эти примеры, вы можете ознакомиться с использованием SkiaSharp в Xamarin.Forms. Всю необходимую вам для этого информацию вы сможете найти в первых двух статьях, которые посвящены основам рисования, а также линиям и контурам в SkiaSharp.
На странице рисования эллипса вы можете нарисовать эллипс, проводя пальцами по экрану. В зависимости от характера движения пальцев, вы можете нарисовать эллипс из верхнего левого угла в нижний правый либо из любого другого угла в противоположный. Цвет и степень непрозрачности эллипса задаются случайным образом.
При необходимости вы можете коснуться любого нарисованного эллипса и перетащить его в новое место. Для этого применяется метод проверки на попадание, в рамках которого осуществляется поиск графического объекта в конкретной точке. Эллипсы SkiaSharp не являются элементами Xamarin.Forms и не имеют собственной логики обработки объекта TouchEffect
. Эффект TouchEffect
должен применяться ко всему объекту SKCanvasView
.
Файл EllipseDrawPage.xaml создает экземпляр SKCanvasView
в объекте Grid
из одной ячейки. Объект TouchEffect
присоединяется к этому объекту Grid
:
<Grid x:Name="canvasViewGrid"
Grid.Row="1"
BackgroundColor="White">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
В Android и на универсальной платформе Windows объект TouchEffect
можно присоединять напрямую к SKCanvasView
, однако в iOS такой подход не работает. Обратите внимание, что свойству Capture
присвоено значение true
.
Каждый эллипс, отрисовываемый SkiaSharp, представлен объектом типа EllipseDrawingFigure
:
class EllipseDrawingFigure
{
SKPoint pt1, pt2;
public EllipseDrawingFigure()
{
}
public SKColor Color { set; get; }
public SKPoint StartPoint
{
set
{
pt1 = value;
MakeRectangle();
}
}
public SKPoint EndPoint
{
set
{
pt2 = value;
MakeRectangle();
}
}
void MakeRectangle()
{
Rectangle = new SKRect(pt1.X, pt1.Y, pt2.X, pt2.Y).Standardized;
}
public SKRect Rectangle { set; get; }
// For dragging operations
public Point LastFingerLocation { set; get; }
// For the dragging hit-test
public bool IsInEllipse(SKPoint pt)
{
SKRect rect = Rectangle;
return (Math.Pow(pt.X - rect.MidX, 2) / Math.Pow(rect.Width / 2, 2) +
Math.Pow(pt.Y - rect.MidY, 2) / Math.Pow(rect.Height / 2, 2)) < 1;
}
}
Свойства StartPoint
и EndPoint
используются, когда программа обрабатывает сенсорный ввод. Свойство Rectangle
используется при рисовании эллипса. Свойство LastFingerLocation
задействуется при перетаскивании эллипса, а метод IsInEllipse
используется при проверке на попадание. Этот метод возвращает значение true
, если точка находится внутри эллипса.
В файле кода программной части содержатся три коллекции:
Dictionary<long, EllipseDrawingFigure> inProgressFigures = new Dictionary<long, EllipseDrawingFigure>();
List<EllipseDrawingFigure> completedFigures = new List<EllipseDrawingFigure>();
Dictionary<long, EllipseDrawingFigure> draggingFigures = new Dictionary<long, EllipseDrawingFigure>();
Словарь draggingFigure
содержит подмножество коллекции completedFigures
. Обработчик события PaintSurface
SkiaSharp просто отрисовывает объекты в этих коллекциях completedFigures
и inProgressFigures
:
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Fill
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKCanvas canvas = args.Surface.Canvas;
canvas.Clear();
foreach (EllipseDrawingFigure figure in completedFigures)
{
paint.Color = figure.Color;
canvas.DrawOval(figure.Rectangle, paint);
}
foreach (EllipseDrawingFigure figure in inProgressFigures.Values)
{
paint.Color = figure.Color;
canvas.DrawOval(figure.Rectangle, paint);
}
}
Самая сложная часть начинается при обработке действия Pressed
. На этом этапе выполняется проверка на попадание, однако если код обнаруживает эллипс в точке касания пальца, его перетаскивание будет возможно только в том случае, если он на данный момент не перетаскивается с помощью другого пальца. Если в точке касания пальца не обнаруживается эллипс, код начинает обработку процесса рисования нового эллипса:
case TouchActionType.Pressed:
bool isDragOperation = false;
// Loop through the completed figures
foreach (EllipseDrawingFigure fig in completedFigures.Reverse<EllipseDrawingFigure>())
{
// Check if the finger is touching one of the ellipses
if (fig.IsInEllipse(ConvertToPixel(args.Location)))
{
// Tentatively assume this is a dragging operation
isDragOperation = true;
// Loop through all the figures currently being dragged
foreach (EllipseDrawingFigure draggedFigure in draggingFigures.Values)
{
// If there's a match, we'll need to dig deeper
if (fig == draggedFigure)
{
isDragOperation = false;
break;
}
}
if (isDragOperation)
{
fig.LastFingerLocation = args.Location;
draggingFigures.Add(args.Id, fig);
break;
}
}
}
if (isDragOperation)
{
// Move the dragged ellipse to the end of completedFigures so it's drawn on top
EllipseDrawingFigure fig = draggingFigures[args.Id];
completedFigures.Remove(fig);
completedFigures.Add(fig);
}
else // start making a new ellipse
{
// Random bytes for random color
byte[] buffer = new byte[4];
random.NextBytes(buffer);
EllipseDrawingFigure figure = new EllipseDrawingFigure
{
Color = new SKColor(buffer[0], buffer[1], buffer[2], buffer[3]),
StartPoint = ConvertToPixel(args.Location),
EndPoint = ConvertToPixel(args.Location)
};
inProgressFigures.Add(args.Id, figure);
}
canvasView.InvalidateSurface();
break;
Еще один пример использования SkiaSharp содержит страницу рисования пальцами. Вы можете выбрать цвет и ширину кисти в двух представлениях Picker
, а затем начать рисование одним или несколькими пальцами:
В этом примере также используется отдельный класс, который представляет каждую рисуемую на экране линию:
class FingerPaintPolyline
{
public FingerPaintPolyline()
{
Path = new SKPath();
}
public SKPath Path { set; get; }
public Color StrokeColor { set; get; }
public float StrokeWidth { set; get; }
}
Для отрисовывания каждой линии используется объект SKPath
. В файле FingerPaint.xaml.cs хранятся две коллекции таких объектов, в одной из которой содержатся рисуемые в данный момент, а в другой завершенные ломаные линии:
Dictionary<long, FingerPaintPolyline> inProgressPolylines = new Dictionary<long, FingerPaintPolyline>();
List<FingerPaintPolyline> completedPolylines = new List<FingerPaintPolyline>();
При обработке действия Pressed
создается новый объект FingerPaintPolyline
, для объекта контура вызывается метод MoveTo
для сохранения начальной точки, после чего этот объект добавляется в словарь inProgressPolylines
. При обработке действия Moved
для объекта контура вызывается метод LineTo
с данными о новой позиции пальца, а при обработке действия Released
завершенная ломаная линия переносится из словаря inProgressPolylines
в completedPolylines
. Сам код рисования SkiaSharp по-прежнему достаточно прост:
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeCap = SKStrokeCap.Round,
StrokeJoin = SKStrokeJoin.Round
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKCanvas canvas = args.Surface.Canvas;
canvas.Clear();
foreach (FingerPaintPolyline polyline in completedPolylines)
{
paint.Color = polyline.StrokeColor.ToSKColor();
paint.StrokeWidth = polyline.StrokeWidth;
canvas.DrawPath(polyline.Path, paint);
}
foreach (FingerPaintPolyline polyline in inProgressPolylines.Values)
{
paint.Color = polyline.StrokeColor.ToSKColor();
paint.StrokeWidth = polyline.StrokeWidth;
canvas.DrawPath(polyline.Path, paint);
}
}
Отслеживание сенсорного ввода между представлениями
Во всех предыдущих примерах свойству Capture
объекта TouchEffect
присваивалось значение true
либо при создании объекта TouchEffect
, либо при вызове события Pressed
. В таком случае один элемент будет принимать все события, связанные с пальцем, который первым коснулся представления. В заключительном примере этой статьи свойству Capture
не присваивается значение true
. В этом случае поведение при перемещении пальца, касающегося экрана, от одного элемента к другому реализуется иначе. Элемент, из которого перемещается палец, получает событие, для которого свойству Type
присваивается значение TouchActionType.Exited
. Второй элемент получает событие, свойству Type
которого присвоено значение TouchActionType.Entered
.
Такой подход к обработке прикосновений эффективен в приложении, имитирующем клавиатуру музыкального приложения. Каждая клавиша должна обнаруживать собственное событие нажатия, а также событие перемещения пальца с одной клавиши на другую.
На странице беззвучной клавиатуры определяются небольшие классы WhiteKey
и BlackKey
, производные от класса Key
, который, в свою очередь, является производным от BoxView
.
Класс Key
уже готов для использования в реальной музыкальной программе. В нем определены открытые свойства IsPressed
и KeyNumber
, которому будут присваиваться коды клавиш, устанавливаемые стандартом MIDI. Класс Key
также определяет событие StatusChanged
, которое вызывается при изменении свойства IsPressed
.
Каждая клавиша поддерживает прикосновения нескольких пальцев. Поэтому в классе Key
используется объект List
, содержащий идентификаторы прикосновений для всех пальцев, касающихся в данный момент соответствующей клавиши:
List<long> ids = new List<long>();
Обработчик события TouchAction
добавляет идентификатор в список ids
для типов событий Pressed
и Entered
, но делает это только в том случае, если свойству IsInContact
для события Entered
присвоено значение true
. Идентификатор удаляется из списка List
для событий Released
и Exited
:
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
switch (args.Type)
{
case TouchActionType.Pressed:
AddToList(args.Id);
break;
case TouchActionType.Entered:
if (args.IsInContact)
{
AddToList(args.Id);
}
break;
case TouchActionType.Moved:
break;
case TouchActionType.Released:
case TouchActionType.Exited:
RemoveFromList(args.Id);
break;
}
}
Методы AddToList
и RemoveFromList
проверяют, был ли объект List
изменен с пустого на заполненный, и в соответствующем случае вызывают событие StatusChanged
.
Различные элементы WhiteKey
и BlackKey
упорядочиваются в файле XAML страницы, который удобнее просматривать в альбомном режиме:
Если вы проведете пальцами по клавишам, вы увидите небольшое изменение цвета, свидетельствующее о передаче событий прикосновения от одной клавиши другой.
Итоги
В этой статье демонстрируется вызов событий в эффекте, а также описываются принципы написания и использования эффекта для низкоуровневой обработки мультисенсорного ввода.