原文地址

在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 MSDNUIElement类的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(按压)事件为trueReleased事件为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实现必然更复杂,因为当一个手指从一个元素移动到其他元素是,他们必须实现ExitedEntered事件。这两个实现的结构类似。

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 codeTouchEffect示例关联起来。

当手指按压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为了ExitedEntered从一个视图转移到另一个视图。然而,手指可能被移动到一个没有附加TouchEffect的视图区域,或者从该区域移动到带有附加TouchEffect的视图。

当视图被存取时注意trycatch代码块。在导航页面,导航回主界面时,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类的结构类似于AndroidTouchEffect类。

让触摸效果发挥作用

TouchTrackingEffectDemos程序包含5个页面,他们用来测试常见的触摸跟踪效果。

BoxView Dragging页面运行你去添加BoxView元素到一个AbsoluteLayout,然后在屏幕上拖拽他们。XAML file实例化两个Button按钮分别添加BoxView元素到AbsoluteLayout,或清空AbsoluteLayout。

code-behind file中的方法添加一个新的BoxViewAbsoluteLayout,并且将一个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界面定义了少量的WhiteKeyBlackKey类,这些是源自BoxView的Key

Key类类已经准备好用于实际的音乐程序。它定义公共的IsPressed和KeyNumber属性,这将被设置为MIDI标准所建立的关键代码。Key类也定义了名为StatusChanged的事件,当IsPressed属性被更改时调用。

每个键上允许有多个手指。为此,Key类维护了当前触摸该键的所有手指touch ID的List。

List<long> ids = new List<long>();

TouchAction 事件处理程序为Pressed(释放)事件类型和Entered(退出)事件类型在ids列表中添加ID,但是只有当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页面中设置了各种白键和黑键元素,当手机处于横向模式时,效果最好:

如果你把手指划过这些键,你会看到,触摸事件从一个键转移到另一个键的颜色的细微变化。

总结

本文演示了如何在效果中调用事件,以及如何编写和使用实现低级多点触摸处理的效果。

XamarinForm Effects 调用事件的更多相关文章

  1. Vue父组件与子组件传递事件/调用事件

    1.Vue父组件向子组件传递事件/调用事件 <div id="app"> <hello list="list" ref="child ...

  2. c# 开发ActiveX控件,添加事件,QT调用事件

    c# 开发 ActiveX 的过程参考我的另一篇文章 :  https://www.cnblogs.com/baqifanye/p/10414004.html 本篇讲如何 在C# 开发的ActiveX ...

  3. PowerBuilder学习笔记之调用事件和函数

    2.7.1调用事件和函数 完整语法:[ObjectName]ancestorclass::[type][when]name([argumnetlist]) 说明:ObjectName:指定函数或事件的 ...

  4. C#中怎样跨窗体调用事件-从事件订阅实例入手

    场景 C#中委托与事件的使用-以Winform中跨窗体传值为例: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/100150700 ...

  5. [原创] delphi KeyUp、KeyPress、Keydown区别和用法,如何不按键盘调用事件

    KeyPress (Sender: TObject; var Key: Char);   当用户按下键盘上的字符键(字母,数字) 会触发该事件,功能键则不会(F1-F12,Ctrl,Alt,Shift ...

  6. 从ajax获取的数据无法通过Jquery选择器来调用事件

    如果标签是动态生成的,比如说div.tr.td等,若需通过Jquery来获取事件,那么需要用live来绑定相应的事件. 比如说绑定div的click事件 $("div").live ...

  7. vue 父组件向子组件传递事件/调用事件

    方法一:子组件监听父组件发送的方法 方法二:父组件调用子组件方法 子组件: export default { mounted: function () { this.$nextTick(functio ...

  8. 【学徒日记】Unity 动画调用事件

    http://note.youdao.com/noteshare?id=a15f965fc57a0b25c87ee09388cf0f4a 具体内容看上面的链接. 1. 在脚本里写一个函数,它的参数只能 ...

  9. CAD对象的夹点被编辑完成后调用事件(com接口VB语言)

    主要用到函数说明: _DMxDrawXEvents::ObjectGripEdit 对象的夹点被编辑完成后,会调用该事件,详细说明如下: 参数 说明 LONGLONG lId 对象的id LONG i ...

随机推荐

  1. MVC学习笔记(一)

    首先感谢慕课网这个平台提供给我的学习机会,感谢PengCheng老师的"MVC架构模式分析与设计课程". 1.数组的声明: $controllerAllow = array('te ...

  2. Android Activity的四种经典传值方法

    文/ http://blog.csdn.net/sk719887916/article/details/41723613  skay 开发中遇到多个activity的传值问题 相邻两个之间的传值 或者 ...

  3. LeetCode之“动态规划”:Triangle

    题目链接 题目要求: Given a triangle, find the minimum path sum from top to bottom. Each step you may move to ...

  4. Android特效专辑(十二)——仿支付宝咻一咻功能实现波纹扩散特效,精细小巧的View

    Android特效专辑(十二)--仿支付宝咻一咻功能实现波纹扩散特效,精细小巧的View 先来看看这个效果 这是我的在Only上添加的效果,说实话,Only现在都还只是半成品,台面都上不了,怪自己技术 ...

  5. 数据结构之---二叉树C实现

    学过数据结构的都知道树,那么什么是树? 树(tree)是包含n(n>0)个结点的有穷集,其中: (1)每个元素称为结点(node): (2)有一个特定的结点被称为根结点或树根(root). (3 ...

  6. err:安装程序试图挂载映像 1(缺少ISO 9660图像)

    一般出现此错误是因为你没有把相应的CentOS-6.4-i386-bin-DVD1.iso文件放入到你装系统所引导的盘中,造成找不到挂载映像文件. ubuntu-12.04.3-desktop-i38 ...

  7. DB Query Analyzer 5.02 is distributed, 53 articles concerned have been published

    DB Query Analyzer is presented by Master Gen feng, Ma from Chinese Mainland. It has English version ...

  8. 在Eclipse 中集成SVN

    在项目开发的过程中,我们需要用到版本控制工具,最常见的也就是SVN了,下面就来介绍最简单的一种在Elipse中集成svn工具. 第一步:下载 svn包,如site-1.6.5.zip或者site-1. ...

  9. Mac OS X下让ruby支持tcl/tk

    我记得在老早在OS X10.8下使用ruby1.9.x的时候只要到下载安装ActiveTcl8.5,没怎么配置就运行tk好好的.但是近日想重新执行下tk代码,发现在require 'tk'的时候就报错 ...

  10. Angular集成admin-lte框架

    其实上一篇里面提到的集成datatables.net就是admin-lte里面的一个子插件,不过这个子插件,他是自带types定义文件的,admin-lte这个东西在DefinitelyTyped里面 ...