跳出手掌心--如何立即觸發UIButton邊界事件


最近在使用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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章