你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 跳出手掌心

跳出手掌心

編輯:IOS開發基礎

blob.png

最近在使用UIButton的過程中遇到一個問題,我想要獲得手指拖動button並離開button邊界時的回調,於是監聽UIControlEventTouchDragExit事件,如文檔所述:

An event where a finger is dragged from within a control to outside its bounds.

這個事件正是我所需要的,可是最後卻發現當手指離開button邊界時,事件並沒有觸發,而是到了遠離button近70個像素時才收到回調。

目錄

  • 來自StackOverflow的答案

    • 檢驗結果

  • 換個思路

    • 注冊回調

    • 回調函數

    • 處理TouchUp事件

  • 結尾

為了更好的說明問題,我做了一個示例,見下圖。所期待的行為是:當手指離開button邊界時會將button的內容改為離開,進入時改為進入。另外在手指的位置給出手指距離button最上端的像素差。

1.gif

但是,當手指離開button邊界時,button的內容並沒有改變。而當手指距離button頂端70像素時才變為離開。由此可以看出,UIControlEventTouchDragExit事件並不是在離開button邊界時立刻觸發,而是在距button頂端70像素時才會。

在這裡我只是演示了手指向上移動的情況,其實向另外三個方向移動時,也會有一樣的效果,有興趣的同學可以自己嘗試一番。

而且並不僅僅是UIControlEventTouchDragExit這一個事件,所有與邊界有關的事件都有這一問題:

  • UIControlEventTouchDragInside

  • UIControlEventTouchDragOutside

  • UIControlEventTouchDragEnter

  • UIControlEventTouchDragExit

  • UIControlEventTouchUpInside

  • UIControlEventTouchUpOutside

不知道蘋果為什麼要這樣設定,一直沒有查到相關的資料。猜測可能是蘋果覺得人的手指比較粗,和屏幕的接觸面積比較大,定位也不需要那麼精准,所以設定了一個這麼大的外部區域吧。

但是很多情況下,如果我們需要更為精確的控制時,這70個像素的擴張就不行了。那麼有沒有辦法能夠更快的跳出button的手掌心呢?

來自StackOverflow的答案

經過一番查找,在StackOverflow上面找到了一個答案,它是通過覆蓋UIControl的continueTrackingWithTouch:withEvent方法,由於UIButton是派生自UIControl,因此也繼承了此方法。先來看看它的聲明:

 - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
 /*
 Description
   Sent continuously to the control as it tracks a touch related to the given event within the control’s bounds.
 Parameters
   touch
     A UITouch object that represents a touch on the receiving control during tracking.
   event
     An event object encapsulating the information specific to the user event
 Returns
   YES if touch tracking should continue; otherwise NO.
 */

這個方法判斷是否保持追蹤當前的觸摸事件。這裡根據得到的位置來判斷是否正處於button的范圍內,進而發送對應的事件。相應的代碼為:

 - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
 {
     CGFloat boundsExtension = 25.0f;
     CGRect outerBounds = CGRectInset(self.bounds, -1 * boundsExtension, -1 * boundsExtension);
 
     BOOL touchOutside = !CGRectContainsPoint(outerBounds, [touch locationInView:self]);
     if(touchOutside) {
         BOOL previousTouchInside = CGRectContainsPoint(outerBounds, [touch previousLocationInView:self]);
         if(previousTouchInside) {
             NSLog(@"Sending UIControlEventTouchDragExit");
             [self sendActionsForControlEvents:UIControlEventTouchDragExit];
         }
         else
         {
             NSLog(@"Sending UIControlEventTouchDragOutside");
             [self sendActionsForControlEvents:UIControlEventTouchDragOutside];
         }
     }
     return [super continueTrackingWithTouch:touch withEvent:event];
 }

在代碼中,boundsExtension設置為25,它便是對應著前面所討論的70,即button“手掌心”的范圍。當然我們可以將它設置為其它任何值。

檢驗結果

這個方法看起來非常好,也被原問題采納為正確答案。但在嘗試之後,我發現它有兩個嚴重的問題:

  • UIControlEventTouchDragExit會響應兩次,分別為:

    • 手指離開button邊界25個像素時觸發

    • 第二次依然是70個像素時觸發,這是UIButton的默認行為

  • 第二個問題是在事件的回調函數:

    - (void)callback:(UIButton *)sender withEvent:(UIEvent *)event

    中,由UIEvent參數計算得到的位置始終是(0, 0),它並未正確的初始化

仔細一想便能理解,在覆蓋的函數中我們進行判斷之後觸發了對應的事件,但這並沒有取消原來UIControl本應該觸發的事件,這便導致了兩次響應;並且在我們的處理中,僅僅只是觸發了事件,這裡並沒有涉及到UIEvent的初始化工作,因此最後得到的位置肯定不對了。

對於重復響應的問題,有人可能會猜,會不會上面最後一行調用父類方法有影響:

1 return [super continueTrackingWithTouch:touch withEvent:event];

我後來也嘗試過,直接在結尾返回YES,上面的問題仍然存在,可見並不是它的緣故。

換個思路

由於上面兩個問題的緣故,這個答案不可取。那還有別的辦法麼?

我們來仔細觀察前面的方法,用前半部分的代碼,可以很容易的判斷出當前位置是否位於button之內。那麼我們是否可以不在底層處理,而是在上層的回調函數中去判斷?基於這一思路,我又做了這樣的嘗試:

注冊回調

 // to get the drag event
 [btn addTarget:self action:@selector(btnDragged:withEvent:) forControlEvents:UIControlEventTouchDragInside];
 [btn addTarget:self action:@selector(btnDragged:withEvent:) forControlEvents:UIControlEventTouchDragOutside];

第一步仍然是注冊回調函數,但是注意看,這裡兩個事件注冊的是同一個回調函數btnDragged:withEvent:。而且並沒有注冊UIControlEventTouchDragExit和UIControlEventTouchDragEnter,取而代之的是UIControlEventTouchDragInside和UIControlEventTouchDragOutside,為什麼?請接著向下看。

回調函數

回調函數裡面采用了前面答案中的判斷方法,可以根據當前和之前的位置判斷出是否在button內部。然後就可以判斷出此時到底屬於哪一個事件,如下面的注釋所示。至此,我們便可以在每一個分支中做對應的處理了。

 - (void)btnDragged:(UIButton *)sender withEvent:(UIEvent *)event {
     UITouch *touch = [[event allTouches] anyObject];
     CGFloat boundsExtension = 25.0f;
     CGRect outerBounds = CGRectInset(sender.bounds, -1 * boundsExtension, -1 * boundsExtension);
     BOOL touchOutside = !CGRectContainsPoint(outerBounds, [touch locationInView:sender]);
     if (touchOutside) {
         BOOL previewTouchInside = CGRectContainsPoint(outerBounds, [touch previousLocationInView:sender]);
         if (previewTouchInside) {
             // UIControlEventTouchDragExit
         } else {
             // UIControlEventTouchDragOutside
         }
     } else {
         BOOL previewTouchOutside = !CGRectContainsPoint(outerBounds, [touch previousLocationInView:sender]);
         if (previewTouchOutside) {
             // UIControlEventTouchDragEnter
         } else {
             // UIControlEventTouchDragInside
         }
     }    
 }

注意看,這裡我們僅僅通過注冊兩個事件,卻達到了相當於四個事件的效果。最後的效果如下,這裡依然是設置了boundsExtension為25,當然你可以設置成任意你想要的值。

2.gif

處理TouchUp事件

在本文開頭我們提到過,所有需要判斷是否在button內部的事件都有這個問題,如UIControlEventTouchUpInside和UIControlEventTouchUpOutside,當然也可以使用同樣的辦法來處理:

先為兩個事件注冊同一個回調函數:

// to get the touch up event
[btn addTarget:self action:@selector(btnTouchUp:withEvent:) forControlEvents:UIControlEventTouchUpInside];
[btn addTarget:self action:@selector(btnTouchUp:withEvent:) forControlEvents:UIControlEventTouchUpOutside];

然後處理回調函數:

 - (void)btnTouchUp:(UIButton *)sender withEvent:(UIEvent *)event {
     UITouch *touch = [[event allTouches] anyObject];
     CGFloat boundsExtension = 25.0f;
     CGRect outerBounds = CGRectInset(sender.bounds, -1 * boundsExtension, -1 * boundsExtension);
     BOOL touchOutside = !CGRectContainsPoint(outerBounds, [touch locationInView:sender]);
     if (touchOutside) {
         // UIControlEventTouchUpOutside
     } else {
         // UIControlEventTouchUpInside
     }
 }

結尾

因為UIButton的addTarget:action:forControlEvents方法是繼承自UIControl,因此上面的辦法對於所有UIControl的子類都同樣適用,比如UISwitch,UISlider等等。

我也在StackOverflow原來的問題上作了補充。如果你有更好的辦法,或者知道為何蘋果如此處理,請給我留言或者在原問題上回答。

(全文完)

feihu

2015.05.21 於 Shenzhen

本文來自南栀傾寒(簡書)的投稿,翻譯自蘋果Swift博客,原文:Memory Safety: Ensuring Values are Defined Before Use

歡迎通過“投稿爆料”渠道或者support@cocoachina.com投稿

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved