在Xamarin.Forms控件中实现底层多点触控跟踪。
一个effect可以定义和调用一个事件,在底层本地视图中发出信号的变化。这篇文章演示如何实现底层多点触控跟踪,以及如何生成信号触摸活动的事件。
本文描述的Effect提供了对底层触摸事件的访问。这些低级事件在现有的GestureRecognizer类中是不可用的,但是它们对于某些类型的应用程序来说是非常重要的。例如,手指画画应用程序需要跟踪单个手指在屏幕上移动的情况。音乐键盘应用程序需要检测每个按键上的点击和释放,以及一个手指从一个键滑动到另一个键的滑音。
Effect是一个理想多点触控跟踪的,因为它可以附加到任何一个Xamarin.Forms元素上。
平台触摸事件
iOS、Android和通用的Windows平台都包含一个底层API,它允许应用程序检测触摸活动。这些平台都能区分三种基础触摸事件类型:
- Pressed 当一个手指触摸到屏幕时。
- Moved 当一个手指触摸到屏幕移动时。
- Released 当一个手指从屏幕上释放时。
在多点触控环境中,同一时间可以有多个手指触摸屏幕。各种平台包含一个识别(ID)号,应用程序可以用来区分多个手指。
在iOS中,UIView类定义了三个可覆盖的方法,TouchesBegan,TouchesMoved和TouchesEnded
来对应这三个事件。文章多点触控跟踪描写了如何使用这些方法。但是,iOS程序不需要覆盖从UIView派生的类来使用这些方法。iOSUIGestureRecognizer
也定义了这三个方法,并且你可以附加一个类的实例,它从UIGestureRecognizer派生到任何UIView对象。
在Android中,View类定义了一个可覆盖的OnTouchEvent
方法去处理所有的触摸活动。这里触摸活动类型定义为枚举类型Down、PointerDown、Move、Up和PointerUp,描述在文章多点触摸跟踪中。Android View也定义了名为Touch的事件,他允许一个事件handler附加到任何View对象上。
在通用Windows平台(UWP)中,UIElement类定义了名为PointerPressed,PointerMoved和PointerReleased的事件。在文章Handle Pointer Input article on MSDN和UIElement类的API文档中描写了这些事件。
通用Windows平台中的Pointer
(指针)API旨在统一鼠标、触摸和笔输入。因此,当鼠标移动到一个元素上时,即使鼠标按钮没有被抑制,PointerMoved
事件也会被调用。与这些事件关联的PointerRoutedEvent-Args
对象有一个名为Pointer
的属性,这个属性有一个名为IsInContact
的属性,该属性指示是按下鼠标按钮还是与屏幕进行接触。
此外,UWP定义两个名为PointerEntered
和PointerExited的鼠标事件。这些指示当鼠标或手指从一个元素移动到其他元素。例如,考虑两个相邻的元素A和B。这两个元素都为指针事件安装了处理程序。当一个手指按压A时,PointerPressed
事件被触发,当手指移动时,A调用PointerMoved事件。如果手指从A移动到B,A触发一个PointerExited事件,B触发一个PointerEntered
事件。如果指被释放,B调用一个pointerrelease事件。
iOS和Android平台不同于UWP:当手指触摸到视图时,第一个调用TouchesBegan或OnTouchEvent的视图继续得到所有的触摸活动,即使手指移动到不同的视图。如果应用程序捕捉到指针,UWP的行为类似:在pointerentry事件处理程序中,元素调用CapturePointer,然后从该手指获取所有的触摸活动。
UWP方法对某些类型的应用程序非常有用,例如,音乐键盘。每个键都可以处理该键的触摸事件,并且使用pointerenter和PointerExited 事件
检测当一个手指从一个键滑到另一个键。
因此,本文描述的触摸跟踪效果实现了UWP方法。
触摸跟踪Effect API
Touch Tracking Effect Demos示例包含实现底层触摸跟踪的类(和枚举)。这些类型属于命名空间TouchTracking
,并都以单词Touch开始。TouchTrackingEffectDemos便携式类库项目包括触摸事件类型的TouchActionType枚举:
public enum TouchActionType { Entered, Pressed, Moved, Released, Exited, Cancelled }
所有平台还包含一个指示触摸事件已被取消的事件。
TouchEffect
类在PCL源自于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); } }
应用程序可以使用Id属性跟踪单个手指。通知IsInContact
属性。这个属性永远是Pressed
(按压)事件为true
,Released
事件为false
。也总是在iOS和Android上Moved
(移动)事件为true
。在UWP上,当程序运行在桌面鼠标指针移动时没有按下按钮时,IsInContact
属性Moved
(移动)事件为可能为false
。
你可以在自己的应用程序中使用TouchEffect
类到,包括解决方案的PCL项目中的文件,并添加一个实例到任何Xamarin.Froms元素的Effects集合中。附加一个处理程序到TouchAction
事件已获得触摸事件。
在你自己的应用程序中使用TouchEffect
,你还需要在TouchTrackingEffectDemos解决方案中包含平台的实现。
触摸跟踪Effect实现
iOS、Android和UWP对TouchEffect
实现的描写在下面,首先是简单的实现(UWP),最后是iOS的实现,因为iOS的实现比其他的更加复杂。
UWP实现
UWP实现TouchEffect
是简单的,类继承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() { // 获取与该效果附加到的元素对应的Windows FrameworkElement frameworkElement = Control == null ? Container : Control; // 获取PCL中的 TouchEffect 类 effect = (TouchTracking.TouchEffect)Element.Effects. FirstOrDefault(e => e is TouchTracking.TouchEffect); if (effect != null && frameworkElement != null) { // 保存方法,以调用触摸事件 onTouchAction = effect.OnTouchAction; // 在FrameworkElement上设置事件处理程序 frameworkElement.PointerEntered += OnPointerEntered; frameworkElement.PointerPressed += OnPointerPressed; frameworkElement.PointerMoved += OnPointerMoved; frameworkElement.PointerReleased += OnPointerReleased; frameworkElement.PointerExited += OnPointerExited; frameworkElement.PointerCanceled += OnPointerCancelled; } } ... }
OnPointerPressed
处理程序通过调用CommonHandler
方法中的onTouchAction
字段来调用效果事件:
public class TouchEffect : PlatformEffect { ... void OnPointerPressed(object sender, PointerRoutedEventArgs args) { CommonHandler(sender, TouchActionType.Pressed, args); // 检查捕获属性的设置。 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也会检查PCL effect类中Capture
属性的值,如果值为true,则调用CapturePointer
。
其他UWP事件处理程序更简单:
public class TouchEffect : PlatformEffect { ... void OnPointerEntered(object sender, PointerRoutedEventArgs args) { CommonHandler(sender, TouchActionType.Entered, args); } ... }
Android实现
Android和iOS实现必然更复杂,因为当一个手指从一个元素移动到其他元素是,他们必须实现Exited
和
事件。这两个实现的结构类似。Entered
AndroidTouchEffect
类添加一个
事件的处理程序:Touch
view = Control == null ? Container : Control; ... view.Touch += OnTouch;
TouchEffect
类还要定义两个静态的字典:
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>(); ...
每次调用
覆盖时,OnAttached
viewDictionary
都会获取一个新的entry
viewDictionary.Add(view, this);
在OnDetached中将entry从字典中删除。每个TouchEffect
的实例都与一个特定的视图关联,这个视图的effect
是附加的。静态的字典允许任何TouchEffect
的实现去枚举所有其他视图和他们对于的
实现。这是允许将事件从一个视图转移到另一个视图的必要条件。TouchEffect
Android
分配一个ID code
到触摸事件,为了允许应用程序跟踪单个手指。idToEffectDictionary
将这个ID code
与
示例关联起来。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 = pclTouchEffect.Capture; break;
当手指从屏幕中释放时,项从
中删除,idToEffectDictionary
方法只收集调用FireEvent
方法所需的所有信息:OnTouchAction
void FireEvent(TouchEffect touchEffect, int id, TouchActionType actionType, Point pointerLocation, bool isInContact) { // 获取调用触发事件的方法。 Action<Element, TouchActionEventArgs> onTouchAction = touchEffect.pclTouchEffect.OnTouchAction; // 获取视图中指针的位置。 touchEffect.view.GetLocationOnScreen(twoIntArray); double x = pointerLocation.X - twoIntArray[0]; double y = pointerLocation.Y - twoIntArray[1]; Point point = new Point(fromPixels(x), fromPixels(y)); // 调用方法 onTouchAction(touchEffect.formsElement, new TouchActionEventArgs(id, actionType, point, isInContact)); }
所有其他触摸类型都以两种不同的方式处理:如果Capture
属性为true,
触摸事件可以直接的简单转化为
信息。当TouchEffect
属性为Capture
信息获取更加困难,因为触摸事件可能需要从一个视图移动到其他视图。这是false,
TouchEffect
方法的职责,它在移动事件中被调用。这个方法使用两个静态字典。他通过枚举CheckForBoundaryHop
判断手指当前触摸的视图,并且使用viewDictionary
存储现在的idToEffectDictionary
实现(和现在的视图)关联到一个独有的TouchEffect
ID:
void CheckForBoundaryHop(int id, Point pointerLocation) { TouchEffect touchEffectHit = null; foreach (Android.Views.View view in viewDictionary.Keys) { // 获取视图矩形 try { view.GetLocationOnScreen(twoIntArray); } catch // System.ObjectDisposedException: 无法访问已处理的对象。 { 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; } }
如果idToEffectionDictionary
有更新,方法可能调用
为了FireEvent
和Exited
从一个视图转移到另一个视图。然而,手指可能被移动到一个没有附加Entered
的视图区域,或者从该区域移动到带有附加TouchEffect
的视图。TouchEffect
当视图被存取时注意try
和catch
代码块。在导航页面,导航回主界面时,
方法是没有被调用的,并且项保留在OnDetached
中,但是viewDictionary
认为他们已被处理。Android
iOS实现
iOS
实现与Android
实现类似,只是iOS
类必须实例化一个TouchEffect
的派生类。这是一个在UIGestureRecognizer
项目名为iOS
的类。这个类维持两个静态的字典,用来存储TouchRecognizer
的实例:TouchRecognizer
static Dictionary<UIView, TouchRecognizer> viewDictionary = new Dictionary<UIView, TouchRecognizer>(); static Dictionary<long, TouchRecognizer> idToTouchDictionary = new Dictionary<long, TouchRecognizer>();
这个
类的结构类似于TouchRecognizer
的Android
类。TouchEffect
让触摸效果发挥作用
TouchTrackingEffectDemos程序包含5个页面,他们用来测试常见的触摸跟踪效果。
BoxView Dragging页面运行你去添加BoxView
元素到一个
然后在屏幕上拖拽他们。AbsoluteLayout,
实例化两个XAML file
按钮分别添加Button
元素到BoxView
或清空AbsoluteLayout,
AbsoluteLayout。
code-behind file中的方法添加一个新的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提供了一个条目,并且那个拖动的BoxView的touch Id属性与字典条目中的一个条目相匹配:
void OnTouchEffectAction(object sender, TouchActionEventArgs args) { BoxView boxView = sender as BoxView; switch (args.Type) { case TouchActionType.Pressed: // 在已经触摸的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
逻辑将
对象的TouchEffect
属性设置为Capture(捕获)
。这可以将所有后续事件交付给同一个事件处理程序。true
Moved
逻辑通过变更
的附加属性来移动LayoutBounds
。事件参数的BoxView
属性总是相对于被拖拽的Location
而言,如果BoxView
被一个恒定的速率拖拽,连贯事件的BoxView
属性将会大致相同。例如,如果一个手指在Location
中心按压,BoxView
操作保存一个Pressed
的属性,对于后续事件来说,这仍然是相同的。如果PressPoint(50,50)
是已恒定的熟虑拖拽对角线,后来的BoxView
属性在Location
操作时,它的值应该是Moved
,在这种情况下,移动的逻辑在(55,55)
的水平和垂直位置增加了BoxView
。这移动了5
,使它的中心再次直接在手指下面。BoxView
您可以使用不同的手指同时移动多个BoxView元素。
子类视图
通常Xamarin.Forms元素容易处理自己的触摸事件。Draggable BoxView Dragging页的功能与BoxView Dragging页的相同,但是用户拖拽的元素是来自BoxView的DraggableBoxView类的实例:
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
下面两个示范需要graphics(制图),并且为了这个目的使用了SkiaSharp。在你学些这些实例前,你可能需要学习一些Using SkiaSharp in Xamarin.Forms。前面两篇文章("SkiaSharp Drawing Basics" 和"SkiaSharp Lines and Paths")包含你需要的任何东西。
Ellipse Drawing页允许你使用手指在屏幕上画一个椭圆。依赖你如何移动你的手指,你可以从左上到右下画椭圆,或从任何一个地方到其他地方。使用随机颜色和不透明度绘制椭圆。
然后如果你触摸一个椭圆,你可以拖拽他到其他地方。这需要一种称为“hit-testing”的技术,它涉及在特定的点上搜索图形对象。SkiaSharp椭圆不是Xamarin.Forms元素,所以他们不能执行我们的
TouchEffect处理。TouchEffect必须应用于整个SKCanvasView对象。
EllipseDrawPage.xaml文件在一个single-cell Grid中实例化
SKCanvasView。
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和UWP中
TouchEffect可以直接附加到SKCanvasView上,但是在iOS上
TouchEffect不能工作。注意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; } // 拖拽操作 public Point LastFingerLocation { set; get; } // 拖拽测试 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。
code-behind file维护三个集合:
Dictionary<long, EllipseDrawingFigure> inProgressFigures = new Dictionary<long, EllipseDrawingFigure>(); List<EllipseDrawingFigure> completedFigures = new List<EllipseDrawingFigure>(); Dictionary<long, EllipseDrawingFigure> draggingFigures = new Dictionary<long, EllipseDrawingFigure>();
draggingFigure字典包含一个
completedFigures集合的子集。SkiaSharp的
PaintSurface事件处理程序简单渲染
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
的处理。这是hit-testing处理的地方,但是如果代码发现用户手指下的椭圆,那么椭圆只能被拖拽,如果它没有还没有被另外的手指拖拽。如果用户手指下没有椭圆,那么代码开始处理绘画一个新的椭圆:
case TouchActionType.Pressed: bool isDragOperation = false; // 循环已完成的图形 foreach (EllipseDrawingFigure fig in completedFigures.Reverse<EllipseDrawingFigure>()) { // 检查手指是否碰到了一个椭圆 if (fig.IsInEllipse(ConvertToPixel(args.Location))) { // 暂时假定这是一个拖动操作。 isDragOperation = true; // 循环所有当前开始拖拽的手指 foreach (EllipseDrawingFigure draggedFigure in draggingFigures.Values) { // 如果这里匹配, 我们需要挖掘更深 if (fig == draggedFigure) { isDragOperation = false; break; } } if (isDragOperation) { fig.LastFingerLocation = args.Location; draggingFigures.Add(args.Id, fig); break; } } } if (isDragOperation) { // 将拖动的椭圆移动到completedFigures 的末尾,这样它就会被绘制在顶部
EllipseDrawingFigure fig = draggingFigures[args.Id]; completedFigures.Remove(fig); completedFigures.Add(fig); } else // 开始创建一个新的椭圆 { // 产生随机byte为了随机颜色 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;
Finger Paint页是SkiaSharp的其他示例,你可以从两个选择器视图中选择一个笔划颜色和笔画宽度,然后用一个或多个手指绘制:
这个示例也需要一个单独的类来表示屏幕上绘制的每一行:
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,调用在path对象上
MoveTo去存储初始点,并且添加哪个对象到inProgressPolylines字典中。
Moved处理用新的手指位置调用path对象上的
LineTo,而
Released处理将以完成的polyline从
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); } }
跟踪视图到视图的触摸
之前所有的实例都为了TouchEffect将
Capture属性设置为true,当
TouchEffect被创建时或
Pressed事件被触发时。确保相同的元素接收第一个按下视图的手指所关联的所有事件。最后一个示例没有将
Capture设置为true。这是因为当手指接触屏幕从一个元素到其他元素时行为是不一样的。手指移动的元素从接收到一个Type属性设置到
TouchActionType.Exited
,第二个元素接收一个带有TouchActionType.Entered的Type
设置的事件。
这种类型的触摸处理对音乐键盘非常有用。一个键应该能够在被按压的时候检测到,而且当手指从一个键滑到另一个键时也能检测到。
Silent Keyboard界面定义了少量的WhiteKey
和BlackKey
类,这些是源自BoxView的Key
。
Key类类已经准备好用于实际的音乐程序。它定义公共的IsPressed和KeyNumber
属性,这将被设置为MIDI标准所建立的关键代码。Key类也定义了名为StatusChanged的事件,当IsPressed属性被更改时调用。
每个键上允许有多个手指。为此,Key类维护了当前触摸该键的所有手指touch ID的List。
List<long> ids = new List<long>();
TouchAction 事件处理程序为
Pressed(释放)事件类型和
ID,但是只有当Entered(退出)事件类型在ids列表中添加
IsInContact属性为true时才为Entered事件添加。ID是用来从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事件。
XAML file页面中设置了各种白键和黑键元素,当手机处于横向模式时,效果最好:
如果你把手指划过这些键,你会看到,触摸事件从一个键转移到另一个键的颜色的细微变化。
总结
本文演示了如何在效果中调用事件,以及如何编写和使用实现低级多点触摸处理的效果。