原文地址

在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. Developing User Interfaces

      Developing a User Interface with ADF Faces Purpose This tutorial covers developing the user interf ...

  2. redis注册成window服务

    注册服务 redis-server.exe –service-install redis.windows.conf 删除服务 redis-server –service-uninstall 开启服务 ...

  3. HBase事务

    众所周知,ACID是指原子性(Atomicity),一致性(Consistency),隔离性(Isolation)和持久性(Durability). HBase对同一行数据的操作提供ACID保证.HB ...

  4. HBase丢失数据的故障和原因分析

    hbase的稳定性是近期社区的重要关注点,毕竟稳定的系统才能被推广开来,这里有几次稳定性故障和大家分享.     第一次生产故障的现象及原因     现象: 1 hbase发现无法写入 2 通过hbc ...

  5. SharePoint 2007 图片库视图不可用、页面标题不显示

    描述: 问题1:SharePoint新建图片库,想选择"视图"-"所有图片",选择"详细信息.幻灯片.缩略图"等视图,均没有反应.如图1. ...

  6. shell中关于sort的-o选项

    sort -o选项意思为将排序后的结果写入文件,但你可能会说我可以重定向啊: sort >file 但如果你要排序文件names里的行再写回排序后的结果: sort names > nam ...

  7. Yii2.0源码阅读-PHP如何与redis通信?

    PHP与Redis可以通过socket进行通信,前提是PHP需要实现Redis的协议 RESP协议描述: 字符串 \r\n : 表示一个正确的状态信息,具体信息是'+'后面的字符(Simple Str ...

  8. MOOS通配符订阅

    MOOS通配符订阅 简介 通配符订阅是MOOSV10的重要进步,客户端可以通过此方式订阅名字和来源符合简单正则表达式的数据. 现在仅支持"*"和"?"两种通配符 ...

  9. RunTime运行时在iOS中的应用之UITextField占位符placeholder

    RunTime运行时机制 runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API. 在我们平时编写的Objective-C代码中, 程序运行过程时, 其实最终 ...

  10. nodejs+express blog项目分享

    项目简介:项目采用nodejs+express+typescript+mongodb技术搭建 主要功能: 1.用户注册 2.用户登录 3.文章管理模块 4.图片管理模块 5.token认证 6.密码加 ...