本文系轉(zhuǎn)載,原文地址為iOS觸摸事件全家桶
關(guān)于手勢(shì)識(shí)別器即 UIGestureRecognizer
本身的使用不是本文要所討論的內(nèi)容,按下不表。此處要探討的是:手勢(shì)識(shí)別器與UIResponder的聯(lián)系棍辕。
附:
UIGestureRecognizer
子類
離散型手勢(shì)
持續(xù)性手勢(shì)
- UIPinchGestureRecognizer
- UIRotationGestureRecognizer
- UIPanGestureRecognizer
- UIScreenEdgePanGestureRecognizer
- UILongPressGestureRecognizer
事實(shí)上,手勢(shì)分為離散型手勢(shì)(discrete gestures)和持續(xù)型手勢(shì)(continuous gesture)还绘。系統(tǒng)提供的離散型手勢(shì)包括點(diǎn)按手勢(shì)(UITapGestureRecognizer
)和輕掃手勢(shì)(UISwipeGestureRecognizer
)楚昭,其余均為持續(xù)型手勢(shì)。
兩者主要區(qū)別在于狀態(tài)變化過程:
離散型:
識(shí)別成功:Possible —> Recognized
識(shí)別失斉那辍:Possible —> Failed持續(xù)型:
完整識(shí)別:Possible —> Began —> [Changed] —> Ended
不完整識(shí)別:Possible —> Began —> [Changed] —> Cancel
離散型手勢(shì)
先拋開上面的場(chǎng)景抚太,看一個(gè)簡(jiǎn)單的demo。
控制器的視圖上add了一個(gè)View記為YellowView昔案,并綁定了一個(gè)單擊手勢(shì)識(shí)別器尿贫。
// LXFViewController
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(actionTap)];
[self.view addGestureRecognizer:tap];
}
- (void)actionTap{
NSLog(@"View Taped");
}
單擊YellowView,日志打印如下:
-[YellowView touchesBegan:withEvent:]
View Taped
-[YellowView touchesCancelled:withEvent:]
從日志上看出YellowView最后Cancel了對(duì)觸摸事件的響應(yīng)爱沟,而正常應(yīng)當(dāng)是觸摸結(jié)束后帅霜,YellowView的 touchesEnded:withEvent:
的方法被調(diào)用才對(duì)。另外呼伸,期間還執(zhí)行了手勢(shì)識(shí)別器綁定的action 身冀。我從官方文檔找到了這樣的解釋:
A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties.
大致理解是钝尸,Window在將事件傳遞給hit-tested view之前,會(huì)先將事件傳遞給相關(guān)的手勢(shì)識(shí)別器并由手勢(shì)識(shí)別器優(yōu)先識(shí)別搂根。若手勢(shì)識(shí)別器成功識(shí)別了事件,就會(huì)取消hit-tested view對(duì)事件的響應(yīng)剩愧;若手勢(shì)識(shí)別器沒能識(shí)別事件仁卷,hit-tested view才完全接手事件的響應(yīng)權(quán)穴翩。
一句話概括:手勢(shì)識(shí)別器比UIResponder具有更高的事件響應(yīng)優(yōu)先級(jí)!锦积!
按照這個(gè)解釋,Window在將事件傳遞給hit-tested view即YellowView之前背蟆,先傳遞給了控制器根視圖上的手勢(shì)識(shí)別器。手勢(shì)識(shí)別器成功識(shí)別了該事件哮幢,通知Application取消YellowView對(duì)事件的響應(yīng)带膀。
然而看日志,卻是YellowView的 touchesBegan:withEvent:
先調(diào)用了垛叨,既然手勢(shì)識(shí)別器先響應(yīng)柜某,不應(yīng)該上面的action先執(zhí)行嗎,這又怎么解釋还棱?事實(shí)上這個(gè)認(rèn)知是錯(cuò)誤的惭等。手勢(shì)識(shí)別器的action的調(diào)用時(shí)機(jī)(即此處的 actionTap
)并不是手勢(shì)識(shí)別器接收到事件的時(shí)機(jī),而是手勢(shì)識(shí)別器成功識(shí)別事件后的時(shí)機(jī)辞做,即手勢(shì)識(shí)別器的狀態(tài)變?yōu)?a href="apple-reference-documentation://hcCvvcGh5U" target="_blank" rel="nofollow">UIGestureRecognizerStateRecognized。因此從該日志中并不能看出事件是優(yōu)先傳遞給手勢(shì)識(shí)別器的稚补,那該怎么證明Window先將事件傳遞給了手勢(shì)識(shí)別器框喳?
要解決這個(gè)問題厦坛,只要知道手勢(shì)識(shí)別器是如何接收事件的乍惊,然后在接收事件的方法中打印日志對(duì)比調(diào)用時(shí)間先后即可润绎。說起來你可能不信,手勢(shì)識(shí)別器對(duì)于事件的響應(yīng)也是通過這4個(gè)熟悉的方法來實(shí)現(xiàn)的呢蛤。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
需要注意的是,雖然手勢(shì)識(shí)別器通過這幾個(gè)方法來響應(yīng)事件棍郎,但它并不是UIResponder的子類顾稀,相關(guān)的方法聲明在 UIGestureRecognizerSubclass.h
中坝撑。
這樣一來粮揉,我們便可以自定義一個(gè)單擊手勢(shì)識(shí)別器的類扶认,重寫這幾個(gè)方法來監(jiān)聽手勢(shì)識(shí)別器接收事件的時(shí)機(jī)。創(chuàng)建一個(gè)UITapGestureRecognizer的子類狱从,重寫響應(yīng)事件的方法叠纹,每個(gè)方法中調(diào)用父類的實(shí)現(xiàn),并替換demo中的手勢(shì)識(shí)別器与涡。另外需要在.m文件中引入 import <UIKit/UIGestureRecognizerSubclass.h>
持偏,因?yàn)橄嚓P(guān)方法聲明在該頭文件中。
// LXFTapGestureRecognizer (繼承自UITapGestureRecognizer)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesCancelled:touches withEvent:event];
}
現(xiàn)在酌畜,再次點(diǎn)擊YellowView桥胞,日志如下:
-[LXFTapGestureRecognizer touchesBegan:withEvent:]
-[YellowView touchesBegan:withEvent:]
-[LXFTapGestureRecognizer touchesEnded:withEvent:]
View Taped
-[YellowView touchesCancelled:withEvent:]
很明顯,確實(shí)是手勢(shì)識(shí)別器先接收到了事件井誉。之后手勢(shì)識(shí)別器成功識(shí)別了手勢(shì)整胃,執(zhí)行了action,再由Application取消了YellowView對(duì)事件的響應(yīng)在岂。
Window怎么知道要把事件傳遞給哪些手勢(shì)識(shí)別器蛮寂?
之前探討過Application怎么知道要把event傳遞給哪個(gè)Window,以及Window怎么知道要把event傳遞給哪個(gè)hit-tested view的問題及老,答案是這些信息都保存在event所綁定的touch對(duì)象上范抓。手勢(shì)識(shí)別器也是一樣的匕垫,event綁定的touch對(duì)象上維護(hù)了一個(gè)手勢(shì)識(shí)別器數(shù)組,里面的手勢(shì)識(shí)別器毫無疑問是在hit-testing的過程中收集的寞秃。打個(gè)斷點(diǎn)看一下touch上綁定的手勢(shì)識(shí)別器數(shù)組:
Window先將事件傳遞給這些手勢(shì)識(shí)別器偶惠,再傳給hit-tested view。一旦有手勢(shì)識(shí)別器成功識(shí)別了手勢(shì)堂淡,Application就會(huì)取消hit-tested view對(duì)事件的響應(yīng)扒腕。
持續(xù)型手勢(shì)
將上面Demo中視圖綁定的單擊手勢(shì)識(shí)別器用滑動(dòng)手勢(shì)識(shí)別器(UIPanGestureRecognizer)替換瘾腰。
- (void)viewDidLoad {
[super viewDidLoad];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(actionPan)];
[self.view addGestureRecognizer:pan];
}
- (void)actionPan{
NSLog(@"View panned");
}
在YellowView上執(zhí)行一次滑動(dòng):
日志打印如下:
-[YellowView touchesBegan:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesCancelled:withEvent:]
View panned
View panned
View panned
...
在一開始滑動(dòng)的過程中蹋盆,手勢(shì)識(shí)別器處在識(shí)別手勢(shì)階段硝全,滑動(dòng)產(chǎn)生的連續(xù)事件既會(huì)傳遞給手勢(shì)識(shí)別器又會(huì)傳遞給YellowView楞抡,因此YellowView的 touchesMoved:withEvent:
在開始一段時(shí)間內(nèi)會(huì)持續(xù)調(diào)用召廷;當(dāng)手勢(shì)識(shí)別器成功識(shí)別了該滑動(dòng)手勢(shì)時(shí),手勢(shì)識(shí)別器的action開始調(diào)用先紫,同時(shí)通知Application取消YellowView對(duì)事件的響應(yīng)筹煮。之后僅由滑動(dòng)手勢(shì)識(shí)別器接收事件并響應(yīng),YellowView不再接收事件本冲。
另外劫扒,在滑動(dòng)的過程中,若手勢(shì)識(shí)別器未能識(shí)別手勢(shì),則事件在觸摸滑動(dòng)過程中會(huì)一直傳遞給hit-tested view闷板,直到觸摸結(jié)束院塞。讀者可自行驗(yàn)證。
先總結(jié)一下手勢(shì)識(shí)別器與UIResponder對(duì)于事件響應(yīng)的聯(lián)系:
- 當(dāng)觸摸發(fā)生或者觸摸的狀態(tài)發(fā)生變化時(shí)县遣,Window都會(huì)傳遞事件尋求響應(yīng)汹族。
- Window先將綁定了觸摸對(duì)象的事件傳遞給觸摸對(duì)象上綁定的手勢(shì)識(shí)別器,再發(fā)送給觸摸對(duì)象對(duì)應(yīng)的hit-tested view夸政。
- 手勢(shì)識(shí)別器識(shí)別手勢(shì)期間榴徐,若觸摸對(duì)象的觸摸狀態(tài)發(fā)生變化,事件都是先發(fā)送給手勢(shì)識(shí)別器再發(fā)送給hit-test view耗帕。
- 手勢(shì)識(shí)別器若成功識(shí)別了手勢(shì)仿便,則通知Application取消hit-tested view對(duì)于事件的響應(yīng),并停止向hit-tested view發(fā)送事件探越;
- 若手勢(shì)識(shí)別器未能識(shí)別手勢(shì)钦幔,而此時(shí)觸摸并未結(jié)束,則停止向手勢(shì)識(shí)別器發(fā)送事件搀擂,僅向hit-test view發(fā)送事件卷玉。
- 若手勢(shì)識(shí)別器未能識(shí)別手勢(shì),且此時(shí)觸摸已經(jīng)結(jié)束威恼,則向hit-tested view發(fā)送end狀態(tài)的touch事件以停止對(duì)事件的響應(yīng)寝并。
手勢(shì)識(shí)別器的3個(gè)屬性
@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;
cancelsTouchesInView
默認(rèn)為YES
衬潦。表示當(dāng)手勢(shì)識(shí)別器成功識(shí)別了手勢(shì)之后,會(huì)通知Application取消響應(yīng)鏈對(duì)事件的響應(yīng)镀岛,并不再傳遞事件給hit-test view
漂羊。
若設(shè)置成NO
,表示手勢(shì)識(shí)別成功后不取消響應(yīng)鏈對(duì)事件的響應(yīng)稻据,事件依舊會(huì)傳遞給hit-test view
。
demo中設(shè)置:pan.cancelsTouchesInView = NO
滑動(dòng)時(shí)日志如下:
-[YellowView touchesBegan:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesMoved:withEvent:]
View panned
View panned
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesMoved:withEvent:]
即便滑動(dòng)手勢(shì)識(shí)別器識(shí)別了手勢(shì)匆赃,Application也會(huì)依舊發(fā)送事件給YellowView今缚。
delaysTouchesBegan
默認(rèn)為 NO
姓言。默認(rèn)情況下手勢(shì)識(shí)別器在識(shí)別手勢(shì)期間,當(dāng)觸摸狀態(tài)發(fā)生改變時(shí)囱淋,Application都會(huì)將事件傳遞給手勢(shì)識(shí)別器和hit-tested view
餐塘;
若設(shè)置成YES
,則表示手勢(shì)識(shí)別器在識(shí)別手勢(shì)期間税手,截?cái)嗍录枘桑床粫?huì)將事件發(fā)送給hit-tested view
。
設(shè)置pan.delaysTouchesBegan = YES
日志如下:
View panned
View panned
View panned
View panned
因?yàn)榛瑒?dòng)手勢(shì)識(shí)別器在識(shí)別期間兵扬,事件不會(huì)傳遞給YellowView口蝠,因此期間YellowView的 touchesBegan:withEvent:
和 touchesMoved:withEvent:
都不會(huì)被調(diào)用亚皂;而后滑動(dòng)手勢(shì)識(shí)別器成功識(shí)別了手勢(shì)国瓮,也就獨(dú)吞了事件,不會(huì)再傳遞給YellowView禁漓。因此只打印了手勢(shì)識(shí)別器成功識(shí)別手勢(shì)后的action調(diào)用孵睬。
delaysTouchesEnded
默認(rèn)為YES
。當(dāng)手勢(shì)識(shí)別失敗時(shí)秘狞,若此時(shí)觸摸已經(jīng)結(jié)束烁试,會(huì)延遲
一小段時(shí)間(0.15s)再調(diào)用響應(yīng)者的 touchesEnded:withEvent:;
若設(shè)置成NO
靖诗,則在手勢(shì)識(shí)別失敗時(shí)會(huì)立即
通知Application發(fā)送狀態(tài)為end的touch事件給hit-tested view以調(diào)用 touchesEnded:withEvent: 結(jié)束事件響應(yīng)支示。
總結(jié):手勢(shì)識(shí)別器比響應(yīng)鏈具有更高的事件響應(yīng)優(yōu)先級(jí)。