在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頁面中設置了各種白鍵和黑鍵元素,當手機處於橫向模式時,效果最好:
如果你把手指划過這些鍵,你會看到,觸摸事件從一個鍵轉移到另一個鍵的顏色的細微變化。
總結
本文演示了如何在效果中調用事件,以及如何編寫和使用實現低級多點觸摸處理的效果。