精簡地說:iOS事件分為傳遞和響應兩個部分。
事件傳遞(建立傳遞鏈):
iOS系統檢測到手指觸摸(Touch)操作時會將其打包成一個UIEvent對象,并放入當前活動Application的事件隊列,單例的UIApplication會從事件隊列中取出觸摸事件并傳遞給單例的UIWindow來處理,UIWindow對象首先會使用hitTest:withEvent:方法尋找此次Touch操作初始點所在的視圖(View),即需要將觸摸事件傳遞給其處理的視圖,這個過程稱之為hit-test view。
hittest的目的就是找到最終的傳遞鏈。
hitTest:withEvent:流程如下:
- 先判斷當前視圖hidden=YES,userInteractionEnabled=NO,alpha<0.01等屬性,如果滿足其中之一,返回nil。
- 再看當前視圖的pointInside:withEvent:方法判斷觸摸點是否在當前視圖內;
- 若返回NO,則hitTest:withEvent:返回nil;
- 若返回YES,則向當前視圖的【一級子視圖(subviews)遞歸發送】hitTest:withEvent:消息,所有子視圖的遍歷順序是【從subviews數組的末尾向前遍歷】,直到有子視圖返回非空對象或者全部子視圖遍歷完畢;
- 若第一次有子視圖返回非空對象,則hitTest:withEvent:方法返回此對象,處理結束;如所有子視圖都返回非,則hitTest:withEvent:方法返回自身。
提醒:
hittest返回nil表示該條傳遞鏈已經終止,不是正確的傳遞鏈。
hittest返回非nil對象表示已經找到傳遞鏈的葉子節點,即找到正確的傳遞鏈。
hitTest:withEvent:遇到以下會返回nil。
1.hidden=YES的視圖。
2.userInteractionEnabled=NO的視圖(注意userInteractionEnabled是影響子視圖事件傳遞,但不影響兄弟視圖)
3.alpha<0.01的視圖。
4.顯示區域超過父視圖bounds區域的視圖。那么超出區域不能識別。當然,可以重寫pointInside:withEvent:方法來識別。
hitTest:底層實現
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判斷自己能否接收觸摸事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判斷觸摸點在不在自己范圍內
if (![self pointInside:point withEvent:event]) return nil;
// 3.從后往前遍歷自己的子控件,看是否有子控件更適合響應此事件
int count = self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];
CGPoint childPoint = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childPoint withEvent:event];
if (fitView) {
return fitView;
}
}
// 沒有找到比自己更合適的view
return self;
}
結合例子分析:
舉例分析:用戶點擊了View D,下面結合上圖介紹hit-test view的流程:
1、A是UIWindow的根視圖,因此,UIWindwo對象會首先對A進行hit-test;
2、顯然用戶點擊的范圍是在A的范圍內,因此,pointInside:withEvent:返回了YES,這時會繼續檢查A的子視圖;
3、這時候會有兩個分支,B和C:
C在subviews的末尾,先遞歸遍歷C。點擊的范圍在C內,即C的pointInside:withEvent:返回YES;
4、這時候有D和E兩個分支:
點擊的范圍不再E內,因此E的pointInside:withEvent:返回NO,對應的hitTest:withEvent:返回nil;
點擊的范圍在D內,即D的pointInside:withEvent:返回YES,由于D沒有子視圖,因此,D的hitTest:withEvent:會將D返回,再往回回溯,就是C的hitTest:withEvent:返回D--->>A的hitTest:withEvent:返回D。
即A->C->E(返回nil)->D(返回D)->C(返回D)->A(返回D)
至此,本次點擊事件的第一響應者就通過響應者鏈的事件分發邏輯成功的找到了。
不難看出,這個處理流程有點類似二分搜索的思想,這樣能以最快的速度,最精確地定位出能響應觸摸事件的UIView。
另外hittest可能會被調用2-3次,這里請不要再hittest作業務相關操作,否則會導致執行多次。
事件響應(回溯響應鏈):
經過hittest后,我們已經找到了鏈尾【第一響應者】,這時候開始回溯響應操作。響應鏈的關系如圖:
請注意:響應鏈在傳遞鏈的基礎上增加了UIViewController。
NextResponder:
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(UIView和UIViewController都是繼承UIResponder)中有對應的方法來分別處理這幾個階段的事件:
touchesBegan:NSArray<UITouch *>>withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:
Touch默認流程(以單擊為例)是:從響應鏈葉子節點開始回溯,先是TouchBegan回溯,然后是TouchEnded回溯。
舉例:對于觸摸事件來說,UIApplication會首先把事件交給keyWindow,Window會將事件交給UIGestureRecognizer
處理,如果UIGestureRecognizer
識別了傳遞過來的事件,則交給相對應的target去處理,回溯終止,事件不會再傳遞!當然這里也可以重寫touchBegan等方法手動讓手勢繼續往superview傳從而實現多級響應。
如果UIGestureRecognizer
并沒有識別傳遞過來的事件(可能是沒有視圖添加手勢,也可能手勢識別不成功),事件會傳遞到視圖樹形結構
touches方法實際上什么事都沒做,UIView繼承了它進行重寫,就是把事件傳遞給nextResponder,相當于[self.nextResponder touchesBegan:touches withEvent:event]。所以當一個view沒有重寫touch事件,那么這個事件就會一直傳遞下去,直到UIApplication。如果重寫了touch方法,這個view響應了事件之后,事件就被攔截了,它的nextResponder不會收到這個事件。這個時候如果想事件繼續傳遞下去,可以調用[self.nextResponder touchesBegan:touches withEvent:event]
手勢:
通過touches方法監聽view觸摸事件,有很明顯的幾個缺點:必須得自定義view、由于是在view內部的touches方法中監聽觸摸事件,因此默認情況下,無法讓其他外界對象監聽view的觸摸事件、不容易區分用戶的具體手勢行為。
所以iOS把觸摸事件做了封裝, 對常用的手勢進行了處理, 封裝了6種常見的手勢
UITapGestureRecognizer(敲擊)
UILongPressGestureRecognizer(長按)
UISwipeGestureRecognizer(輕掃)
UIRotationGestureRecognizer(旋轉)
UIPinchGestureRecognizer(捏合,用于縮放)
UIPanGestureRecognizer(拖拽)
UIControlEvent:
其實要了解UIControlEvent,必須簡單說一下UITouch和UIEvent事件,都是和觸摸相關,UIEvent是一系列UITouch的集合,在IOS中負責響應觸摸事件。
UIControl是UIView的子類,當然也是UIResponder的子類。UIControl是諸如UIButton、UISwitch、UITextField等控件的父類,它本身也包含了一些屬性和方法。
UIControl對象采用了一種新的事件處理機制,將觸摸事件轉換成簡單操作,其實就是重寫了UIResponder的方法中(如touchBegan:withEvent)中,即事件不再往上回溯響應。這樣方便了事件處理,而不用每次都重寫TouchBegan方法。
UIResponder可以參考這篇文章UIKit: UIResponder。
比如我點擊一個UIButton,即使你未添加UIControlEventTouchUpInside,它的父類touchBegan也不會被調用。因為UIControl重寫的方法touchBegan:withEvent并未調用[super touchBegan:withEvent]
UITapGestureRecognzier:
UITapGestureRecognzier其實就是對各類復雜觸摸操作響應過程的一個封裝。
在六種手勢識別中,只有一種手勢是離散手勢,它就是UITapGestureRecognzier。離散手勢的特點就是一旦識別就無法取消,而且只會調用一次手勢操作事件(初始化手勢時指定的觸發方法)。換句話說其他五種手勢是連續手勢,連續手勢的特點就是會多次調用手勢操作事件,而且在連續手勢識別后可以取消手勢。【下圖是手勢狀態圖】
分別以UITap和UIPan兩種手勢說明流程:
UITap:TouchBegan回溯->Tap->TouchCancelled回溯。
UIPan:TouchBegan回溯->TouchMoved多次回溯->UIPanBegan-TouchCancelled回溯->UIPanChanged多次->UIPanEnded。
事件總結:
1.父視圖不能接收事件,則子視圖無法接受事件
2.子視圖超出父視圖的部分,不能接收事件
3.同一個父視圖下,最上面的視圖,首先遭遇事件,如果能夠響應,就不向下傳遞事件。如果不能接收,事件向下傳遞
總結:
- iOS事件流程分為尋找響應鏈和響應鏈回溯,其中響應鏈回溯部分和android類似。
- 為了事件響應處理的方便,蘋果又推出了UIControlEvent和UITapGestureRecognzier。它們都是針對響應鏈回溯過程。
- UITapGestureRecognzier和UIControlEvent不同,UIControlEvent不會響應父類的TouchBegan等操作,而UITapGestureRecognzier會響應。