iOS中有三類事件:UIEventTypeTouches觸摸事件碴卧、 UIEventTypeMotion “動作”事件,比如搖晃手機設備访敌、UIEventTypeRemoteControl遠程控制事件献雅。還有一種在iOS9.0之后出現的UIEventTypePresses事件玫鸟,和觸按物理按鈕有關缓熟。
三大類事件分別有一些子事件:
響應者對象:不過在ios中不是任何對象都可以處理事件梢卸,只有繼承了UIResponder的對象才能接收走诞、處理事件,比如UIApplication蛤高、UIViewController蚣旱、UIView碑幅、UIWindow。
觸摸事件
UIView是UIResponder的子類塞绿。UIResponder有以下四個方法處理觸摸事件沟涨,UIView可以重寫這些方法去自定義事件處理。
一根或者多根手指開始觸摸view(手指按下)
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
一根或者多根手指在view上移動(隨著手指的移動异吻,會持續(xù)調用該方法)
-(void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
一根或者多根手指離開view(手指抬起)
-(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
某個系統(tǒng)事件(例如電話呼入)打斷觸摸過程
-(void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event
對于這四個觸摸事件處理方法的參數的說明:
- 第一個參數:(NSSet)touches*
NSSet和 NSArray類似
但NSSet的區(qū)別在:
1.無序不重復(哈希)裹赴。與添加順序也沒有關系,也不能通過序號來取出某個元素诀浪;即使多次重復添加相同的元素棋返,儲存的都只有一個。
2.通過 anyObject方法來隨機訪問單個元素雷猪。
3.如果要訪問NSSet 中的每個元素懊昨,通過for in循環(huán)遍歷。
4.好處: 效率高春宣。比如重用 Cell 的時候, 從緩存池中隨便獲取一個就可以了, 無需按照指定順序來獲取嫉你; 當需要把數據存放到一個集合中, 然后判斷集合中是否有某個對象的時候
touches參數中存放的都是UITouch對象月帝。
UITouch
當用一根手指觸摸屏幕時,會創(chuàng)建一個與手指相關聯的UITouch對象幽污。如果兩根手指同時觸摸屏幕嚷辅,則會調用一次touchesBegan方法,創(chuàng)建兩個UITouch對象(如果不是同時觸摸距误,調用兩次方法簸搞,每次的touches參數都只有一個UITouch對象)。
判斷是否多點觸摸:NSSet有多少個UITouch對象元素准潭。
UITouch保存著跟本次手指觸摸相關的信息趁俊,比如觸摸的位置、時間刑然。當手指移動時寺擂,系統(tǒng)會更新同一個UITouch對象,使之能夠一直保存該手指的觸摸位置泼掠。當手指離開屏幕時怔软,系統(tǒng)會銷毀相應的UITouch對象。
比如择镇,判斷單擊挡逼、雙擊或者多擊:tapCount屬性
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch * touch = touches.anyObject;//獲取觸摸對象
NSLog(@"%@",@(touch.tapCount));//短時間內的點擊次數
}
UITouch常用方法:
-(CGPoint)locationInView:(UIView*)view;
返回觸摸在參數view上的位置,該位置基于view的坐標系(以view的左上角為原點(0, 0))腻豌;如果調用時傳入的view參數為nil的話家坎,返回的是觸摸點在UIWindow的位置
-(CGPoint)previousLocationInView:(UIView*)view;
前一個觸摸點的位置,參數同上
-
第二個參數(UIEvent)event*
每產生一個事件嘱能,就會產生一個UIEvent對象,UIEvent保存事件產生的事件和類型乘盖。UIEvent還提供了相應的方法可以獲得在某個view上面的UITouch觸摸對象焰檩。
一次完整的觸摸過程中,只會產生一個事件對象订框,4個觸摸方法都是同一個event參數.
UIView無法與用戶交互的情況
- userInteractionEnabled= NO 如果父視圖不能與用戶交互, 那么所有子控件也不能與用戶交互
- hidden= YES
- alpha= 0.0 ~ 0.01
- 子視圖的位置超出了父視圖的有效范圍, 那么子視圖超出部分無法與用戶交互的
- UIImageView的userInteractionEnabled默認是NO析苫,因此UIImageView以及它的子控件默認是不能接收觸摸事件的
事件的傳遞&響應
事件傳遞中UIWindow會根據不同的事件類型(3種),用不同的方式尋找initial object穿扳。比如Touch Event衩侥,UIWindow會首先試著把事件傳遞給事件發(fā)生的那個view,就是下文要說的hit-testview矛物。對于Motion和Remote Event茫死,UIWindow會把例如震動或者遠程控制的事件傳遞給當前的firstResponder
尋找響應者Hit-Test&Hit-Test View
Hit-Test的目的就是找到手指點擊到的最外層的那個view。它進行類似于探測的工作履羞,判斷是否點擊在某個視圖上峦萎。
Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.
-
什么時候Hit-Test
與Hit-Test 相關有兩個方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
runloop
發(fā)生觸摸事件后,系統(tǒng)會將該事件加入到一個由UIApplication管理的事件隊列中忆首;UIApplication會從事件隊列中取出最前面的事件并將其分發(fā)處理爱榔,通常,先發(fā)送事件給應用程序的主窗口UIWindow糙及。UIWindow會調hitTest:withEvent:
方法详幽,(從后往前遍歷subviews數組)找到點擊的點在哪個subview,然后繼續(xù)調用subView的hitTest:withEvent:方法浸锨,直到在視圖繼承樹中找到一個最合適的子視圖來處理觸摸事件唇聘,該子視圖即為hit-test view。
這個view和它上面依附的手勢柱搜,都會和一個UITouch的對象關聯起來迟郎,這個UITouch會作為事件傳遞的參數之一。我們可以看到UITouch.h里有一個view和gestureRecognizers的屬性聪蘸,就是Hit-Test view和它的手勢谎亩。
- ** hitTest:withEvent:如何找到最合適的控件來處理事件**
1.判斷自己是否能接收觸摸事件(能否與用戶交互)
2.觸摸點是否在自己身上? 調用pointInside:withEvent:
3.從后往前遍歷子控件數組宇姚,重復前面的兩個步驟 (從后往前:按照addsubview的順序匈庭,越晚添加的越先訪問)
4.如果沒有符合條件的子控件,那么就自己最適合處理
找到合適的視圖控件后浑劳,就會調用視圖控件的touches方法來作具體的事件處理阱持。
要攔截事件傳遞,可以使用pointInside:withEvent:
方法魔熏,在實現里面直接return NO;
即可衷咽,那么hitTest:withEvent:方法返回nil鸽扁。又或者在hitTest:withEvent:
直接return self;不傳遞給子視圖。
摘自網絡:hitTest:方法內部的參考實現
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@----hitTest:", [self class]);
// 如果控件不允許與用戶交互那么返回 nil
if (self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES) {
return nil;
}
// 如果這個點不在當前控件中那么返回 nil
if (![self pointInside:point withEvent:event]) {
return nil;
}
// 從后向前遍歷每一個子控件
for (int i = (int)self.subviews.count - 1; i >= 0; i--) {
// 獲取一個子控件
UIView *lastVw = self.subviews[i];
// 把當前觸摸點坐標轉換為相對于子控件的觸摸點坐標
CGPoint subPoint = [self convertPoint:point toView:lastVw];
// 判斷是否在子控件中找到了更合適的子控件
UIView *nextVw = [lastVw hitTest:subPoint withEvent:event];
// 如果找到了返回
if (nextVw) {
return nextVw;
}
}
// 如果以上都沒有執(zhí)行 return, 那么返回自己(表示子控件中沒有"更合適"的了)
return self;
}
要擴大view的點擊區(qū)域镶骗,比如要擴大按鈕的點擊區(qū)域(按鈕四周之外的10pt也可以響應按鈕的事件)桶现,可以怎么做呢?或許重寫hittest:withEvent:是個好辦法鼎姊,hitest就是返回可以響應事件的view骡和,在button的子類里面重寫它,判斷如果point在button的frame之外的10pt內相寇,就返回button自己慰于。
事件響應
什么是第一響應者?簡單的講唤衫,第一響應者是一個UIWindow對象接收到一個事件后婆赠,第一個來響應的該事件的對象。
如果hit-test視圖不處理收到的事件消息佳励,UIKit則將事件轉發(fā)到響應者鏈中的下一個響應者休里,看其是否能對該消息進行處理。
響應鏈:
所有的視圖按照樹狀層次結構組織赃承,每個view都有自己的superView份帐,包括vc的self.view:
1.當一個view被添加到superView上的時候,它的nextResponder就會被指向它的superView楣导;
2.當vc被初始化的時候,self.view(topmost view)的nextResponder會被指向所在的controller畜挨;
(概括前兩者就是:如果當前這個view是控制器的self.view,那么控制器就是上一個響應者 如果當前這個view不是控制器的view,那么父控件就是上一個響應者)
3.vc的nextResponder會被指向self.view的superView筒繁。
4.最頂級的vc的nextResponder指向UIWindow。
5.UIWindow的nextResponder指向UIApplication
這就形成了響應鏈巴元。并沒有一個對象來專門存儲這樣的一條鏈毡咏,而是通過UIResponder的串連起來的。
對于touches方法的描述:
The default implementation of this method does nothing. However immediate UIKit subclasses of UIResponder, particularly UIView, forward the message up the responder chain. To forward the message to the next responder, send the message to super (the superclass implementation); do not send the message directly to the next responder. For example,
[super touchesBegan:touches withEvent:event];
If you override this method without calling super (a common use pattern), you must also override the other methods for handling touch events, if only as stub (empty) implementations.
touches方法實際上什么事都沒做逮刨,UIView繼承了它進行重寫呕缭,就是把事件傳遞給nextResponder,相當于[self.nextResponder touchesBegan:touches withEvent:event]
修己。所以當一個view沒有重寫touch事件恢总,那么這個事件就會一直傳遞下去,直到UIApplication睬愤。如果重寫了touch方法片仿,這個view響應了事件之后,事件就被攔截了尤辱,它的nextResponder不會收到這個事件砂豌。這個時候如果想事件繼續(xù)傳遞下去厢岂,可以調用[super touchesBegan:touches withEvent:event]
,不建議直接調[self.nextResponder touchesBegan:touches withEvent:event]
阳距。
調用[super touches...]
(實際運行打斷點查看:之后父類響應touches塔粒,一直傳遞下去,最后UIResponse來響應touches筐摘,然后再由下一個響應者響應touches卒茬;前提是它們都重寫了touches方法,以及調用[super touches...]
)
附上一個響應鏈傳送門
不過UIScrollview的touches響應又是另一回事蓄拣。
響應鏈事件傳遞(向上傳遞):
1.如果view的控制器存在扬虚,就傳遞給控制器;如果控制器不存在球恤,則將其傳遞給它的父視圖
2.在視圖層次結構的最頂級視圖辜昵,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對象進行處理
3.如果window對象也不處理咽斧,則其將事件或消息傳遞給UIApplication對象
4.如果UIApplication也不能處理該事件或消息堪置,則將其丟棄
總結:
監(jiān)聽事件的基本流程:
1> 當應用程序啟動以后創(chuàng)建 UIApplication 對象
2> 然后啟動“消息循環(huán)”監(jiān)聽所有的事件
3> 當用戶觸摸屏幕的時候, "消息循環(huán)"監(jiān)聽到這個觸摸事件
4> "消息循環(huán)" 首先把監(jiān)聽到的觸摸事件傳遞了 UIApplication 對象
5> UIApplication 對象再傳遞給 UIWindow 對象
6> UIWindow 對象再傳遞給 UIWindow 的根控制器(rootViewController)
7> 控制器再傳遞給控制器所管理的 view
8> 控制器所管理的 View 在其內部搜索看本次觸摸的點在哪個控件的范圍內
9> 找到某個控件以后(調用這個控件的 touchesXxx 方法), 再一次向上返回, 最終返回給"消息循環(huán)"
10> "消息循環(huán)"知道哪個按鈕被點擊后, 在搜索這個按鈕是否注冊了對應的事件, 如果注冊了, 那么就調用這個"事件處理"程序。(一般就是執(zhí)行控制器中的"事件處理"方法)
手勢
手勢識別和觸摸事件是兩個獨立的事张惹,不要混淆舀锨。
通過touches方法監(jiān)聽view觸摸事件,有很明顯的幾個缺點:必須得自定義view宛逗、由于是在view內部的touches方法中監(jiān)聽觸摸事件坎匿,因此默認情況下,無法讓其他外界對象監(jiān)聽view的觸摸事件雷激、不容易區(qū)分用戶的具體手勢行為替蔬。
iOS3.2之后, 把觸摸事件做了封裝, 對常用的手勢進行了處理, 封裝了6種常見的手勢
UITapGestureRecognizer(敲擊)
UILongPressGestureRecognizer(長按)
UISwipeGestureRecognizer(輕掃)
UIRotationGestureRecognizer(旋轉)
UIPinchGestureRecognizer(捏合,用于縮放)
UIPanGestureRecognizer(拖拽)
下面談幾個在項目中遇到的問題:
關于手勢和touch的相互影響
tap的cancelsTouchesInView方法
“A Boolean value affecting whether touches are delivered to a view when a gesture is recognized.”也就是說屎暇,可以通過設置這個布爾值承桥,來設置手勢被識別時觸摸事件是否被傳送到視圖。
當值為YES(默認值)的時候根悼,系統(tǒng)會識別手勢凶异,并取消觸摸事件;為NO的時候挤巡,手勢識別之后剩彬,系統(tǒng)將觸發(fā)觸摸事件。
- 把手勢添加到btn上
- (void)viewDidLoad {
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 80, 80)];
button.backgroundColor = [UIColor redColor];
[self.view addSubview:button];
[button addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapAction:)];
tap.cancelsTouchesInView = NO;
[button addGestureRecognizer:tap];
}
- (void)tapAction:(UITapGestureRecognizer *)sender {
NSLog(@"tapAction");
}
- (void)btnAction:(UIButton *)btn {
NSLog(@"btnAction");
}
當cancelsTouchesInView為NO的時候矿卑,點擊按鈕襟衰,會先后觸發(fā)“tapAction:”和“btnAction:”方法;而當cancelsTouchesInView為YES的時候,只會觸發(fā)“tapAction:”方法瀑晒。
把手勢添加到btn的父view上即
[self.view addGestureRecognizer:tap];
cancelsTouchesInView=NO绍坝,點擊按鈕,會先后觸發(fā)“tapAction:”和“btnAction:”方法苔悦;cancelsTouchesInView=YES轩褐,只會觸發(fā)按鈕方法不會觸發(fā)手勢。但如果不是btn而是別的控件玖详,把手勢添加到控件的父view上
項目中用到的是collectionView把介,cancelsTouchesInView=NO,點擊collectionViewCell蟋座,先后觸發(fā)手勢和Cell拗踢,cancelsTouchesInView=YES只會觸發(fā)手勢。
對于UIButton,UISlider等繼承自UIControl的控件向臀,都會先響應觸摸事件巢墅,從而阻止手勢事件。手勢可以理解為是“特殊的層”券膀。對于TableView君纫,CollectionView這種弱點擊事件,系統(tǒng)優(yōu)先響應手勢芹彬,如果要響應Cell點擊事件就要實現代理方法
實現手勢的代理方法對手勢進行攔截蓄髓。
called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
判斷,手勢的觸擊方法是否在控件區(qū)域舒帮,如果是会喝,則返回NO,禁用手勢玩郊。否則返回YES.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
// NSLog(@"%d",[touch.view isKindOfClass:[UIButton class]]);
if ([touch.view.superview isKindOfClass:[UICollectionViewCell class]]) {//如果點擊的是UICollectionViewCell肢执,touch.view是collectionViewCell的contentView,contentView的父view才是collectionCell
return NO;
}else if ([touch.view isKindOfClass:[UIButton class]]) {
return NO;
}
return YES;
}
其他:
項目上沒遇到且目前還沒有深入了解瓦宜,先po鏈接方便以后查:
丟一個傳送門講Gesture Recognizers與事件分發(fā)路徑的關系:
http://blog.csdn.net/chun799/article/details/8194893
手勢的3個混淆屬性 cancelsTouchesInView/delaysTouchesBegan/delaysTouchesEnded: http://www.mamicode.com/info-detail-868542.html
補充
對于UIControl類型的控件,一個給定的事件岭妖,UIControl會調用- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event
來將action message轉發(fā)到UIApplication對象临庇,再由UIApplication對象調用其sendAction:to:fromSender:forEvent:
方法來將消息分發(fā)到指定的target上,如果沒有指定target(即nil)昵慌,則會將事件分發(fā)到響應鏈上第一個想處理消息的對象上假夺。而如果UIControl子類想監(jiān)控或修改這種行為的話,則可以重寫```sendAction: to: forEvent:``斋攀。
將外部添加的Target-Action放在控件內部來處理事件,實現如下:
// Btn.m
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
// 將事件傳遞到對象本身來處理
[super sendAction:@selector(handleAction:) to:self forEvent:event];
}
- (void)handleAction:(id)sender {
NSLog(@"handle Action");
}
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
Btn *btn = [[Btn alloc]initWithFrame:CGRectMake(30, 30, 100, 100)];
btn.backgroundColor = [UIColor yellowColor];
[btn addTarget:self action:@selector(btnclick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
- (IBAction)btnclick:(id)sender {
NSLog(@"click");
}
最后處理事件的Selector是Btn的handleAction:方法已卷,而不是ViewController的btnclick:方法。
另外淳蔼,sendAction:to:forEvent:實際上也被UIControl的另一個方法所調用侧蘸,即sendActionsForControlEvents:裁眯。這個方法的作用是發(fā)送與指定類型相關的所有行為消息。我們可以在任意位置(包括控件內部和外部)調用控件的這個方法來發(fā)送參數controlEvents指定的消息讳癌。在我們的示例中穿稳,在ViewController.m中作了如下測試:
- (void)viewDidLoad {
// ...
[btn addTarget:self action:@selector(btnclick:) forControlEvents:UIControlEventTouchUpInside];
[btn sendActionsForControlEvents:UIControlEventTouchUpInside];
}
沒有點擊btn,觸發(fā)了UIControlEventTouchUpInside事件晌坤,并執(zhí)行handleAction:方法逢艘。