用戶以多種方式操縱他們的iOS設(shè)備,例如觸摸屏幕或搖動(dòng)設(shè)備蜻牢。 iOS會(huì)解釋用戶何時(shí)以及如何操作硬件并將此信息傳遞到您的應(yīng)用程序。 您的應(yīng)用程序以自然和直觀的方式響應(yīng)操作的次數(shù)越多似芝,對(duì)用戶而言越有吸引力的體驗(yàn)嵌赠。
一、事件分類
事件是發(fā)送到應(yīng)用程序用于通知用戶操作的對(duì)象忽妒。 在iOS中玩裙,事件可以采取多種形式:多點(diǎn)觸摸事件,運(yùn)動(dòng)事件和用于控制多媒體的事件段直。 這最后一種類型的事件被稱為遙控事件或者遠(yuǎn)程控制事件吃溅,因?yàn)樗梢栽醋酝獠扛郊6谖覀冮_發(fā)過程中最常用的就是多點(diǎn)觸摸事件坷牛。
二罕偎、事件傳遞與響應(yīng)鏈
當(dāng)您設(shè)計(jì)應(yīng)用程式時(shí)很澄,可能需要?jiǎng)討B(tài)響應(yīng)事件京闰。 例如,觸摸可以發(fā)生在屏幕上的許多不同對(duì)象中甩苛,并且您必須決定您想要那個(gè)對(duì)象響應(yīng)事件蹂楣,并且理解該對(duì)象如何接收該事件。
當(dāng)用戶生成的事件發(fā)生時(shí)讯蒲,UIKit創(chuàng)建一個(gè)包含處理事件所需信息的事件對(duì)象痊土。 然后它將事件對(duì)象放置在活動(dòng)應(yīng)用程序的事件隊(duì)列中。 對(duì)于觸摸事件墨林,該對(duì)象是在UIEvent對(duì)象中打包的一組觸摸赁酝。 對(duì)于運(yùn)動(dòng)事件犯祠,事件對(duì)象因您使用的框架和您感興趣的運(yùn)動(dòng)事件類型而異。
事件沿著特定路徑傳遞酌呆,直到它被傳遞到可以處理它的對(duì)象衡载。 首先,單例UIApplication對(duì)象從隊(duì)列的頂部獲取一個(gè)事件并分發(fā)處理隙袁。 通常痰娱,它將事件發(fā)送到應(yīng)用程序的key window對(duì)象,該對(duì)象將事件傳遞到初始對(duì)象(initial object)進(jìn)行處理菩收。 初始對(duì)象取決于事件的類型梨睁。
觸摸事件:對(duì)于觸摸事件,窗口對(duì)象首先嘗試將事件傳遞到發(fā)生觸摸的視圖娜饵。 該視圖稱為命中測(cè)試視圖(hit-test view)坡贺。 找到命中測(cè)試視圖(hit-test view)的過程稱為命中測(cè)試(hit-testing),這在Hit-Testing返回觸摸發(fā)生的視圖中描述箱舞。
運(yùn)動(dòng)和遙控事件:對(duì)于這些事件拴念,窗口對(duì)象將搖動(dòng)或遠(yuǎn)程控制事件發(fā)送到第一響應(yīng)者以進(jìn)行處理。 第一響應(yīng)者在響應(yīng)者鏈由響應(yīng)者對(duì)象組成中描述褐缠。
這些事件路徑的最終目標(biāo)是找到一個(gè)可以處理和響應(yīng)事件的對(duì)象政鼠。 因此,UIKit首先將事件發(fā)送到最適合處理事件的對(duì)象队魏。 對(duì)于觸摸事件公般,該對(duì)象是命中測(cè)試視圖(hit-test view),對(duì)于其他事件胡桨,該對(duì)象是第一個(gè)響應(yīng)者官帘。 以下部分更詳細(xì)地說明命中測(cè)試視圖(hit-test view)和第一響應(yīng)者對(duì)象是如何確定的。
1. Hit-Testing返回觸摸發(fā)生的視圖
iOS使用命中測(cè)試(hit-testing)來查找被觸摸的視圖昧谊。 命中測(cè)試(hit-testing)涉及檢查觸摸是否在所有相關(guān)視圖對(duì)象的邊界內(nèi)刽虹。 如果是,它會(huì)遞歸檢查視圖的所有子視圖呢诬。視圖層級(jí)中包含觸摸點(diǎn)的最低的視圖成為命中測(cè)試視圖(hit-test view) 涌哲。 iOS確定命中測(cè)試視圖(hit-test view)后,它會(huì)將觸摸事件傳遞到該視圖進(jìn)行處理尚镰。
舉例說明阀圾,假設(shè)用戶觸摸下圖中的View E。 iOS通過按照此順序檢查子視圖來查找命中測(cè)試視圖(hit-test view):
觸摸在View A的邊界內(nèi)狗唉,因此它檢查子視圖View B和View C.
觸摸不在View B的界限內(nèi)初烘,但它在View C的界限內(nèi),因此它檢查子視圖View D和View E.
-
觸摸不在View D的界限內(nèi),但它在View E的界限內(nèi)肾筐。
View E是視圖層級(jí)中包含觸摸的最低的視圖哆料,因此它成為命中測(cè)試視圖(hit-test view)。
Hit-testing returns the subview that was touched
hitTest:withEvent:
方法為給定的CGPoint和UIEvent返回一個(gè)點(diǎn)擊測(cè)試視圖(hit-test view)吗铐。hitTest:withEvent:
方法首先調(diào)用pointInside:withEvent:
方法剧劝。 如果傳遞到hitTest:withEvent:
方法的點(diǎn)是在視圖的邊界內(nèi),pointInside:withEvent:
返回YES抓歼。然后讥此,在每個(gè)返回YES的子視圖上遞歸調(diào)用hitTest:withEvent:
方法 。
如果傳遞到hitTest:withEvent:
方法的點(diǎn)不在視圖的邊界內(nèi)谣妻,第一次調(diào)用pointInside:withEvent:
方法返回 NO 萄喳,該點(diǎn)被忽略,hitTest:withEvent:
返回nil 蹋半。 如果子視圖返回NO他巨,則視圖層級(jí)結(jié)構(gòu)的這個(gè)整個(gè)分支將被忽略,因?yàn)槿绻|摸沒有發(fā)生在該子視圖中减江,則它也不會(huì)出現(xiàn)在該子視圖的任何子視圖中染突。這意味著在子視圖內(nèi)而在父視圖之外的任何點(diǎn)都不能接受點(diǎn)擊事件,因?yàn)橛|摸點(diǎn)必須在父視圖和子視圖邊界內(nèi)辈灼。如果子視圖的clipsToBounds屬性設(shè)置為NO份企,則可能出現(xiàn)此問題。見示例將事件傳遞給子視圖
注:觸摸對(duì)象為其生命周期而關(guān)聯(lián)到其命中測(cè)試視圖(hit-test view)巡莹,即使觸摸稍后移動(dòng)到視圖之外司志。
命中測(cè)試視圖(hit-test view)被給予首先處理觸摸事件的機(jī)會(huì)。 如果命中測(cè)試視圖(hit-test view)無法處理的事件降宅,事件沿著響應(yīng)者鏈向上傳播(如響應(yīng)者鏈由響應(yīng)者對(duì)象組成中描述)骂远,直到系統(tǒng)找到一個(gè)可以處理它的對(duì)象。
2. 響應(yīng)者鏈由響應(yīng)者對(duì)象組成
許多類型的事件依賴于為事件傳遞的響應(yīng)者鏈腰根。 響應(yīng)鏈?zhǔn)且幌盗斜绘溄悠饋淼捻憫?yīng)對(duì)象激才。 它從第一響應(yīng)者開始,到程序?qū)ο螅║IApplication object)結(jié)束额嘿。 如果第一響應(yīng)者不能處理事件瘸恼,它轉(zhuǎn)發(fā)事件到響應(yīng)者鏈中的下一個(gè)響應(yīng)者。
響應(yīng)者對(duì)象是一個(gè)可以響應(yīng)和處理事件的對(duì)象岩睁。 UIResponder類是所有響應(yīng)者對(duì)象的基類钞脂,它不僅為事件處理定義編程接口揣云,也為常見響應(yīng)者行為定義編程接口捕儒。UIApplication, UIViewController和UIView類的實(shí)例都是響應(yīng)者(responder),這意味著所有的視圖和大多數(shù)控制器對(duì)象都是響應(yīng)者刘莹。 注意核心動(dòng)畫層不是響應(yīng)者阎毅。
第一個(gè)響應(yīng)者被指定為第一個(gè)接收事件。 通常点弯,第一響應(yīng)者是視圖對(duì)象扇调。 一個(gè)對(duì)象通過做兩件事情成為第一個(gè)響應(yīng)者:
- 重寫
canBecomeFirstResponder
方法返回YES。 - 接收
becomeFirstResponder
消息抢肛。 如果需要狼钮,對(duì)象可以向自身發(fā)送此消息。
注:請(qǐng)確保您的應(yīng)用程序在指派一個(gè)對(duì)象成為第一個(gè)響應(yīng)者之前已經(jīng)建立了對(duì)象圖(has established its object graph捡絮,個(gè)人感覺應(yīng)該理解為對(duì)象已經(jīng)被渲染完成)熬芜。 例如,您通常在重寫的
viewDidAppear:
方法中調(diào)用becomeFirstResponder
方法福稳。 如果您嘗試在viewWillAppear:
中指派第一響應(yīng)者涎拉,你的對(duì)象圖尚未建立(object graph is not yet established,個(gè)人理解為對(duì)象渲染尚未完成)的圆,所以becomeFirstResponder
方法返回 NO 鼓拧。
事件不是唯一依賴響應(yīng)者鏈的對(duì)象,響應(yīng)者鏈用于以下所有情況:
- 觸摸事件(Touch events):如果命中測(cè)試視圖(hit-test view)不能夠處理觸摸事件越妈,事件以命中測(cè)試視圖(hit-test view)為起點(diǎn)沿著響應(yīng)者鏈向上傳遞季俩。
- 運(yùn)動(dòng)事件(Motion events):為了使用UIKit處理搖動(dòng)動(dòng)作事件,第一響應(yīng)者必須實(shí)現(xiàn)
UIResponder
類的motionBegan:withEvent:
或motionEnded:withEvent:
的方法梅掠。 - 遙控事件(Remote control event):為了處理遙控事件种玛,第一響應(yīng)者必須實(shí)現(xiàn)
UIResponder
類的remoteControlReceivedWithEvent:
方法。 - 動(dòng)作消息(Action messages):當(dāng)用戶操作一個(gè)控制對(duì)象瓤檐,例如一個(gè)按鈕(button)或者開關(guān)(switch)赂韵,并且動(dòng)作方法(action method)的目標(biāo)(target)是nil,則消息以控制視圖為起點(diǎn)沿著響應(yīng)者鏈傳遞挠蛉。參閱示例:將事件傳遞給父視圖
- 編輯菜單消息(Editing-menu messages):當(dāng)用戶點(diǎn)擊編輯菜單中的命令祭示,iOS使用響應(yīng)者鏈找到實(shí)現(xiàn)了必要方法的對(duì)象(如
cut:
,copy:
和paste:
)谴古。 想了解更多信息质涛,請(qǐng)參閱顯示和管理編輯菜單 。
- 文本編輯(Text editing):當(dāng)用戶點(diǎn)擊text field或text view掰担,該視圖自動(dòng)成為第一個(gè)響應(yīng)者汇陆。 默認(rèn)情況下,虛擬鍵盤出現(xiàn)带饱,text field或text view成為編輯的焦點(diǎn)毡代。您可以顯示自定義輸入視圖阅羹,而不是鍵盤。 您還可以向任何響應(yīng)者對(duì)象添加自定義輸入視圖教寂。 想了解更多信息捏鱼,請(qǐng)參閱自定義數(shù)據(jù)輸入視圖 。
UIKit自動(dòng)設(shè)置用戶點(diǎn)擊的text field或text view為第一個(gè)響應(yīng)者; 應(yīng)用程序必須使用becomeFirstResponder
方法顯式設(shè)置所有其他對(duì)象為第一響應(yīng)者酪耕。
3. 響應(yīng)者鏈遵循特定傳遞路徑
如果初始對(duì)象(命中測(cè)試視圖或第一個(gè)響應(yīng)者)不處理事件导梆,UIKit將事件傳遞給鏈中的下一個(gè)響應(yīng)者。 每個(gè)響應(yīng)者決定是否它要處理事件或通過調(diào)用其nextRsponder
方法傳遞給它自己的下一個(gè)響應(yīng)者迂烁。這種處理持續(xù)進(jìn)行看尼,直到一個(gè)響應(yīng)者對(duì)象處理事件或有沒有更多的響應(yīng)者。
當(dāng)iOS檢測(cè)到事件并將其傳遞給初始對(duì)象(通常是視圖)時(shí)盟步,響應(yīng)者鏈序列開始狡忙。 初始視圖擁有第一機(jī)會(huì)處理事件。下圖顯示了兩個(gè)不同配置應(yīng)用程序的兩個(gè)不同事件傳遞路徑址芯。應(yīng)用程序的事件傳遞路徑取決于其特定結(jié)構(gòu)灾茁,但所有事件傳遞路徑都遵循相同的探視程序。
對(duì)于左側(cè)的應(yīng)用程序谷炸,事件遵循以下路徑:
- 初始視圖試圖處理該事件或消息北专。如果它不能處理這個(gè)事件,它將事件傳遞到其父視圖 旬陡,因?yàn)槌跏家晥D在它的視圖控制器的視圖層次中不是最頂部的視圖拓颓。
- 父視圖嘗試處理該事件。如果父視圖不能處理事件描孟,它將事件傳遞到其超級(jí)視圖驶睦,因?yàn)樗匀徊皇且晥D層次中最頂部的視圖。
- 視圖控制器的視圖層次中最頂層視圖嘗試處理該事件匿醒。如果最頂層的視圖不能處理事件场航,它將事件傳遞到它的視圖控制器。
- 視圖控制器嘗試處理該事件廉羔,如果不能溉痢,將事件傳遞到窗口。
- 如果窗口對(duì)象不能處理該事件憋他,傳遞事件到單例應(yīng)用程序?qū)ο蟆?/li>
- 如果應(yīng)用程序?qū)ο?/strong>不能處理這個(gè)事件孩饼,它丟棄該事件。
右側(cè)的應(yīng)用程序遵循稍微不同的路徑竹挡,但所有事件傳遞路徑遵循以下探視程序:
視圖在其視圖控制器的視圖層次結(jié)構(gòu)上向上傳遞事件镀娶,直到它到達(dá)最頂層視圖。
最頂層視圖將事件傳遞到其視圖控制器揪罕。
-
視圖控制器將事件傳遞到其最頂層視圖的父視圖梯码。
重復(fù)步驟1-3宝泵,直到事件到達(dá)根視圖控制器。
根視圖控制器將事件傳遞到窗口對(duì)象忍些。
窗口將事件傳遞給應(yīng)用程序?qū)ο蟆?/p>
重要提示:如果您實(shí)現(xiàn)一個(gè)自定義視圖來處理遙控事件鲁猩,動(dòng)作消息坎怪,UIKit的搖移動(dòng)事件罢坝,或編輯菜單消息,不要直接轉(zhuǎn)發(fā)事件或消息到
nextResponder
來沿響應(yīng)者鏈向上傳遞搅窿。 相反嘁酿,調(diào)用當(dāng)前事件處理方法的超類實(shí)現(xiàn),讓UIKit處理響應(yīng)者鏈的遍歷男应。
三闹司、應(yīng)用
從事件傳遞與響應(yīng)者鏈的內(nèi)容思考一些應(yīng)用例子。
1. 擴(kuò)大視圖的點(diǎn)擊區(qū)域
一個(gè)按鈕的尺寸是20*20沐飘,如果要擴(kuò)大按鈕的點(diǎn)擊區(qū)域(上下左右各擴(kuò)大10)游桩,有以下處理方法:
- 按鈕設(shè)置image,然后按鈕的size設(shè)置的比實(shí)際大一倍耐朴。
- 在按鈕上覆蓋一層較大的View或者Button借卧,設(shè)置點(diǎn)擊事件。
- 自定義Button筛峭,覆蓋
hitTest:withEvent:
或者pointInside:withEvent:
方法铐刘。
我們只舉例說明第三種方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%s", __PRETTY_FUNCTION__);
if (self.userInteractionEnabled == NO || self.hidden || self.alpha <= 0.01) {
return nil;
}
CGRect responseRect = CGRectInset(self.bounds, -10, -10);
if (CGRectContainsPoint(responseRect, point)) {
for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subView convertPoint:point fromView:self];
UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}
或者
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%s", __PRETTY_FUNCTION__);
CGRect bounds = CGRectInset(self.bounds, -10, -10);
return CGRectContainsPoint(bounds, point);
}
2. 將事件傳遞給父視圖
在controller中有一個(gè)YKNoteEventHandingView,其上面再添加一個(gè)YKNoteEventHandlingButton影晓,點(diǎn)擊Button將事件傳遞到View镰吵。有以下幾種做法:
Button的
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
方法返回nil,hit-test view為父視圖YKNoteEventHandingView的
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
方法返回self挂签,阻止事件傳遞給子視圖設(shè)置Button的target為nil疤祭,Button無法處理事件響應(yīng),事件沿著響應(yīng)者鏈向上傳遞饵婆,傳遞到父視圖画株。示例如下
#import "YKNoteEventHandingView.h"
@implementation YKNoteEventHandingView
//在View中寫一個(gè)action方法,判斷View中的Button的target為nil的時(shí)候是否會(huì)執(zhí)行啦辐,若執(zhí)行谓传,則消息沿著響應(yīng)者鏈向上傳遞了
- (void)ykNoteEventHandlingGreenButtonDidTouchUpInside:(UIButton *)button {
NSLog(@"%s \n %@", __PRETTY_FUNCTION__, button);
}
@end
#import "YKNoteEventHandlingButton.h"
//在Button中寫一個(gè)action方法,判斷Button的target為nil的時(shí)候是否會(huì)執(zhí)行芹关,若執(zhí)行续挟,則消息沿著響應(yīng)者鏈傳遞了
@implementation YKNoteEventHandlingButton
- (void)ykNoteEventHandlingGreenButtonDidTouchUpInside:(UIButton *)button {
NSLog(@"%s \n %@", __PRETTY_FUNCTION__, button);
}
#import "YKNoteEventHandingViewController.h"
#import "YKNoteEventHandingView.h"
#import "YKNoteEventHandlingButton.h"
@interface YKNoteEventHandingViewController ()
@property (nonatomic, strong) YKNoteEventHandingView *yKNoteEventHandingView;
@property (nonatomic, strong) YKNoteEventHandlingButton *ykNoteEventHandlingButton;
@end
@implementation YKNoteEventHandingViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"EventHandling";
self.view.backgroundColor = [UIColor whiteColor];
//View
[self.yKNoteEventHandingView setFrame:CGRectMake(50, 100, 200, 200)];
[self.view addSubview:self.yKNoteEventHandingView];
//Button
[self.ykNoteEventHandlingButton setFrame:CGRectMake(60, 60, 100, 100)];
[self.yKNoteEventHandingView addSubview:self.ykNoteEventHandlingButton];
}
#pragma mark - event
- (void)ykNoteEventHandlingGreenButtonDidTouchUpInside:(UIButton *)button {
NSLog(@"%s \n %@", __PRETTY_FUNCTION__, button);
}
#pragma mark - getter
- (YKNoteEventHandingView *)yKNoteEventHandingView {
if (_yKNoteEventHandingView == nil) {
_yKNoteEventHandingView = [[YKNoteEventHandingView alloc] init];
_yKNoteEventHandingView.backgroundColor = [UIColor redColor];
}
return _yKNoteEventHandingView;
}
- (YKNoteEventHandlingButton *)ykNoteEventHandlingButton {
if (_ykNoteEventHandlingButton == nil) {
_ykNoteEventHandlingButton = [[YKNoteEventHandlingButton alloc] init];
_ykNoteEventHandlingButton.backgroundColor = [UIColor greenColor];
[_ykNoteEventHandlingButton addTarget:nil action:@selector(ykNoteEventHandlingGreenButtonDidTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
}
return _ykNoteEventHandlingButton;
}
//Button的target設(shè)置為nil的時(shí)候,執(zhí)行了YKNoteEventHandlingButton中的方法侥衬,說明target為nil的時(shí)候事件沿著響應(yīng)者鏈傳遞了
-[YKNoteEventHandlingButton ykNoteEventHandlingGreenButtonDidTouchUpInside:]
<YKNoteEventHandlingButton: 0x100224950; baseClass = UIButton; frame = (60 60; 100 100); opaque = NO; layer = <CALayer: 0x17002a1a0>>
//注釋掉Button中的方法诗祸。輸出內(nèi)容如下跑芳,說明事件沿著響應(yīng)者鏈向上傳遞了。
-[YKNoteEventHandingView ykNoteEventHandlingGreenButtonDidTouchUpInside:]
<YKNoteEventHandlingButton: 0x10030fe40; baseClass = UIButton; frame = (60 60; 100 100); opaque = NO; layer = <CALayer: 0x17003d520>>
//注釋掉Button和View中的方法直颅。輸出內(nèi)容如下博个,說明事件沿著響應(yīng)者鏈向上傳遞了。
-[YKNoteEventHandingViewController ykNoteEventHandlingGreenButtonDidTouchUpInside:]
<YKNoteEventHandlingButton: 0x100402fd0; baseClass = UIButton; frame = (60 60; 100 100); opaque = NO; layer = <CALayer: 0x1740315a0>>
3. 將事件傳遞給兄弟視圖
假設(shè)有下圖所示的布局功偿,我們希望點(diǎn)擊view C的時(shí)候view B響應(yīng)事件盆佣,而點(diǎn)擊View D和View E的時(shí)候正常響應(yīng)。這個(gè)時(shí)候通過重寫view C的hittest可以解決這個(gè)問題械荷,在C的hittest里面直接返回nil就行了共耍。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%s", __PRETTY_FUNCTION__);
UIView *hitTestView = [super hitTest:point withEvent:event];
if (hitTestView == self) {
return nil;
}
return hitTestView;
}
4. 將事件傳遞給子視圖
如下圖,banner為CollectionView中的一個(gè)樓層吨瞎,CollectionViewCell中有個(gè)scrollView痹兜,scrollView中為圖片,現(xiàn)在將cell的寬度縮小一半(變?yōu)樗{(lán)色框部分)颤诀,設(shè)置cell和scrollview的clipsToBounds為NO字旭,現(xiàn)在在右側(cè)處滑動(dòng),scrollview中的圖片顯然不會(huì)滑動(dòng)崖叫,因?yàn)椴粷M足pointInside:withEvent:
遗淳,這時(shí)只需要修改cell的- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
方法,返回scrollview即可归露。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitTestView = [super hitTest:point withEvent:event];
if (hitTestView == nil) {
hitTestView = self.scrollView;
}
return hitTestView;
}
參考: