最近在使用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最上端的像素差。
但是,當手指離開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,當然你可以設置成任意你想要的值。
處理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投稿