本文主要想講的是觸摸事件和手勢(shì)混合使用的一個(gè)問(wèn)題畜号,但作為知識(shí)儲(chǔ)備帚呼,還是把兩者再單獨(dú)介紹一下质涛。兩者的基本知識(shí)點(diǎn)都是iOS開(kāi)發(fā)文檔或者參考其他博客的,算是一個(gè)總結(jié)箱蝠,文章最后會(huì)標(biāo)出參考鏈接续捂。
iOS的事件有Touch Events
、Motion Events
宦搬、Remote Events
牙瓢,最常見(jiàn)的是觸摸事件Touch Events
。觸摸事件除了是view來(lái)處理间校,還有高級(jí)的手勢(shì)可以處理矾克。所以,本文分別來(lái)講講觸摸事件和手勢(shì)憔足,并結(jié)合例子講講兩者混合使用的問(wèn)題胁附。
由于本文講的東西有點(diǎn)繁雜,特在此列一個(gè)目錄四瘫,方便大家有一個(gè)清晰的第一印象汉嗽。
- UITouch對(duì)象
- UIEvent對(duì)象
- 響應(yīng)鏈
- Hit-Testing
- 手勢(shì)識(shí)別
- 手勢(shì)識(shí)別與事件響應(yīng)混用
UITouch對(duì)象
一個(gè)手指第一次點(diǎn)擊屏幕,就會(huì)生成一個(gè)UITouch
對(duì)象找蜜,到手指離開(kāi)時(shí)銷毀。當(dāng)我們有多個(gè)手指觸摸屏幕時(shí)稳析,會(huì)生成多個(gè)UITouch
對(duì)象洗做。UITouch
對(duì)象可以表明觸摸的位置、狀態(tài)彰居,其狀態(tài)的類型有:
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
UIEvent對(duì)象
一個(gè)UIEvent
對(duì)象代表iOS的一個(gè)事件诚纸。一個(gè)觸摸事件定義為第一個(gè)手指開(kāi)始觸摸屏幕到最后一個(gè)手指離開(kāi)屏幕。所以陈惰,一個(gè)UIEvent
對(duì)象實(shí)際上對(duì)應(yīng)多個(gè)UITouch
對(duì)象畦徘。
響應(yīng)鏈
響應(yīng)鏈可以理解為一種虛擬的鏈表,每一個(gè)節(jié)點(diǎn)是一個(gè)UIResponder
對(duì)象抬闯。UIResponder
是事件接收與處理的基類井辆,UIApplication
、UIViewController
和UIView
都繼承自UIResponder
溶握。UIResponder
提供了幾個(gè)事件處理的方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
UIResponder
對(duì)象之間的聯(lián)系靠nextResponder
指針杯缺。
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif
一個(gè)觸摸事件到達(dá)真正處理它的對(duì)象時(shí)經(jīng)過(guò)了一個(gè)搜索路徑,這就是響應(yīng)鏈的一部分睡榆。事件沿著這個(gè)響應(yīng)鏈一直傳遞萍肆,直到碰到可以處理這個(gè)事件的UIResponder
對(duì)象或者到達(dá)響應(yīng)鏈的末尾(UIApplication)袍榆。
響應(yīng)鏈的構(gòu)造規(guī)則如下:
- 程序啟動(dòng)時(shí),UIApplication會(huì)生成一個(gè)單例塘揣,并會(huì)關(guān)聯(lián)一個(gè)APPDelegate包雀。APPDelegate作為整個(gè)響應(yīng)鏈的根建立起來(lái),UIApplication的nextResponser為APPDelegate亲铡。
- 程序啟動(dòng)后馏艾,任何的UIWindow被創(chuàng)建時(shí),UIWindow內(nèi)部都會(huì)把nextResponser設(shè)置為UIApplication單例奴愉。
- UIWindow初始化rootViewController, rootViewController的nextResponser會(huì)設(shè)置為UIWindow琅摩。
- UIViewController初始化View,View的nextResponser會(huì)設(shè)置為rootViewController锭硼。
- AddSubView時(shí)房资,如果subView不是ViewController的View,那么subView的nextResponser會(huì)被設(shè)置為superView。否則就是 subView -> subView.VC ->superView檀头。
有了這個(gè)響應(yīng)鏈轰异,事件就可以按照這個(gè)路徑逐級(jí)傳遞了。當(dāng)前的對(duì)象不能處理這個(gè)事件暑始,就交給nextResponser搭独,一直到UIApplication單例。如果仍然不能處理廊镜,就丟棄牙肝。
實(shí)際應(yīng)用中,一個(gè)事件都是由一個(gè)響應(yīng)對(duì)象來(lái)處理嗤朴,不會(huì)繼續(xù)向下傳遞配椭,不過(guò)有了上面的知識(shí),我們是可以手動(dòng)傳遞的雹姊,做我們想做的事情股缸。
Hit-Testing
當(dāng)我們觸摸屏幕時(shí),到底應(yīng)該由哪個(gè)對(duì)象最先響應(yīng)這個(gè)事件呢吱雏?這就需要去探測(cè)敦姻,這個(gè)過(guò)程稱為Hit-Testing
,最后的結(jié)果稱為hit-test view
歧杏。涉及到兩個(gè)方法是:
//先判斷點(diǎn)是否在View內(nèi)部镰惦,然后遍歷subViews
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
//判斷點(diǎn)是否在這個(gè)View內(nèi)部
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
Hit-Testing
是一個(gè)遞歸的過(guò)程,每一步監(jiān)測(cè)觸摸位置是否在當(dāng)前view中得滤,如果是陨献,就遞歸監(jiān)測(cè)subviews;否則懂更,返回nil眨业。
遞歸的根節(jié)點(diǎn)是UIWindow急膀,對(duì)subviews的遍歷順序按照 后添加的先遍歷 原則。大致過(guò)程的代碼如下:
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
//判斷是否合格
if (!self.hidden && self.alpha > 0.01 && self.isUserInteractionEnabled) {
//判斷點(diǎn)擊位置是否在自己區(qū)域內(nèi)
if ([self pointInside: point withEvent:event]) {
UIView *attachedView;
for (int i = self.subviews.count - 1; i >= 0; i--) {
UIView *view = self.subviews[i];
//對(duì)子view進(jìn)行hitTest
attachedView = [view hitTest:point withEvent:event];
if (attachedView)
break;
}
if (attachedView) {
return attachedView;
} else {
return self;
}
}
}
return nil;
}
來(lái)看最經(jīng)典的示例圖:
當(dāng)點(diǎn)擊E時(shí)龄捡,探測(cè)的步驟是:
1 觸摸點(diǎn)在A的范圍內(nèi)卓嫂,所以繼續(xù)探測(cè)A的subView,既view B和view C聘殖。
2 觸摸點(diǎn)不在view B里晨雳,在view C里,所以繼續(xù)探測(cè)C的subView D 和 E奸腺。
3 觸摸點(diǎn)不在D里餐禁,在E里,E已經(jīng)是最低級(jí)的View突照,故返回E帮非。
綜上,我們可以看出有兩個(gè)幾乎相反的鏈讹蘑,一是響應(yīng)鏈末盔,一是探測(cè)鏈。 有觸摸事件時(shí)座慰,先依賴探測(cè)鏈來(lái)確定響應(yīng)鏈的開(kāi)始節(jié)點(diǎn)(UIResponder對(duì)象)陨舱,然后依賴響應(yīng)鏈來(lái)確定最終處理事件的對(duì)象。
手勢(shì)識(shí)別
手勢(shì)是Apple提供的更高級(jí)的事件處理技術(shù)版仔,可以完成更多更復(fù)雜的觸摸事件游盲,比如旋轉(zhuǎn)、滑動(dòng)邦尊、長(zhǎng)按等背桐。基類是UIGestureRecognizer
蝉揍,派生的類有:
Gesture | UIKit Class |
---|---|
Tapping (any number of taps) | UITapGestureRecognizer |
Pinching in and out (for zooming a view) | UIPinchGestureRecognizer |
Panning or dragging | UIPanGestureRecognizer |
Swiping (in any direction) | UISwipeGestureRecognizer |
Rotating (fingers moving in opposite directions) | UIRotationGestureRecognizer |
Long press (also known as “touch and hold”) | UILongPressGestureRecognizer |
手勢(shì)綁定到一個(gè)View上,一個(gè)View上可以綁定多個(gè)手勢(shì)畦娄。
UIGestureRecognizer
同UIResponder
一樣也有四個(gè)方法
//UIGestureRecognizer
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
手勢(shì)會(huì)在以上四個(gè)方法中去對(duì)手勢(shì)的State做更改又沾,手勢(shì)的State表明當(dāng)前手勢(shì)是識(shí)別還是失敗等等。比如單擊手勢(shì)會(huì)在touchesBegan
時(shí)記錄點(diǎn)擊位置熙卡,然后在touchesEnded
判斷點(diǎn)擊次數(shù)杖刷、時(shí)間、是否移動(dòng)過(guò)驳癌,最后得出否識(shí)別該手勢(shì)滑燃。這幾個(gè)方法一般在自定義手勢(shì)里面使用。
手勢(shì)的State有:
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible, // the recognizer has not yet recognized its gesture, but may be evaluating touch events. this is the default state
UIGestureRecognizerStateBegan, // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop
UIGestureRecognizerStateChanged, // the recognizer has received touches recognized as a change to the gesture. the action method will be called at the next turn of the run loop
UIGestureRecognizerStateEnded, // the recognizer has received touches recognized as the end of the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
UIGestureRecognizerStateCancelled, // the recognizer has received touches resulting in the cancellation of the gesture. the action method will be called at the next turn of the run loop. the recognizer will be reset to UIGestureRecognizerStatePossible
UIGestureRecognizerStateFailed, // the recognizer has received a touch sequence that can not be recognized as the gesture. the action method will not be called and the recognizer will be reset to UIGestureRecognizerStatePossible
// Discrete Gestures – gesture recognizers that recognize a discrete event but do not report changes (for example, a tap) do not transition through the Began and Changed states and can not fail or be cancelled
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
};
手勢(shì)在這些狀態(tài)之間變化颓鲜,形成了一個(gè)有限狀態(tài)機(jī):
左側(cè)是非連續(xù)手勢(shì)(比如單擊)的狀態(tài)機(jī)表窘,右側(cè)是連續(xù)手勢(shì)(比如滑動(dòng))的狀態(tài)機(jī)典予。所有的手勢(shì)的開(kāi)始狀態(tài)都是UIGestureRecognizerStatePossible。
非連續(xù)的手勢(shì)要么識(shí)別成功(UIGestureRecognizerStateRecognized)乐严,要么識(shí)別失敗(UIGestureRecognizerStateFailed)瘤袖。
連續(xù)的手勢(shì)識(shí)別到第一個(gè)手勢(shì)時(shí),變成UIGestureRecognizerStateBegan昂验,然后變成UIGestureRecognizerStateChanged捂敌,并且不斷地在這個(gè)狀態(tài)下循環(huán),當(dāng)用戶最后一個(gè)手指離開(kāi)view時(shí)既琴,變成UIGestureRecognizerStateEnded占婉,當(dāng)然如果手勢(shì)不再符合它的模式的時(shí)候,狀態(tài)也可能變成UIGestureRecognizerStateCancelled甫恩。
手勢(shì)識(shí)別與事件響應(yīng)混用
重點(diǎn)來(lái)了逆济!前面我們知道觸摸事件可以通過(guò)響應(yīng)鏈來(lái)傳遞與處理,也可以被綁定在view上的手勢(shì)識(shí)別和處理填物。那么這兩個(gè)一起用會(huì)出現(xiàn)什么問(wèn)題纹腌?我們來(lái)看一個(gè)簡(jiǎn)單的例子。
圖中baseView 有兩個(gè)subView滞磺,分別是testView和testBtn升薯。我們?cè)赽aseView和testView都重載touchsBegan:withEvent
、touchsEnded:withEvent
击困、
touchsMoved:withEvent
涎劈、touchsCancelled:withEvent
方法,并且在baseView上添加單擊手勢(shì)阅茶,action名為tapAction
蛛枚,給testBtn綁定action名為testBtnClicked
。
主要代碼如下:
//baseView
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)];
[self.view addGestureRecognizer:tap];
...
[_testBtn addTarget:self action:@selector(testBtnClicked) forControlEvents:UIControlEventTouchUpInside];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Began");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Moved");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Ended");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Cancelled");
}
- (void)tapAction {
NSLog(@"=========> single Tapped");
}
- (void)testBtnClicked {
NSLog(@"=========> click testbtn");
}
//test view
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Began");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Moved");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Ended");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Cancelled");
}
情景A :?jiǎn)螕鬮aseView脸哀,輸出結(jié)果為:
=========> base view touchs Began
=========> single Tapped
=========> base view touchs Cancelled
情景B :?jiǎn)螕魌estView蹦浦,輸出結(jié)果為:
=========> test view touchs Began
=========> single Tapped
=========> test view touchs Cancelled
情景C :?jiǎn)螕魌estBtn, 輸出結(jié)果為:
=========> click testbtn
情景D :按住testView,過(guò)5秒后或更久釋放撞蜂,輸出結(jié)果為:
=========> test view touchs Began
=========> test view touchs Ended
何解懊は狻?
原來(lái)蝌诡,手勢(shì)有更高的優(yōu)先級(jí)來(lái)識(shí)別一個(gè)觸摸事件溉贿。
Gesture Recognizers Get the First Opportunity to Recognize a Touch
A window delays the delivery of touch objects to the view so that the gesture recognizer can analyze the touch first. During the delay, if the gesture recognizer recognizes a touch gesture, then the window never delivers the touch object to the view, and also cancels any touch objects it previously sent to the view that were part of that recognized sequence.
既觸摸事件首先傳遞到手勢(shì)上,如果手勢(shì)識(shí)別成功浦旱,就會(huì)取消事件的繼續(xù)傳遞宇色,否則,事件還是會(huì)被響應(yīng)鏈處理。具體地宣蠕,系統(tǒng)維持了與響應(yīng)鏈關(guān)聯(lián)的所有手勢(shì)例隆,事件首先發(fā)給這些手勢(shì),然后再發(fā)給響應(yīng)鏈植影。
這就可以解釋了情景A和B了裳擎,首先我們的單擊事件傳遞到了tap手勢(shì)上了,不過(guò)這個(gè)是手勢(shì)識(shí)別需要一點(diǎn)時(shí)間思币,手勢(shì)還在Possible狀態(tài)的時(shí)候事件傳遞到了響應(yīng)鏈的第一個(gè)響應(yīng)對(duì)象(baseView或者testView)鹿响,于是就回調(diào)用它們的touchsBegan:withEvent:
方法,然后手勢(shì)識(shí)別成功谷饿,就會(huì)去cancel之前傳遞到的所有響應(yīng)對(duì)象惶我,于是就會(huì)調(diào)用它們的touchsCancelled:withEvent:
方法。
按照這個(gè)道理博投,情景C也應(yīng)該是這樣才對(duì)俺窆薄!這就涉及到一個(gè)響應(yīng)級(jí)別的問(wèn)題了毅哗,iOS開(kāi)發(fā)文檔里說(shuō)到
In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer. This applies only to gesture recognition that overlaps the default action for a control, which includes:
- A single finger single tap on a UIButton, UISwitch, UISegmentedControl, UIStepper,and UIPageControl.
- A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.
- A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.
所以testBtn的action會(huì)覆蓋手勢(shì)听怕。
在情景D中,由于長(zhǎng)按住testView不釋放虑绵,tap手勢(shì)就會(huì)識(shí)別失敗尿瞭,然后就可以繼續(xù)正常傳遞給testView處理。
假如實(shí)際應(yīng)用中我們?cè)趖estview中有UITableView,上面的方式就不能點(diǎn)擊UITableViewCell了翅睛,除非長(zhǎng)按cell再釋放声搁。 要解決這個(gè)問(wèn)題,需要了解UIGestureRecognizer的cancelsTouchsInView
屬性:
//default is YES. causes touchesCancelled:withEvent: or pressesCancelled:withEvent: to
//be sent to the view for all touches or presses recognized as part of this gesture immediately
//before the action method is called.
@property(nonatomic) BOOL cancelsTouchesInView;
既在手勢(shì)識(shí)別成功之后,是否取消傳遞給響應(yīng)鏈的觸摸事件捕发。
我們?cè)O(shè)置tap的cancelsTouchsInView為NO疏旨,那么輸出結(jié)果就變成
//A
=========> base view touchs Began
=========> single Tapped
=========> base view touchs Ended
//B
=========> test view touchs Began
=========> single Tapped
=========> test view touchs Ended
//C
=========> single Tapped
=========> click testbtn
//D
=========> test view touchs Began
=========> test view touchs Ended
此時(shí),baseView 和testView上的觸摸事件就可以完整執(zhí)行扎酷。
順便再提一下delaysTouchesBegan
屬性:
// default is NO. causes all touch or press events to be delivered to
//the target view only after this gesture has failed recognition. set to
//YES to prevent views from processing any touches or presses that
//may be recognized as part of this gesture
@property(nonatomic) BOOL delaysTouchesBegan;
既延遲傳遞事件給view檐涝,我們?cè)O(shè)置tap的delaysTouchesBegan為YES,那么輸出結(jié)果就變成
//A
=========> single Tapped
//B
=========> single Tapped
//C
=========> click testbtn
//D
=========> test view touchs Began
=========> test view touchs Ended
所以事件先被手勢(shì)識(shí)別了法挨,就不再傳遞給響應(yīng)鏈了骤铃。