移動平臺上的開發主要關注數據以及數據的處理,事件的處理以及UI。因此事件的分發處理是很重要的一個環節,對於一個平臺的優劣來講也是一項重要的參數。若是事件的分發設計的很差,一些複雜的UI場景就會變得很難寫甚至無法寫。從小屏沒有觸摸的功能機開始到如今大屏多點觸摸的智能機,對於事件的分發處理基本思路都是同樣的——鏈(設計模式中有個模式就是職責鏈chain of responsibility),只是斷定的複雜程度不一樣。設計模式
iOS中的事件有3類,觸摸事件(單點,多點,手勢)、傳感器事件(加速度傳感器)和遠程控制事件,這裏我介紹的是第一種事件的分發處理。數組
上面的這張圖來自蘋果的官方。描述了Responder的鏈,同時也是事件處理的順序。經過這兩張圖,咱們能夠發現:app
1. 事件順着responder chain傳遞,若是一環不處理,則傳遞到下一環,若是都沒有處理,最後回到UIApplication,再不處理就會拋棄ide
2. view的下一級是包含它的viewController,若是沒有viewController則是它的superViewspa
3. viewController的下一級是它的view的superView.net
4. view以後是window,最後傳給application,這點iOS會比OS X簡單(application就一個,window也一個)設計
總結出來傳遞規則是這樣的:code
這樣事件就會從first responder逐級傳遞過來,直到被處理或者被拋棄。orm
因爲UI的複雜,這個responder chain是須要根據事件來計算的。好比,我如今在一個view內加入了2個Button,先點擊了一個,則first responder確定是這個點擊過的button,但我下面能夠去點擊另外一個button,因此顯然,當觸摸事件來時,這個chain是須要從新計算更新的,這個計算的順序是事件分發的順序,基本上是分發的反過來。對象
不管是哪一種事件,都是系統自己先得到,是iOS系統來傳給UIApplication的,由Application再決定交給誰去處理,因此若是咱們要攔截事件,能夠在UIApplication層面或者UIWindow層面去攔截。
UIView是如何斷定這個事件是不是本身應該處理的呢?iOS系統檢測到一個觸摸操做時會打包一個UIEvent對象,並放入Application的隊列,Application從隊列中取出事件後交給UIWindow來處理,UIWindow會使用hitTest:withEvent:方法來遞歸的尋找操做初始點所在的view,這個過程成爲hit-test view。
hitTest:withEvent:方法的處理流程以下:調用當前view的pointInside:withEvent:方法來斷定觸摸點是否在當前view內部,若是返回NO,則hitTest:withEvent:返回nil;若是返回YES,則向當前view內的subViews發送hitTest:withEvent:消息,全部subView的遍歷順序是從數組的末尾向前遍歷,直到有subView返回非空對象或遍歷完成。若是有subView返回非空對象,hitTest方法會返回這個對象,若是每一個subView返回都是nil,則返回本身。
處理原理以下:
• 當用戶點擊屏幕時,會產生一個觸摸事件,系統會將該事件加入到一個由UIApplication管理的事件隊列中
• UIApplication會從事件隊列中取出最前面的事件進行分發以便處理,一般,先發送事件給應用程序的主窗口(UIWindow)
• 主窗口會調用hitTest:withEvent:方法在視圖(UIView)層次結構中找到一個最合適的UIView來處理觸摸事件
(hitTest:withEvent:實際上是UIView的一個方法,UIWindow繼承自UIView,所以主窗口UIWindow也是屬於視圖的一種)
• hitTest:withEvent:方法大體處理流程是這樣的:
首先調用當前視圖的pointInside:withEvent:方法判斷觸摸點是否在當前視圖內:
▶ 若pointInside:withEvent:方法返回NO,說明觸摸點不在當前視圖內,則當前視圖的hitTest:withEvent:返回nil
▶ 若pointInside:withEvent:方法返回YES,說明觸摸點在當前視圖內,則遍歷當前視圖的全部子視圖(subviews),調用子視圖的hitTest:withEvent:方法重複前面的步驟,子視圖的遍歷順序是從top到bottom,即從subviews數組的末尾向前遍歷,直到有子視圖的hitTest:withEvent:方法返回非空對象或者所有子視圖遍歷完畢:
▷ 若第一次有子視圖的hitTest:withEvent:方法返回非空對象,則當前視圖的hitTest:withEvent:方法就返回此對象,處理結束
▷ 若全部子視圖的hitTest:withEvent:方法都返回nil,則當前視圖的hitTest:withEvent:方法返回當前視圖自身(self)
• 最終,這個觸摸事件交給主窗口的hitTest:withEvent:方法返回的視圖對象去處理。
拿到這個UIView後,就調用該UIView的touches系列方法。
1.二、消息處理過程,在找到的那個視圖裏處理,處理完後根據須要,利用響應鏈nextResponder可將消息往下一個響應者傳遞。
UIAppliactionDelegate <- UIWindow <- UIViewController <- UIView <- UIView
【關鍵】:要理解的有三點:一、iOS判斷哪一個界面能接受消息是從View層級結構的父View向子View傳遞,即樹狀結構的根節點向葉子節點遞歸傳遞。二、hitTest和pointInside成對,且hitTest會調用pointInside。三、iOS的消息處理是,當消息被人處理後默認再也不向父層傳遞。
好了,咱們仍是看個例子:
這裏ViewA包含ViewB和ViewC,ViewC中繼續包含ViewD和ViewE。假設咱們點擊了viewE區域,則hit-test View斷定過程以下:
1. 觸摸在A內部,因此須要檢查B和C
2. 觸摸不在B內部,在C內部,因此須要檢查D和E
3. 觸摸不在D內部,但在E內部,因爲E已是葉子了,因此斷定到此結束
咱們能夠運行一段代碼來驗證,首先從UIView繼承一個類myView,重寫裏面的
[objc] view plain copy
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *retView = nil;
NSLog(@"hitTest %@ Entry! event=%@", self.name, event);
retView = [super hitTest:point withEvent:event];
NSLog(@"hitTest %@ Exit! view = %@", self.name, retView);
return retView;
}
[objc] view plain copy
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL ret = [super pointInside:point withEvent:event];
// if ([self.name isEqualToString:@"viewD"]) {
// ret = YES;
// }
if (ret) {
NSLog(@"pointInside %@ = YES", self.name);
} else {
NSLog(@"pointInside %@ = NO", self.name);
}
return ret;
}
在viewDidLoad方法中手動加入5個view,都是myView的實例。
[objc] view plain copy
- (void)viewDidLoad
{
[super viewDidLoad];
_viewA = [[myView alloc] initWithFrame:CGRectMake(10, 10, 300, 200) Color:[UIColor blackColor] andName:@"viewA"];
[self.view addSubview:_viewA];
[_viewA release];
_viewB = [[myView alloc] initWithFrame:CGRectMake(10, 240, 300, 200) Color:[UIColor blackColor] andName:@"viewB"];
[self.view addSubview:_viewB];
[_viewB release];
_viewC = [[myView alloc] initWithFrame:CGRectMake(10, 10, 120, 180) Color:[UIColor blueColor] andName:@"viewC"];
[_viewB addSubview:_viewC];
[_viewC release];
_viewD = [[myView alloc] initWithFrame:CGRectMake(170, 10, 120, 180) Color:[UIColor blueColor] andName:@"viewD"];
[_viewB addSubview:_viewD];
[_viewD release];
_viewE = [[myView alloc] initWithFrame:CGRectMake(30, 40, 60, 100) Color:[UIColor redColor] andName:@"viewE"];
[_viewD addSubview:_viewE];
[_viewE release];
}
這個樣式以下:
當我點擊viewE的時候,打印信息以下:
2014-01-25 18:32:46.538 eventDemo[1091:c07] hitTest viewB Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(
)}
2014-01-25 18:32:46.538 eventDemo[1091:c07] pointInside viewB = YES
2014-01-25 18:32:46.539 eventDemo[1091:c07] hitTest viewD Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(
)}
2014-01-25 18:32:46.539 eventDemo[1091:c07] pointInside viewD = YES
2014-01-25 18:32:46.539 eventDemo[1091:c07] hitTest viewE Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(
)}
2014-01-25 18:32:46.540 eventDemo[1091:c07] pointInside viewE = YES
2014-01-25 18:32:46.540 eventDemo[1091:c07] hitTest viewE Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>
2014-01-25 18:32:46.540 eventDemo[1091:c07] hitTest viewD Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>
2014-01-25 18:32:46.541 eventDemo[1091:c07] hitTest viewB Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>
2014-01-25 18:32:46.541 eventDemo[1091:c07] touchesBegan viewE
2014-01-25 18:32:46.624 eventDemo[1091:c07] touchesEnded viewE
從打印信息能夠看到,先判斷了viewB,而後是viewD,最後是viewE,但事件就是直接傳給了viewE。