一張UI繼承關(guān)系圖示
iOS中的幾種常見事件
這篇文章只討論觸摸事件。對于觸摸事件UIResponder內(nèi)部提供了以下方法來處理事件:
事件對象在UIEvent
UIEvent.h文件中,我們可以看到有一個UIEventType類型的屬性矮嫉,這個屬性表示了當(dāng)前的響應(yīng)事件類型癞埠。分別有多點觸控芽世、搖一搖以及遠(yuǎn)程操作(在iOS之后新增了3DTouch事件類型)。在一個用戶點擊事件處理過程中藻三,UIEvent對象是唯一的洪橘。點擊對象UITouch
UITouch表示單個點擊,其類文件中存在枚舉類型UITouchPhase的屬性棵帽,用來表示當(dāng)前點擊的狀態(tài)熄求。這些狀態(tài)包括點擊開始、移動逗概、停止不動弟晚、結(jié)束和取消五個狀態(tài)。每次點擊發(fā)生的時候逾苫,點擊對象都放在一個集合中傳入UIResponder的回調(diào)方法中卿城,我們通過集合中對象獲取用戶點擊的位置。
其中通過- (CGPoint)locationInView:(nullable UIView *)view獲取當(dāng)前點擊坐標(biāo)點隶垮,- (CGPoint)previousLocationInView:(nullable UIView *)view獲取上個點擊位置的坐標(biāo)點藻雪。
觸摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
其中只有在程序強(qiáng)制退出或者來電時,取消點擊事件才會調(diào)用狸吞。
加速計事件 (搖一搖)
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
遠(yuǎn)程控制事件
額外配件如耳機(jī)上的音視頻播放按鍵所觸發(fā)的事件(視頻播放勉耀、下一首)
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
事件鏈
- 當(dāng)我們用手指輕觸屏幕,iPhone OS會將它識別為一組觸摸對象蹋偏,并將它們封裝在UITouch和UIEvent形式的實例便斥,消息循環(huán)(runloop)會接收到觸摸事件并放入當(dāng)前應(yīng)用程序的事件隊列中。
- 負(fù)責(zé)管理應(yīng)用程序的UIApplication單件對象將事件從隊列的頂部取出威始,找到當(dāng)前運行的程序枢纠,典型情況下,它會將事件發(fā)送給應(yīng)用程序的鍵盤焦點窗口—即擁有當(dāng)前用戶事件焦點的窗口黎棠,然后代表該窗口的UIWindow對象晋渺。
- UIWindow對象接受到事件開始進(jìn)行最優(yōu)響應(yīng)視圖查詢的過程(逆序遍歷subviews,后加載的先遍歷)脓斩。
- UIView對象并不一定會把事件傳遞給每一個子view木西,因為UIView是通過hitTest方法來判斷點擊事件發(fā)生在哪個子view上面的,會采用逆序查詢也就是優(yōu)先查詢后加載的子試圖随静,這樣做也是為了優(yōu)化查找速度八千,畢竟后addSubview的視圖在上易于命中吗讶。如果它第一個hitTest就命中了的話,這個事件就不會再被傳遞給其他子試圖了恋捆。
舉個例子:
就像上圖那樣照皆,點擊了紅色的View,
如果先加載藍(lán)色View沸停,后加載紅色UIView 傳遞過程是這樣的:
UIApplication對象——>UIWindow對象——>rootVC.view對象——>redview對象
如果先加載紅色View膜毁,后加載藍(lán)色UIView 傳遞過程是這樣的:
UIApplication對象——>UIWindow對象——>rootVC.view對象——>blueview對象——>redview對象
//************華麗分割線 便于閱讀***********
事件的傳遞其實就是在事件產(chǎn)生與分發(fā)之后如何尋找最優(yōu)響應(yīng)視圖的一個過程。其中涉及到了UIView中的兩個方法(可以重寫)愤钾,當(dāng)hitTest返回YES才會調(diào)用這個View的 Touch事件爽茴,因為如果返回NO,則當(dāng)前View被排除在相應(yīng)鏈之外了绰垂。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
//判斷當(dāng)前點擊事件是否存在最優(yōu)響應(yīng)者(First Responder)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
//判斷當(dāng)前點擊是否在控件的Bounds之內(nèi)
視圖命中查找流程
1.調(diào)用hitTest方法進(jìn)行最優(yōu)響應(yīng)視圖查詢
hidden = YES
userInteractionEnabled = NO
alpha < 0.01
以上三種情況會使該方法返回nil,即當(dāng)前視圖下無最優(yōu)響應(yīng)視圖
2.hitTest方法內(nèi)部會調(diào)用pointInside方法對點擊點進(jìn)行是否在當(dāng)前視圖bounds內(nèi)進(jìn)行判斷火焰,如果超出bounds劲装,hitTest則返回nil。 未超出范圍則進(jìn)行步驟3
3.對當(dāng)前視圖下的subviews采取逆序上述1 2步驟查詢最優(yōu)響應(yīng)視圖昌简。如果hitTest返回了對應(yīng)視圖則說明在當(dāng)前視圖層級下有最優(yōu)響應(yīng)視圖占业,可能為self或者其subview,這個要看具體返回纯赎。
如何看到這一切呢谦疾?
我們可以重寫view的-(UIView )hitTest:(CGPoint)point withEvent:(UIEvent)event方法來測試
#import "UIView+MYtes.h"
#import <objc/runtime.h>
@implementation UIView (MYtes)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL oriSEL = @selector(hitTest:withEvent:);
SEL swiSEL = @selector(wcq_hitTest:withEvent:);
Method oriMethod = class_getInstanceMethod(class, oriSEL);
Method swiMethod = class_getInstanceMethod(class, swiSEL);
BOOL didAddMethod = class_addMethod(class, oriSEL,
method_getImplementation(swiMethod),
method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(class,
swiSEL,
method_getImplementation(oriMethod),
method_getTypeEncoding(oriMethod));
}else {
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
- (UIView *)wcq_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ %s",[self class], __PRETTY_FUNCTION__);
return [self wcq_hitTest:point withEvent:event];
}
然后我們分別新建三個UIView的子類: AView、BView犬金、CView并依次按順序添加到ViewController上
然后我們依次點擊A
念恍、B
視圖看下hitTes
調(diào)用順序是否和預(yù)期一致
響應(yīng)者鏈
介紹響應(yīng)者鏈之前先介紹下響應(yīng)者對象
響應(yīng)者對象:是可以響應(yīng)事件并對其進(jìn)行處理的對象。UIResponder是所有響應(yīng)者對象的基類晚顷,它不僅為事件處理峰伙,而且也為常見的響應(yīng)者行為定義編程接口。UIApplication该默、UIView瞳氓、和所有從UIView派生出來的UIKit類(包括UIWindow)都直接或間接地繼承自UIResponder類。
第一響應(yīng)者是應(yīng)用程序中當(dāng)前負(fù)責(zé)接收觸摸事件的響應(yīng)者對象(通常是一個UIView對象)栓袖。
響應(yīng)者鏈:由一系列“下一個響應(yīng)者”組成
其順序如下:
- 1.iOS系統(tǒng)在處理事件時匣摘,通過UIApplication對象和每個UIWindow對象的sendEvent:方法將事件以消息的形式分發(fā)給具體處理此事件的第一響應(yīng)者,使其有機(jī)會首先處理事件裹刮。如果第一響應(yīng)者沒有進(jìn)行處理音榜,第一響應(yīng)者將事件將處理事件的責(zé)任傳遞給下一個,更高級的對象,即當(dāng)前responder對象的nextResponder必指。
- 2.UIView的nextResponder屬性囊咏,如果有管理此view的UIViewController對象,則為此UIViewController對象;否則nextResponder即為其superview
UIViewController的nextResponder屬性為其管理view的superview.
UIWindow的nextResponder屬性為UIApplication對象梅割。
UIApplication的nextResponder屬性為nil霜第。 - 3.類似地,視圖層次中的每個后續(xù)視圖如果不處理事件都首先傳遞給它的視圖控制器(如果有的話)户辞,然后是它的父視圖泌类。
- 4.最上層的容器視圖將事件傳遞給UIWindow對象。
- 5.UIWindow對象將事件傳遞給UIApplication單件對象底燎。
- 6.如果應(yīng)用程序找不到能夠處理事件的響應(yīng)者對象刃榨,則丟棄該事件。
程序?qū)ふ夷軌蛱幚硎录膶ο笏裕录驮陧憫?yīng)者鏈中向上傳遞枢希。
//******************* 華麗的分割線 ****************
系統(tǒng)先調(diào)用pointInSide: WithEvent:判斷當(dāng)前視圖以及這些視圖的子視圖是否能接收這次點擊事件,然后在調(diào)用hitTest: withEvent:依次獲取處理這個事件的所有視圖對象朱沃,在獲取所有的可處理事件對象后苞轿,開始調(diào)用這些對象的touches回調(diào)方法
在自定義View中重寫 touchesBegan方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UIResponder * next = [self nextResponder];
NSMutableString * prefix = @"".mutableCopy;
while (next != nil) {
NSLog(@"%@%@", prefix, [next class]);
[prefix appendString: @"--"];
next = [next nextResponder];
}
}
通過這張圖我們可以看到響應(yīng)者鏈的組成。
需要注意的是:viewController.m文件中重寫touchBegan:withEvent:方法逗物,相當(dāng)于處理的是viewController的觸摸事件搬卒,想處理自定義View的觸摸事件,必須在自定義UIView中重寫touchBegan:withEvent:方法翎卓,兩者不是一回事契邀,但是都是繼承自UIResponder 。
跟UIResponder相關(guān)其他值得注意的地方
UIApplication對象和每個UIWindow對象都在sendEvent:方法(兩個類都聲明了這個方法)中派發(fā)事件失暴。由于這些方法是事件進(jìn)入應(yīng)用程序的通道坯门,所以,您可以從UIApplication或UIWindow派生出子類锐帜,重載其sendEvent:方法田盈,實現(xiàn)對事件的監(jiān)控或執(zhí)行特殊的事件處理。但是缴阎,大多數(shù)應(yīng)用程序都不需要這樣做允瞧。
在一定的時間內(nèi)關(guān)閉事件的傳遞。應(yīng)用程序可以調(diào)用UIApplication的beginIgnoringInteractionEvents方法蛮拔,并在隨后調(diào)用endIgnoringInteractionEvents方法來實現(xiàn)這個目的述暂。前一個方法使應(yīng)用程序完全停止接收觸摸事件消息,第二個方法則重啟消息的接收建炫。某些時候畦韭,當(dāng)您的代碼正在執(zhí)行動畫時,可能希望關(guān)閉事件的傳遞肛跌。
在view添加單擊手勢之后艺配,原來的touchesEnded方法就無效了察郁。touchesBegin 還是生效的。
-
巧妙利用nextResponder獲得當(dāng)前頁面的控制容器
@implementation UIView (ParentController) -(UIViewController*)parentController{ UIResponder *responder = [self nextResponder]; while (responder) { if ([responder isKindOfClass:[UIViewController class]]) { return (UIViewController*)responder; } responder = [responder nextResponder]; } return nil; } @end
inputAccessoryView
我們在使用UITextView和UITextField的時候转唉,可以通過它們的inputAccessoryView屬性給輸入時呼出的鍵盤加一個附屬視圖皮钠,通常是UIToolBar,用于回收鍵盤赠法。 但是當(dāng)我們要操作的視圖不是UITextView或UITextField的時候麦轰,inputAccessoryView就變成了readonly的。 這時我們?nèi)绻€想再加inputAccessoryView砖织,按API中的說法款侵,就需要新建一個該視圖的子類,并重新聲明inputAccessoryView屬性為readwrite的侧纯。比如我們要實現(xiàn)點擊一個tableView的一行時新锈,呼出一個UIPickerView,并且附加一個用于回收PickerView的toolbar眶熬。因此我們自建一個UITableViewCell類壕鹉,并聲明inputAccessoryView和inputView為readwrite的,并且重寫它們的get方法聋涨,這樣在某個tableviewcell變成第一響應(yīng)者時,它就會自動呼出inputView和inputAccessoryView负乡;
@interface MyTableViewCell : UITableViewCell<UIPickerViewDelegate,UIPickerViewDataSource>
{
UIToolbar *_inputAccessoryView;
UIPickerView *_inputView;
}
@property(strong,nonatomic,readwrite) UIToolbar *inputAccessoryView;
@property(strong,nonatomic,readwrite) UIPickerView *inputView;
@end
-(UIToolbar *)inputAccessoryView
{
if(!_inputAccessoryView)
{
UIToolbar *toolBar = [[UIToolbar alloc]initWithFrame:CGRectMake(0, 0, 320, 44)];
// UIBarButtonItem *right = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonItem target:self action:@selector(dodo)];
UIBarButtonItem *right = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dodo)];
toolBar.items = [NSArray arrayWithObject:right];
return toolBar;
}
return _inputAccessoryView;
}
-(UIPickerView *)inputView
{
if(!_inputView)
{
UIPickerView * pickView = [[UIPickerView alloc]initWithFrame:CGRectMake(0, 200, 320, 200)];
pickView.delegate =self;
pickView.dataSource = self;
pickView.showsSelectionIndicator = YES;
return pickView;
}
return _inputView;
}
手動把它變成第一響應(yīng)者牍白。(難道cell被選中時不是第一響應(yīng)者?)
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
[cell becomeFirstResponder];
}
運行結(jié)果:
- 巧妙重寫pointInside實現(xiàn)點擊圓形區(qū)域判斷
實現(xiàn)過程解析:
-
1.自定義一個View設(shè)置其顏色為橙色抖棘,高度為200茂腥,并設(shè)置
self.layer.cornerRadius = 100; self.clipsToBounds = YES;
-
2.在pointInside中創(chuàng)建一個 UIBezierPath,通過 [path containsPoint: point]來判斷當(dāng)前的點是否在圓內(nèi)切省,pointInside的返回值直接影響到touchesEnded的調(diào)用最岗,如果返回NO是不會調(diào)用touchesEnded事件的。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { UIBezierPath * path = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(0, 0, 200, 200)]; return [path containsPoint: point]; }
-
3.在touchesEnded事件中顯示 UIAlertView即可朝捆。
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { UIAlertView * alert = [[UIAlertView alloc] initWithTitle: nil message: @"點擊圓形視圖" delegate: nil cancelButtonTitle: @"確認(rèn)" otherButtonTitles: nil]; [alert show]; }
參考文章:
iOS開發(fā) - 事件傳遞響應(yīng)鏈
iOS編程中的快遞小哥-Responder Chain(響應(yīng)鏈)
IOS 應(yīng)用事件的傳遞分析