划動(Swipe)html
划動手勢和揮手(wave)手勢相似。識別划動手勢須要不斷的跟蹤用戶手部運動,並保持當前手的位置以前的手的位置。由於手勢有一個速度閾值,咱們須要追蹤手運動的時間以及在三維空間中的座標。下面的代碼展現了存儲手勢位置點的X,Y,Z座標以及時間值。若是熟悉圖形學中的矢量計算,能夠將這個認爲是一個四維向量。將下面的結構添加到類庫中。算法
public struct GesturePoint { public double X { get; set; } public double Y { get; set; } public double Z { get; set; } public DateTime T { get; set; } public override bool Equals(object obj) { var o = (GesturePoint)obj; return (X == o.X) && (Y == o.Y) && (Z == o.Z)&&(T==o.T); } public override int GetHashCode() { return base.GetHashCode(); } }
咱們將在KinectCursorManager對象中實現划動手勢識別的邏輯,這樣在後面的磁吸幻燈片按鈕中就能夠複用這部分邏輯。實現代碼以下,代碼中爲了支持划動識別,須要向KinectCurosrManager對象中添加幾個字段。GesturePoints集合存儲路徑上的全部點,雖然咱們會一邊移除一些點而後添加新的點,可是該集合不可能太大。SwipeTime和swipeDeviation分別提供了划動手勢經歷的時間和划動手勢在y軸上的偏移閾值。划動手勢經歷時間過長和划動手勢路徑偏移y值過大都會使得划動手勢識別失敗。咱們會移除以前的路徑上的點,而後添加新的划動手勢上的點。SwipeLength提供了連續划動手勢的閾值。咱們提供了兩個事件來處理划動手勢識別成功和手勢不合法兩種狀況。考慮到這是一個純粹的手勢,與GUI界面無關,因此在實現過程當中不會使用click事件。編程
xOutOfBoundsLength和initialSwipeX用來設置划動手勢的開始位置。一般,咱們並不關心揮划動手勢的開始位置,只用在gesturePoints中尋找必定數量連續的點,而後進行模式匹配就能夠了。可是有時候,咱們只從某一個划動開始點來進行划動識別也頗有用。例如若是在屏幕的邊緣,咱們實現水平滾動,在這種狀況下,咱們須要一個偏移閾值使得咱們能夠忽略在屏幕外的點,由於這些點不能產生手勢。ide
private List<GesturePoint> gesturePoints; private bool gesturePointTrackingEnabled; private double swipeLength, swipeDeviation; private int swipeTime; public event KinectCursorEventHandler swipeDetected; public event KinectCursorEventHandler swipeOutofBoundDetected; private double xOutOfBoundsLength; private static double initialSwipeX;
下面的代碼展現了一些幫助方法以及公共屬性來管理手勢追蹤。GesturePointTrackingInitialize方法用來初始化各類手勢追蹤的參數。初始化好了划動手勢以後,須要調用GesturePointTrackingStart方法。天然須要一個相應的GesturePointTrackingStop方法來結束揮動手勢識別。最後咱們須要提供兩個重載的幫助方法ResetGesturePoint來管理一系列的咱們不須要的手勢點。函數
public void GesturePointTrackingInitialize(double swipeLength, double swipeDeviation, int swipeTime, double xOutOfBounds) { this.swipeLength = swipeLength; this.swipeDeviation = swipeDeviation; this.swipeTime = swipeTime; this.xOutOfBoundsLength = xOutOfBounds; } public void GesturePointTrackingStart() { if (swipeLength + swipeDeviation + swipeTime == 0) throw new InvalidOperationException("揮動手勢識別參數沒有初始化!"); gesturePointTrackingEnabled = true; } public void GesturePointTrackingStop() { xOutOfBoundsLength = 0; gesturePointTrackingEnabled = false; gesturePoints.Clear(); } public bool GesturePointTrackingEnabled { get { return gesturePointTrackingEnabled ; } } private void ResetGesturePoint(GesturePoint point) { bool startRemoving = false; for (int i= gesturePoints.Count; i >=0; i--) { if (startRemoving) gesturePoints.RemoveAt(i); else if (gesturePoints[i].Equals(point)) startRemoving = true; } } private void ResetGesturePoint(int point) { if (point < 1) return; for (int i = point-1; i >=0; i--) { gesturePoints.RemoveAt(i); } }
ResetGesturePoint 從下劃姿式的底部點向上遍歷,不等於point(傳入的匹配結點)的保留 —— 下劃過程的所有點,等於以及以上的點無效,所有刪除 ~~this
划動(swipe)手勢識別的核心算法在HandleGestureTracking方法中,代碼以下。將KinectCursorManager中的UpdateCursor方法和Kinect中的骨骼追蹤事件綁定。每一次當獲取到新的座標點時,HandGestureTracking方法將最新的GesturePoint數據添加到gesturePoints集合中去。而後執行一些列條件檢查,首先判斷新加入的點是否以手勢開始位置爲起點參考,偏離Y軸過遠。若是是,拋出一個超出範圍的事件,而後將全部以前累積的點清空,而後開始下一次的划動識別。其次,檢查手勢開始的時間和當前的時間,若是時間差大於閾值,那麼移除開始處手勢點,而後將緊接着的點做爲手勢識別的起始點。若是新的手的位置在這個集合中,就很好。緊接着,判斷划動起始點的位置和當前位置的X軸上的距離是否超過了連續划動距離的閾值,若是超過了,則觸發SwipeDetected事件,若是沒有,咱們能夠有選擇性的判斷,當前位置的X點是否超過了划動識別的最大區間返回,而後觸發對於的事件。而後咱們等待新的手部點傳到HandleGestureTracking方法中去。spa
private void HandleGestureTracking(float x, float y, float z) { if (!gesturePointTrackingEnabled) return; // check to see if xOutOfBounds is being used if (xOutOfBoundsLength != 0 && initialSwipeX == 0) { initialSwipeX = x; } GesturePoint newPoint = new GesturePoint() { X = x, Y = y, Z = z, T = DateTime.Now }; gesturePoints.Add(newPoint); GesturePoint startPoint = gesturePoints[0]; var point = new Point(x, y); //check for deviation if (Math.Abs(newPoint.Y - startPoint.Y) > swipeDeviation) { //Debug.WriteLine("Y out of bounds"); if (swipeOutofBoundDetected != null) swipeOutofBoundDetected(this, new KinectCursorEventArgs(point) { Z = z, Cursor = cursorAdorner }); ResetGesturePoint(gesturePoints.Count); return; } if ((newPoint.T - startPoint.T).Milliseconds > swipeTime) //check time { gesturePoints.RemoveAt(0); startPoint = gesturePoints[0]; } if ((swipeLength < 0 && newPoint.X - startPoint.X < swipeLength) // check to see if distance has been achieved swipe left || (swipeLength > 0 && newPoint.X - startPoint.X > swipeLength)) // check to see if distance has been achieved swipe right { gesturePoints.Clear(); //throw local event if (swipeDetected != null) swipeDetected(this, new KinectCursorEventArgs(point) { Z = z, Cursor = cursorAdorner }); return; } if (xOutOfBoundsLength != 0 && ((xOutOfBoundsLength < 0 && newPoint.X - initialSwipeX < xOutOfBoundsLength) // check to see if distance has been achieved swipe left || (xOutOfBoundsLength > 0 && newPoint.X - initialSwipeX > xOutOfBoundsLength)) ) { if (swipeOutofBoundDetected != null) swipeOutofBoundDetected(this, new KinectCursorEventArgs(point) { Z = z, Cursor = cursorAdorner }); } }
磁性幻燈片設計
他比磁性按鈕好的地方就是,不須要用戶等待一段時間。在Xbox遊戲中,沒有人願意去等待。而下壓按鈕又有自身的缺點,最主要的是用戶體驗不是很好。磁性幻燈片和磁性按鈕同樣,一旦用戶進入到按鈕的有效區域,光標就會自定鎖定到某一點上。可是在這一點上,能夠有不一樣的表現。除了懸停在按鈕上方一段時間觸發事件外,用戶能夠划動收來激活按鈕。3d
從編程角度看,磁性幻燈片基本上是磁性按鈕和划動手勢(swipe)的組合。要開發一個磁性幻燈片按鈕,咱們能夠簡單的在可視化樹中的懸浮按鈕上聲明一個計時器,而後再註冊滑動手勢識別事件。下面的代碼展現了磁性幻燈片按鈕的基本結構。其構造函數已經在基類中爲咱們聲明好了計時器。InitializeSwipe和DeinitializeSwipe方法負責註冊KinectCursorManager類中的滑動手勢識別功能。code
另外,咱們也須要將控件的滑動手勢的初始化參數暴露出來,這樣就能夠根據特定的須要進行設置了。下面的代碼展現了SwipeLength和XOutOfBoundsLength屬性,這兩個都是默認值的相反數。這是由於磁性幻燈片按鈕通常在屏幕的右側,須要用戶向左邊划動,所以,相對於按鈕位置的識別偏移以及邊界偏移是其X座標軸的相反數。public class MagneticSlide:MagnetButton { private bool isLookingForSwipes; public MagneticSlide() { base.isLockOn = false; } private void InitializeSwipe() { if (isLookingForSwipes) return; var kinectMgr = KinectCursorManager.Instance; kinectMgr.GesturePointTrackingInitialize(SwipeLength, MaxDeviation, MaxSwipeTime, xOutOfBoundsLength); kinectMgr.swipeDetected += new KinectCursorEventHandler(kinectMgr_swipeDetected); kinectMgr.swipeOutofBoundDetected += new KinectCursorEventHandler(kinectMgr_swipeOutofBoundDetected); kinectMgr.GesturePointTrackingStart(); } private void DeInitializeSwipe() { var KinectMgr = KinectCursorManager.Instance; KinectMgr.swipeDetected -= new KinectCursorEventHandler(kinectMgr_swipeDetected); KinectMgr.swipeOutofBoundDetected -= new KinectCursorEventHandler(kinectMgr_swipeOutofBoundDetected); KinectMgr.GesturePointTrackingStop(); isLookingForSwipes = false; }public static readonly DependencyProperty SwipeLengthProperty = DependencyProperty.Register("SwipeLength", typeof(double), typeof(MagneticSlide), new UIPropertyMetadata(-500d)); public double SwipeLength { get { return (double)GetValue(SwipeLengthProperty); } set { SetValue(SwipeLengthProperty, value); } } public static readonly DependencyProperty MaxDeviationProperty = DependencyProperty.Register("MaxDeviation", typeof(double), typeof(MagneticSlide), new UIPropertyMetadata(100d)); public double MaxDeviation { get { return (double)GetValue(MaxDeviationProperty); } set { SetValue(MaxDeviationProperty, value); } } public static readonly DependencyProperty XOutOfBoundsLengthProperty = DependencyProperty.Register("XOutOfBoundsLength", typeof(double), typeof(MagneticSlide), new UIPropertyMetadata(-700d)); public double XOutOfBoundsLength { get { return (double)GetValue(XOutOfBoundsLengthProperty); } set { SetValue(XOutOfBoundsLengthProperty, value); } } public static readonly DependencyProperty MaxSwipeTimeProperty = DependencyProperty.Register("MaxSwipeTime", typeof(int), typeof(MagneticSlide), new UIPropertyMetadata(300)); public int MaxSwipeTime { get { return (int)GetValue(MaxSwipeTimeProperty); } set { SetValue(MaxSwipeTimeProperty, value); } }
要實現磁性幻燈片按鈕的邏輯,咱們只須要處理基類中的enter事件,以及划動手勢識別事件便可。咱們不會處理基類中的leave事件,由於當用戶作划動手勢時,極有可能會不當心觸發leave事件。咱們不想破壞以前初始化好了的deactivate算法邏輯,因此取而代之的是,咱們等待要麼下一個划動識別成功,要麼在關閉划動識別前划動手勢超出識別範圍。當探測到划動時,觸發一個標準的click事件。
public static readonly RoutedEvent SwipeOutOfBoundsEvent = EventManager.RegisterRoutedEvent("SwipeOutOfBounds", RoutingStrategy.Bubble, typeof(KinectCursorEventHandler), typeof(KinectInput)); public event RoutedEventHandler SwipeOutOfBounds { add { AddHandler(SwipeOutOfBoundsEvent, value); } remove { RemoveHandler(SwipeOutOfBoundsEvent, value); } } void KinectMgr_swipeOutofBoundDetected(object sender, KinectCursorEventArgs e) { DeInitializeSwipe(); RaiseEvent(new KinectCursorEventArgs(SwipeOutOfBoundsEvent)); } void KinectMgr_swipeDetected(object sender, KinectCursorEventArgs e) { DeInitializeSwipe(); RaiseEvent(new RoutedEventArgs(ClickEvent)); } protected override void OnKinectCursorEnter(object sender, KinectCursorEventArgs e) { InitializeSwipe(); base.OnKinectCursorEnter(sender, e); }
垂直滾動條 (Vertical Scroll)
傳統上,垂直滾動條一直是交互界面設計的一個禁忌。可是垂直滾動條在划動觸摸界面中獲得了很好的應用。因此Xbox和Sony PlayStation系統中都使用了垂直滾動條來構建菜單。Harmonix’s的《舞林大會》(Dance Central)這一系列遊戲使用了垂直滾動條式的菜單系統。Dance Central第一次成功的使用了垂直滾動界面做爲手勢交互界面。在下面的手勢交互圖中,當用戶擡起或者放下手臂時會使得屏幕的內容垂直滾動。胳膊遠離身體,擡起手臂會使得屏幕或者菜單從下往上移動,放下手臂會使得從上往下移動。
當用戶作划動手勢時,在整個手的划動過程當中會手的位置在水平方向會保持相對一致。這就使得若是想進行屢次連續的划動手勢時會產生一些問題。有時候會產生一些比較尷尬的場景,那就是會無心中撤銷前一次的划動手勢。例如,用戶使用右手從右向左進行划動手勢,使得頁面會跳轉到下一頁,如今用戶的右手在身體的左邊位置了,而後用戶想將手移動回原始的開始位置以準備下一次的從右向左的揮動手勢。可是,若是用於依然保持手在水平位置大體一致的話,應用程序會探測到一次從左向右的划動操做而後又將界面切換到了以前的那一頁。這就使得用戶必須建立一個循環的運動來避免沒必要要的誤讀。更進一步,頻繁的划動手勢也容易使得用戶疲勞,而垂直方向的划動也只會加重這一問題。
可是垂直滾動條則不會有上述的這些用戶體驗上的缺點。他比較容易使用,對用戶來講也更加友好,另外,用戶也不須要爲了保持手在水平或者垂直方向一致而致使的疲勞。從技術方面來說,垂直滾動操做識別較划動識別簡單。垂直滾動在技術上是一個姿式而不是手勢。滾動操做的探測是基於當前手臂的位置而不是手臂的運動。滾動的方向和大小由手臂和水平方向的夾角來肯定。下圖演示了垂直滾動。
使用以前的姿式識別那篇文章中的內容,咱們可以計算從用戶的身體到肩部和手腕的夾角,定義一個角度區間做爲中間姿式,當用戶手臂在這一區間內時,不會產生任何動做,如上圖中的,當手臂天然處於-5度或者355度時,做爲偏移的零點。建議在實際開發中,將零點的偏移上下加上20度左右。當用戶的手臂離開這一區域時,離開的夾角及變化的幅度根據需求而定。可是建議至少在0度區間上下有兩個區間來表示小幅和大幅的增長。這使得可以更好的實現傳統的人機交互界面中的垂直滾動條的邏輯
通用暫停按鈕 (Universal Pause)
暫停按鈕,一般做爲引導手勢或者退出手勢,是微軟建議在給用戶提供引導時不多的幾個手勢之一。這個手勢是經過將左臂保持和身體45度角來完成的。在不少Kinect的遊戲中都使用到了這一手勢,用來暫停動做或者喚出Xbox菜單。和本文以前介紹的手勢不同,這個手勢並無什麼符號學上的含義,是一個認爲設計的動做。通用暫停手勢很容易實現,也不必定要限制手臂,而且不容易和其餘手勢混淆。