XamarinForm Effects 调用事件

原文地址

在Xamarin.Forms控件中实现底层多点触控跟踪。

一个effect可以定义和调用一个事件,在底层本地视图中发出信号的变化。这篇文章演示如何实现底层多点触控跟踪,以及如何生成信号触摸活动的事件。

 

本文描述的Effect提供了对底层触摸事件的访问。这些低级事件在现有的GestureRecognizer类中是不可用的,但是它们对于某些类型的应用程序来说是非常重要的。例如,手指画画应用程序需要跟踪单个手指在屏幕上移动的情况。音乐键盘应用程序需要检测每个按键上的点击和释放,以及一个手指从一个键滑动到另一个键的滑音。

Effect是一个理想多点触控跟踪的,因为它可以附加到任何一个Xamarin.Forms元素上。


平台触摸事件

iOS、Android和通用的Windows平台都包含一个底层API,它允许应用程序检测触摸活动。这些平台都能区分三种基础触摸事件类型:

  • Pressed 当一个手指触摸到屏幕时。
  • Moved  当一个手指触摸到屏幕移动时。
  • Released 当一个手指从屏幕上释放时。

在多点触控环境中,同一时间可以有多个手指触摸屏幕。各种平台包含一个识别(ID)号,应用程序可以用来区分多个手指。

在iOS中,UIView类定义了三个可覆盖的方法,TouchesBeganTouchesMovedTouchesEnded来对应这三个事件。文章多点触控跟踪描写了如何使用这些方法。但是,iOS程序不需要覆盖从UIView派生的类来使用这些方法。iOSUIGestureRecognizer也定义了这三个方法,并且你可以附加一个类的实例,它从UIGestureRecognizer派生到任何UIView对象。

在Android中,View类定义了一个可覆盖的OnTouchEvent方法去处理所有的触摸活动。这里触摸活动类型定义为枚举类型DownPointerDownMoveUpPointerUp,描述在文章多点触摸跟踪中。Android View也定义了名为Touch的事件,他允许一个事件handler附加到任何View对象上。

在通用Windows平台(UWP)中,UIElement类定义了名为PointerPressedPointerMovedPointerReleased的事件。在文章Handle Pointer Input article on MSDNUIElement类的API文档中描写了这些事件。

通用Windows平台中的Pointer(指针)API旨在统一鼠标、触摸和笔输入。因此,当鼠标移动到一个元素上时,即使鼠标按钮没有被抑制,PointerMoved事件也会被调用。与这些事件关联的PointerRoutedEvent-Args对象有一个名为Pointer的属性,这个属性有一个名为IsInContact的属性,该属性指示是按下鼠标按钮还是与屏幕进行接触。

此外,UWP定义两个名为PointerEnteredPointerExited的鼠标事件。这些指示当鼠标或手指从一个元素移动到其他元素。例如,考虑两个相邻的元素A和B。这两个元素都为指针事件安装了处理程序。当一个手指按压A时,PointerPressed事件被触发,当手指移动时,A调用PointerMoved事件。如果手指从A移动到B,A触发一个PointerExited事件,B触发一个PointerEntered事件。如果指被释放,B调用一个pointerrelease事件。

iOS和Android平台不同于UWP:当手指触摸到视图时,第一个调用TouchesBeganOnTouchEvent的视图继续得到所有的触摸活动,即使手指移动到不同的视图。如果应用程序捕捉到指针,UWP的行为类似:在pointerentry事件处理程序中,元素调用CapturePointer,然后从该手指获取所有的触摸活动。

UWP方法对某些类型的应用程序非常有用,例如,音乐键盘。每个键都可以处理该键的触摸事件,并且使用pointerenterPointerExited 事件检测当一个手指从一个键滑到另一个键。

因此,本文描述的触摸跟踪效果实现了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页面中设置了各种白键和黑键元素,当手机处于横向模式时,效果最好:

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

总结

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

 

posted @ 2018-01-19 14:52  $("#阿飞")  阅读(1554)  评论(0编辑  收藏  举报