iOS中的事件
觸摸事件蕴纳,加速事件(搖一搖)爵卒,遠(yuǎn)程控制事件(耳機(jī)線控,窗口播放)
以最常見的觸摸事件為例堡称,當(dāng)觸摸手機(jī)屏幕時操作系統(tǒng)會將這個事件添加到由UIApplication管理的事件隊列中(FIFO)UIApplication發(fā)送事件到應(yīng)用程序的主窗口(Window)Window會在圖層結(jié)構(gòu)中找到最合適的圖層來處理事件。
UIResponder
UIResponder類是專門用來響應(yīng)用戶的操作處理各種事件的艺演,iOS中大部分控件都繼承自UIResponder却紧,默認(rèn)響應(yīng)事件的方法如下(觸摸事件)
- (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;//觸摸結(jié)束胎撤,手機(jī)離開屏幕
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//中斷晓殊,被手勢或者系統(tǒng)中斷
事件傳遞鏈
UIApplication傳遞事件到當(dāng)前Window是明確的,接下來就是從Window開始找最佳響應(yīng)視圖伤提,此過程有兩個重要的方法:
hitTest方法繼承自UIView(UIWindow是繼承自UIView的)巫俺。從UIApplication開始調(diào)用Window的hitTest方法,默認(rèn)是遞歸調(diào)用的肿男。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return [super pointInside:point withEvent:event];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return [super hitTest:point withEvent:event];
}
傳遞過程如下:
1.系統(tǒng)從UIApplication開始介汹,當(dāng)前window調(diào)用hitTest,hitTest內(nèi)部會通過以下條件判斷window能否能響應(yīng)事件
- 不允許交互:userInteractionEnabled=NO
- 隱藏:hidden = YES
- 透明度:alpha < 0.01舶沛,alpha小于0.01為全透明
2.如果能響應(yīng)嘹承,該函數(shù)內(nèi)部會調(diào)用pointInside判斷當(dāng)前觸摸點是不是在視圖范圍內(nèi)
3.如果在window范圍內(nèi),開始反向遍歷window的子視圖列表subviews如庭,遍歷的同時會調(diào)用subviews中每個子視圖的hitTest叹卷,判斷邏輯和上面的一樣,如果找到循環(huán)就會停止坪它。
4.此過程會遞歸骤竹,直到找到最外層合適的view,最后返回的view就是最佳響應(yīng)視圖往毡。
一種hitTest可能的實現(xiàn)方式如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (!self.userInteractionEnabled|| self.hidden || self.alpha == 0.0){
return nil;
}
if (![self pointInside:point withEvent:event]){
return nil;
}
// 后加入的視圖在圖層上方蒙揣,所以反向遍歷是合理的
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--)
{
UIView *view = self.subviews[i];
// 坐標(biāo)的轉(zhuǎn)換
CGPoint subPoint = [self convertPoint:point toView:view];
// 繼續(xù)遞歸
UIView *lastView = [view hitTest:subPoint withEvent:event];
if (lastView)
{
return lastView;
}
}
return self;
}
以上,這就是事件傳遞過程卖擅,由內(nèi)往外的傳遞過程(從window開始到最外層視圖 )
此過程查找結(jié)束返回最終的view鸣奔,UIApplication會調(diào)用UIWindow的sendEvent墨技,從而觸發(fā)對應(yīng)的響應(yīng)方法:
PS:這里通過在UIWIndow中重寫sendEvent而不調(diào)用super的實現(xiàn),你會發(fā)現(xiàn)所有的點擊事件都不會觸發(fā)
- (void)sendEvent:(UIEvent *)event;
以下是需要注意的點:
實際調(diào)用hitTest過程挎狸,系統(tǒng)為了找到精準(zhǔn)的觸摸點會多次調(diào)用
如果重寫hitTest返回self扣汪,傳遞過程就會終止,當(dāng)前view就是最合適的view锨匆;返回nil,傳遞也會終止崭别,父視圖superView就是最合適的view
如果遍歷subviews的過程都沒找到合適的view,那么subviews中的子view的hitTest會都會被被調(diào)用一次
hitTest會調(diào)用pointInside判斷當(dāng)前視圖是否在點擊區(qū)域恐锣,所以超出父視圖邊界的控件無法響應(yīng)事件
同一個view上的兩個子視圖有重疊部分茅主,后加入的視圖會被加入到事件傳遞鏈
事件響應(yīng)鏈
首先,響應(yīng)者鏈中的各個響應(yīng)者都繼承自UIResponder土榴,常見的UIView,viewController,UIWindow以及AppDelegate都繼承自UIResponder诀姚。響應(yīng)者鏈上的響應(yīng)者在hitTest過程中就已經(jīng)確定,可以通過迭代nextResponder查看所有的響應(yīng)者玷禽。
事件響應(yīng)鏈如下:
通過hitTest返回的view為當(dāng)前事件的第一響應(yīng)者赫段,nextResponder為上一個響應(yīng)者
如果當(dāng)前view默認(rèn)不去重寫,或者重寫調(diào)用了父類的實現(xiàn)矢赁,響應(yīng)就會就會沿著響應(yīng)者鏈向上傳遞(上一個響應(yīng)者一般是superView糯笙,可以通過nextResponder屬性獲取上一個響應(yīng)者)
如果上一個響應(yīng)者是viewController,由viewController的view處理撩银,view本身沒處理给涕,則傳遞給viewController本身
重復(fù)上述過程,直到傳遞到window额获,window如果也不能處理够庙,傳遞到UIApplication,如果UIApplication的delegate繼承自UIResponder咪啡,則交給delegate處理首启,delegate也不處理最后丟棄
以上就是響應(yīng)者鏈,事件響應(yīng)過程是從外向內(nèi)傳遞撤摸,和事件傳遞的過程正好相反
通過遍歷查找所有響應(yīng)者:
UIResponder *respon = self;
while (respon) {
NSLog(@"%@",respon);
respon = respon.nextResponder;
}
有手勢的情況下
手勢識別器的作用就是毅桃,識別到對應(yīng)的手勢后發(fā)送消息給target。iOS中的手勢分為兩種准夷,Apple文檔中有提到:
- 離散型手勢 (UITapGestureRecognizer钥飞,UISwipeGestureRecognizer)
- 持續(xù)性手勢 (UIPinchGestureRecognizer,UIPanGestureRecognizer衫嵌,UIRotationGestureRecognizer读宙,UILongPressGestureRecognizer)
離散型手勢的情況:
view未添加點擊手勢,點擊一次屏幕會調(diào)用touchesBegan和touchesEnde楔绞,當(dāng)我們不考慮touchesEnde的時候可以認(rèn)為它是一次性的
touchesBegan
touchesEnde
view添加tap手勢结闸,點擊屏幕會觸發(fā)手勢對應(yīng)的方法唇兑,touchesBegan和touchesCancelled,這里雖然調(diào)用了touchesCancelled,但實際上touchesBegan已經(jīng)觸發(fā)了
touchesBegan
tap
touchesCancelled
連續(xù)型手勢的情況:
view未添加連續(xù)手勢桦锄,當(dāng)手指在屏幕上拖動時扎附,先touchesBegan,然后touchesMoved隨著手指拖動持續(xù)調(diào)用结耀,停止后調(diào)用touchesEnde
touchesBegan
touchesMoved
...
touchesEnded
view添加pan拖拽手勢留夜,當(dāng)手指在屏幕上拖動時,touchesBegan和touchesMoved會先調(diào)用图甜,當(dāng)pan手勢方法觸發(fā)以后碍粥,touchesMoved將不再出現(xiàn),同時touchesCancelled也觸發(fā)了
touchesBegan
touchesMoved
pan //識別到 pan之后黑毅,就只有pan手勢會響應(yīng)
touchesCancelled
pan
pan
...
以下結(jié)論主要針對連續(xù)型手勢:
- 若手勢成功識別事件嚼摩,就會取消第一響應(yīng)者view對事件的響應(yīng)(touchesCancelled)
- 若手勢沒能識別事件,第一響應(yīng)者view就會接手事件的處理
通過斷點在sendEvent:處查看UIEvent事件博肋,在event->_allTouchesMutable->_gestureRecognizers手勢中可以看到當(dāng)前touch對象中包含所有的手勢對象低斋,通過斷點可以看到數(shù)組中第一個手勢的對象地址0x10510a2d0正是添加的tap手勢的地址。因此可以說明匪凡,手勢會先響應(yīng)
touch的gestureRecognizers數(shù)組:
_gestureRecognizers __NSArrayM * @"6 elements" 0x0000000282bd46f0
[0] UITapGestureRecognizer * 0x10510a2d0 0x000000010510a2d0
[1] UIPanGestureRecognizer * 0x10510a3f0 0x000000010510a3f0
[2] UITapGestureRecognizer * 0x10510a1b0 0x000000010510a1b0
[3] UITapGestureRecognizer * 0x105107f70 0x0000000105107f70
[4] _UISystemGestureGateGestureRecognizer * 0x105011020 0x0000000105011020
[5] _UISystemGestureGateGestureRecognizer * 0x10500ff50 0x000000010500ff50
添加的tap手勢對象:
[2890:328171] tap:<UITapGestureRecognizer: 0x10510a2d0; state = Ended; view = <BlueView 0x105109e40>; target= <(action=tap:, target=<FirstViewController 0x1050114b0>)>>
有UIControl(按鈕)的情況
以Button為例,給Button添加添加tap手勢和TouchDown類型target掘猿,結(jié)果和上面的例子一樣病游,對于一次性手勢都會響應(yīng)
touchesBegan
TouchDown
tap
touchesCancelled
給Button只添加TouchDragInside類型target,touchesMoved和TouchDragInside都會響應(yīng)
touchesMoved
TouchDragInside
給Button添加pan手勢和TouchDragInside類型target,系統(tǒng)識別到pan手勢后就會touchesCancelled稠通,只有手勢pan會執(zhí)行
touchesMoved
TouchDragInside
pan //識別到 pan之后衬衬,就只有pan手勢會響應(yīng)
touchesCancelled
pan
pan
...