IOS事件学习总结
iOS事件機制(一)
http://www.cnblogs.com/zhw511006/p/3517248.html本篇內容將圍繞iOS中事件及其傳遞機制進行學習和分析。在iOS中,事件分為三類:
觸控事件(單點、多點觸控以及各種手勢操作)
傳感器事件(重力、加速度傳感器等)
遠程控制事件(遠程遙控iOS設備多媒體播放等)
這三類事件共同構成了iOS設備豐富的操作方式和使用體驗,本次就首先來針對第一類事件:觸控事件,進行學習和分析。
Gesture Recognizers
Gesture Recognizers是一類手勢識別器對象,它可以附屬在你指定的View上,并且為其設定指定的手勢操作,例如是點擊、滑動或者是拖拽。當觸控事件 發生時,設置了Gesture Recognizers的View會先通過識別器去攔截觸控事件,如果該觸控事件是事先為View設定的觸控監聽事件,那么Gesture Recognizers將會發送動作消息給目標處理對象,目標處理對象則對這次觸控事件進行處理,先看看如下流程圖。
在iOS中,View就是我們在屏幕上看到的各種UI控件,當一個觸控事件發生時,Gesture Recognizers會先獲取到指定的事件,然后發送動作消息(action message)給目標對象(target),目標對象就是ViewController,在ViewController中通過事件方法完成對該事件的處理。Gesture Recognizers能設置諸如單擊、滑動、拖拽等事件,通過Action-Target這種設計模式,好處是能動態為View添加各種事件監聽,而不用去實現一個View的子類去完成這些功能。
以上過程就是我們在開發中在方法中常見的設置action和設置target,例如為UIButton設置監聽事件等。
常用手勢識別類
在UIKit框架中,系統為我們事先定義好了一些常用的手勢識別器,包括點擊、雙指縮放、拖拽、滑動、旋轉以及長按。通過這些手勢識別器我們可以構造豐富的操作方式。
在上表中可以看到,UIKit框架中已經提供了諸如UITapGestureRecognizer在內的六種手勢識別器,如果你需要實現自定義的手勢識別器,也可以通過繼承UIGestureRecognizer類并重寫其中的方法來完成,這里我們就不詳細討論了。
每一個Gesture Recognizer關聯一個View,但是一個View可以關聯多個Gesture Recognizer,因為一個View可能還能響應多種觸控操作方式。當一個觸控事件發生時,Gesture Recognizer接收一個動作消息要先于View本身,結果就是Gesture Recognizer作為View處理觸控事件的代表,或者叫代理。當Gesture Recognizer接收到指定的事件時,它就會發送一條動作消息(action message)給ViewController并處理。
連續和不連續動作
觸控動作同時分為連續動作(continuous)和不連續動作(discrete),連續動作例如滑動和拖拽,它會持續一小段時間,而不連續動作例如單擊,它瞬間就會完成,在這兩類事件的處理上又稍有不同。對于不連續動作,Gesture Recognizer只會給ViewContoller發送一個單一的動作消息(action message),而對于連續動作,Gesture Recognizer會發送多條動作消息給ViewController,直到所有的事件都結束。
為一個View添加GestureRecognizer有兩種方式,一種是通過InterfaceBuilder實現,另一種就是通過代碼實現,我們看看通過代碼來如何實現。
MyViewContoller.m
- (void)viewDidLoad {
? ? ?[super viewDidLoad];
? ? ?// 創建并初始化手勢對象
? ? ?UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
? ? ? ? ? initWithTarget:self action:@selector(respondToTapGesture:)];
? ? ?// 指定操作為單擊一次
? ? ?tapRecognizer.numberOfTapsRequired = 1;
? ? ?// 為當前View添加GestureRecognizer
? ? ?[self.view addGestureRecognizer:tapRecognizer];
? ? ?// ...
}
通過上述代碼,我們實現了為當前MyViewController的View添加一個單擊事件,首先構造了UITapGestureRecognizer對象,指定了target為當前ViewController本身,action就是后面自己實現的處理方法,這里就呼應了前文提到的Action-Target模式。
在事件處理過程中,這兩種方式所處的狀態又各有不同,首先,所有的觸控事件最開始都是處于可用狀態(Possible),對應UIKit里面的UIGestureRecognizerStatePossible類,如果是不連續動作事件,則狀態只會從Possible轉變為已識別狀態(Recognized,UIGestureRecognizerStateRecognized)或者是失敗狀態(Failed,UIGestureRecognizerStateFailed)。例如一次成功的單擊動作,就對應了Possible-Recognized這個過程。
如果是連續動作事件,如果事件沒有失敗并且連續動作的第一個動作被成功識別(Recognized),則從Possible狀態轉移到Began(UIGestureRecognizerStateBegan)狀態,這里表示連續動作的開始,接著會轉變為Changed(UIGestureRecognizerStateChanged)狀態,在這個狀態下會不斷循環的處理連續動作,直到動作執行完成變轉變為Recognized已識別狀態,最終該動作會處于完成狀態(UIGestureRecognizerStateEnded),另外,連續動作事件的處理狀態會從Changed狀態轉變為Canceled(UIGestureRecognizerStateCancelled)狀態,原因是識別器認為當前的動作已經不匹配當初對事件的設定了。每個動作狀態的變化,Gesture Recognizer都會發送消息(action message)給Target,也就是ViewController,它可以根據這些動作消息進行相應的處理。例如一次成功的滑動手勢動作就包括按下、移動、抬起的過程,分別對應了Possible-Began-Changed-Recognized這個過程。
UITouch & UIEvent
在屏幕上的每一次動作事件都是一次Touch,在iOS中用UITouch對象表示每一次的觸控,多個Touch組成一次Event,用UIEvent來表示一次事件對象。
在上述過程中,完成了一次雙指縮放的事件動作,每一次手指狀態的變化都對應事件動作處理過程中得一個階段。通過Began-Moved-Ended這幾個階段的動作(Touch)共同構成了一次事件(Event)。在事件響應對象UIResponder中有對應的方法來分別處理這幾個階段的事件。
touchesBegan:withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:
后面的參數分別對應UITouchPhaseBegan、UITouchPhaseMoved、UITouchPhaseEnded、UITouchPhaseCancelled這幾個類。用來表示不同階段的狀態。
事件傳遞
如上圖,iOS中事件傳遞首先從App(UIApplication)開始,接著傳遞到Window(UIWindow),在接著往下傳遞到View之前,Window會將事件交給GestureRecognizer,如果在此期間,GestureRecognizer識別了傳遞過來的事件,則該事件將不會繼續傳遞到View去,而是像我們之前說的那樣交給Target(ViewController)進行處理。
響應者鏈(Responder Chain)
通常,一個iOS應用中,在一塊屏幕上通常有很多的UI控件,也就是有很多的View,那么當一個事件發生時,如何來確定是哪個View響應了這個事件呢,接下來我們就一起來看看。
尋找hit-test view
什么是hit-test view呢?簡單來說就是你觸發事件所在的那個View,尋找hit-test view的過程就叫做Hit-Testing。那么,系統是如何來執行Hit-Testing呢,首先假設現在有如下這么一個UI布局,一種有ABCDE五個View。
假設一個單擊事件發生在了View D里面,系統首先會從最頂層的View A開始尋找,發現事件是在View A或者其子類里面,那么接著從B和C找,發現事件是在C或者其子類里面,那么接著到C里面找,這時發現事件是在D里面,并且D已經沒有子類了,那么hit-test view就是View D啦。
響應者對象(Responsder Object)
響應者對象是能夠響應并且處理事件的對象,UIResponder是所有響應者對象的父類,包括UIApplication、UIView和UIViewController都是UIResponder的子類。也就意味著所有的View和ViewController都是響應者對象。
第一響應者(First Responder)
第一響應者是第一個接收事件的View對象,我們在Xcode的Interface Builder畫視圖時,可以看到視圖結構中就有First Responder。
這里的First Responder就是UIApplication了。另外,我們可以控制一個View讓其成為First Responder,通過實現 canBecomeFirstResponder方法并返回YES可以使當前View成為第一響應者,或者調用View的becomeFirstResponder方法也可以,例如當UITextField調用該方法時會彈出鍵盤進行輸入,此時輸入框控件就是第一響應者。
事件傳遞機制
如上所說,,如果hit-test view不能處理當前事件,那么事件將會沿著響應者鏈(Responder Chain)進行傳遞,知道遇到能處理該事件的響應者(Responsder Object)。通過下圖,我們來看看兩種不同情況下得事件傳遞機制。
左邊的情況,接收事件的initial view如果不能處理該事件并且她不是頂層的View,則事件會往它的父View進行傳遞。initial view的父View獲取事件后如果仍不能處理,則繼續往上傳遞,循環這個過程。如果頂層的View還是不能處理這個事件的話,則會將事件傳遞給它們的ViewController,如果ViewController也不能處理,則傳遞給Window(UIWindow),此時Window不能處理的話就將事件傳遞給Application(UIApplication),最后如果連Application也不能處理,則廢棄該事件。
右邊圖的流程唯一不同就在于,如果當前的ViewController是由層級關系的,那么當子ViewController不能處理事件時,它會將事件繼續往上傳遞,直到傳遞到其Root ViewController,后面的流程就跟之前分析的一樣了。
這就是事件響應者鏈的傳遞機制,通過這些內容,我們可以更深入的了解事件在iOS中得傳遞機制,對我們在實際開發中更好的理解事件操作的原理有很大的幫助,也對我們實現復雜布局進行事件處理時增添了多一份的理解。
總結
通過前面的內容分析,我們已經學習并了解了如下內容:
Gesture Recognizers,是用來控制手勢識別的過程和方法,并且其通過Action-Target模式與ViewController的通信的方式。連續和不連續手勢動作情況下GestureRecognizer的狀態轉變。
UITouch和UIEvent對象,他們都是UIKit中來進行事件處理的對象,多個UITouch對象構成一個UIEvent對象,重寫相應的方法可以控制和處理事件各個階段的操作。
系尋找hit-test view的方式、事件傳遞機、制響應者鏈
后記:本篇是iOS事件傳遞機制的上篇,下篇將繼續討論多點觸控事件和手勢操作的內容!
========
iOS事件分發機制(一) hit-Testing
http://ios.jobbole.com/81864/??
iOS中的事件大概分為三種,分別是 Milti-Touch Events, Motion Events 和Remote Control Events(events for controlling multimedia)。
本文將主要針對TouchEvents的分發,做一個詳細的介紹。先拋出一個問題,文章的后續部分會對問題進行解答:iOS7原生的自帶NavigationController可以實現從最左側拖動PopViewController(大約13pt),不管當前可見的ViewController有沒有其他的滑動手勢或者事件,這是為什么?如何實現。
我們已經處理過太多觸摸事件了,比如按鈕的點擊事件,一些View的手勢等等。那到底我們點一下屏幕,當前的View是如何知道他被點擊了呢,這個就要通過HitTest來確定了
每當我們點擊了一下iOS設備的屏幕,UIKit就會生成一個事件對象UIEvent,然后會把這個Event分發給當前active的app(官方原文說:Then it places the event object in the active app’s event queue.)
告知當前活動的app有事件之后,UIApplication 單例就會從事件隊列中去取最新的事件,然后分發給能夠處理該事件的對象。UIApplication 獲取到Event之后,Application就糾結于到底要把這個事件傳遞給誰,這時候就要依靠HitTest來決定了。
iOS中,hit-Testing的作用就是找出這個觸摸點下面的View是什么,HitTest會檢測這個點擊的點是不是發生在這個View上,如果是的話,就會去遍歷這個View的subviews,直到找到最小的能夠處理事件的view,如果整了一圈沒找到能夠處理的view,則返回自身。來一個簡單的圖說明一下
假設我們現在點擊到了圖中的E,hit-testing將進行如下步驟的檢測(不包含重寫hit-test并且返回非默認View的情況)
1、觸摸點在ViewA內,所以檢查ViewA的Subview B、C
2、觸摸點不在ViewB內,觸摸點在ViewC內部,所以檢查ViewC的Subview D、E
3、觸摸點不在ViewD內,觸摸點發生在ViewE內部,并且ViewE沒有subview,所以ViewE屬于ViewA中包含這個點的最小單位,所以ViewE變成了該次觸摸事件的hit-TestView
PS.
1、默認的hit-testing順序是按照UIView中Subviews的逆順序
2、如果View的同級別Subview中有重疊的部分,則優先檢查頂部的Subview,如果頂部的Subview返回nil, 再檢查底部的Subview
3、Hit-Test也是比較聰明的,檢測過程中有這么一點,就是說如果點擊沒有發生在某View中,那么該事件就不可能發生在View的Subview中,所以檢測過程中發現該事件不在ViewB內,也直接就不會檢測在不在ViewF內。也就是說,如果你的Subview設置了clipsToBounds=NO,實際顯示區域可能超出了superView的frame,你點擊超出的部分,是不會處理你的事件的,就是這么任性!
Hit-Test的檢查機制如上所示,當確定了Hit-TestView時,如果當前的application沒有忽略觸摸事件 (UIApplication:isIgnoringInteractionEvents),則application就會去分發事件(sendEvent:->keywindow:sendEvent:)
UIView中提供兩個方法用來確定hit-testing View,如下所示 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; // default returns YES if point is in bounds
當一個View收到hitTest消息時,會調用自己的pointInside:withEvent:方法,如果pointInside返回YES,則表明觸摸事件發生在我自己內部,則會遍歷自己的所有Subview去尋找最小單位(沒有任何子view)的UIView,如果當前View.userInteractionEnabled = NO,enabled=NO(UIControl),或者alpha<=0.01, hidden等情況的時候,hitTest就不會調用自己的pointInside了,直接返回nil,然后系統就回去遍歷兄弟節點。簡而言之,可以寫成這樣
Objective-C
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
? ? if (self.alpha <= 0.01 || !self.userInteractionEnabled || self.hidden) {
? ? ? ? return nil;
? ? }
? ? BOOL inside = [self pointInside:point withEvent:event];
? ? UIView *hitView = nil;
? ? if (inside) {
? ? ? ? NSEnumerator *enumerator = [self.subviews reverseObjectEnumerator];
? ? ? ? for (UIView *subview in enumerator) {
? ? ? ? ? ? hitView = [subview hitTest:point withEvent:event];
? ? ? ? ? ? if (hitView) {
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? if (!hitView) {
? ? ? ? ? ? hitView = self;
? ? ? ? }
? ? ? ? return hitView;
? ? } else {
? ? ? ? return nil;
? ? }
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
? ? if (self.alpha <= 0.01 || !self.userInteractionEnabled || self.hidden) {
? ? ? ? return nil;
? ? }
? ? BOOL inside = [self pointInside:point withEvent:event];
? ? UIView *hitView = nil;
? ? if (inside) {
? ? ? ? NSEnumerator *enumerator = [self.subviews reverseObjectEnumerator];
? ? ? ? for (UIView *subview in enumerator) {
? ? ? ? ? ? hitView = [subview hitTest:point withEvent:event];
? ? ? ? ? ? if (hitView) {
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? if (!hitView) {
? ? ? ? ? ? hitView = self;
? ? ? ? }
? ? ? ? return hitView;
? ? } else {
? ? ? ? return nil;
? ? }
}
hit-Test 是事件分發的第一步,就算你的app忽略了事件,也會發生hit-Test。確定了hit-TestView之后,才會開始進行下一步的事件分發。
我們可以利用hit-Test做一些事情,比如我們點擊了ViewA,我們想讓ViewB響應,這個時候,我們只需要重寫View’s hitTest方法,返回ViewB就可以了,雖然可能用不到,但是偶爾還是會用到的。大概代碼如下:
Objective-C
@interface STPView : UIView
@end
@implementation STPView
- (instancetype)initWithFrame:(CGRect)frame {
? ? self = [super initWithFrame:frame];
? ? if (self) {
? ? ? ? UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
? ? ? ? button.frame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame) / 2);
? ? ? ? button.tag = 10001;
? ? ? ? button.backgroundColor = [UIColor grayColor];
? ? ? ? [button setTitle:@"Button1" forState:UIControlStateNormal];
? ? ? ? [self addSubview:button];
? ? ? ? [button addTarget:self action:@selector(_buttonActionFired:) forControlEvents:UIControlEventTouchDown];
? ? ? ??
? ? ? ? UIButton *button2 = [UIButton buttonWithType:UIButtonTypeCustom];
? ? ? ? button2.frame = CGRectMake(0, CGRectGetHeight(frame) / 2, CGRectGetWidth(frame), CGRectGetHeight(frame) / 2);
? ? ? ? button2.tag = 10002;
? ? ? ? button2.backgroundColor = [UIColor darkGrayColor];
? ? ? ? [button2 setTitle:@"Button2" forState:UIControlStateNormal];
? ? ? ? [self addSubview:button2];
? ? ? ? [button2 addTarget:self action:@selector(_buttonActionFired:) forControlEvents:UIControlEventTouchDown];
? ? }
? ? return self;
}
- (void)_buttonActionFired:(UIButton *)button {
? ? NSLog(@"=====Button Titled %@ ActionFired ", [button titleForState:UIControlStateNormal]);
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
? ? UIView *hitView = [super hitTest:point withEvent:event];
? ? if (hitView == [self viewWithTag:10001]) {
? ? ? ? return [self viewWithTag:10002];
? ? }
? ? return hitView;
}
@end
@interface STPView : UIView
?
@end
?
@implementation STPView
?
- (instancetype)initWithFrame:(CGRect)frame {
? ? self = [super initWithFrame:frame];
? ? if (self) {
? ? ? ? UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
? ? ? ? button.frame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame) / 2);
? ? ? ? button.tag = 10001;
? ? ? ? button.backgroundColor = [UIColor grayColor];
? ? ? ? [button setTitle:@"Button1" forState:UIControlStateNormal];
? ? ? ? [self addSubview:button];
? ? ? ? [button addTarget:self action:@selector(_buttonActionFired:) forControlEvents:UIControlEventTouchDown];
? ? ? ??
? ? ? ? UIButton *button2 = [UIButton buttonWithType:UIButtonTypeCustom];
? ? ? ? button2.frame = CGRectMake(0, CGRectGetHeight(frame) / 2, CGRectGetWidth(frame), CGRectGetHeight(frame) / 2);
? ? ? ? button2.tag = 10002;
? ? ? ? button2.backgroundColor = [UIColor darkGrayColor];
? ? ? ? [button2 setTitle:@"Button2" forState:UIControlStateNormal];
? ? ? ? [self addSubview:button2];
? ? ? ? [button2 addTarget:self action:@selector(_buttonActionFired:) forControlEvents:UIControlEventTouchDown];
? ? }
? ? return self;
}
?
- (void)_buttonActionFired:(UIButton *)button {
? ? NSLog(@"=====Button Titled %@ ActionFired ", [button titleForState:UIControlStateNormal]);
}
?
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
? ? UIView *hitView = [super hitTest:point withEvent:event];
? ? if (hitView == [self viewWithTag:10001]) {
? ? ? ? return [self viewWithTag:10002];
? ? }
? ? return hitView;
}
?
@end
大家可以試一試,上述代碼在點擊上面的按鈕的時候,實際會觸發下面按鈕的事件,不是經常用到,但是也算是漲姿勢了,這里給大家提供一個Category,來自STKit,這個category的目的就是方便的編寫hitTest方法,由于hitTest方法是override,而不是delegate,所以使用默認的實現方式就比較麻煩。Category如下
Objective-C
/**
?* @abstract hitTestBlock
?*
?* @param 其余參數 參考UIView hitTest:withEvent:
?* @param returnSuper 是否返回Super的值。如果*returnSuper=YES,則代表會返回 super hitTest:withEvent:, 否則則按照block的返回值(即使是nil)
?*?
?* @discussion 切記,千萬不要在這個block中調用self hitTest:withPoint,否則則會造成遞歸調用。這個方法就是hitTest:withEvent的一個代替。
?*/
typedef UIView * (^STHitTestViewBlock)(CGPoint point, UIEvent *event, BOOL *returnSuper);
typedef BOOL (^STPointInsideBlock)(CGPoint point, UIEvent *event, BOOL *returnSuper);
@interface UIView (STHitTest)
/// althought this is strong ,but i deal it with copy
@property(nonatomic, strong) STHitTestViewBlock hitTestBlock;
@property(nonatomic, strong) STPointInsideBlock pointInsideBlock;
@end
/**
?* @abstract hitTestBlock
?*
?* @param 其余參數 參考UIView hitTest:withEvent:
?* @param returnSuper 是否返回Super的值。如果*returnSuper=YES,則代表會返回 super hitTest:withEvent:, 否則則按照block的返回值(即使是nil)
?*?
?* @discussion 切記,千萬不要在這個block中調用self hitTest:withPoint,否則則會造成遞歸調用。這個方法就是hitTest:withEvent的一個代替。
?*/
typedef UIView * (^STHitTestViewBlock)(CGPoint point, UIEvent *event, BOOL *returnSuper);
typedef BOOL (^STPointInsideBlock)(CGPoint point, UIEvent *event, BOOL *returnSuper);
?
@interface UIView (STHitTest)
/// althought this is strong ,but i deal it with copy
@property(nonatomic, strong) STHitTestViewBlock hitTestBlock;
@property(nonatomic, strong) STPointInsideBlock pointInsideBlock;
?
@end
Objective-C
@implementation UIView (STHitTest)
const static NSString *STHitTestViewBlockKey = @"STHitTestViewBlockKey";
const static NSString *STPointInsideBlockKey = @"STPointInsideBlockKey";
+ (void)load {
? ? method_exchangeImplementations(class_getInstanceMethod(self, @selector(hitTest:withEvent:)),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?class_getInstanceMethod(self, @selector(st_hitTest:withEvent:)));
? ? method_exchangeImplementations(class_getInstanceMethod(self, @selector(pointInside:withEvent:)),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?class_getInstanceMethod(self, @selector(st_pointInside:withEvent:)));
}
- (UIView *)st_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
? ? NSMutableString *spaces = [NSMutableString stringWithCapacity:20];
? ? UIView *superView = self.superview;
? ? while (superView) {
? ? ? ? [spaces appendString:@"----"];
? ? ? ? superView = superView.superview;
? ? }
? ? NSLog(@"%@%@:[hitTest:withEvent:]", spaces, NSStringFromClass(self.class));
? ? UIView *deliveredView = nil;
? ? // 如果有hitTestBlock的實現,則調用block
? ? if (self.hitTestBlock) {
? ? ? ? BOOL returnSuper = NO;
? ? ? ? deliveredView = self.hitTestBlock(point, event, &returnSuper);
? ? ? ? if (returnSuper) {
? ? ? ? ? ? deliveredView = [self st_hitTest:point withEvent:event];
? ? ? ? }
? ? } else {
? ? ? ? deliveredView = [self st_hitTest:point withEvent:event];
? ? }
// ? ?NSLog(@"%@%@:[hitTest:withEvent:] Result:%@", spaces, NSStringFromClass(self.class), NSStringFromClass(deliveredView.class));
? ? return deliveredView;
}
- (BOOL)st_pointInside:(CGPoint)point withEvent:(UIEvent *)event {
? ? NSMutableString *spaces = [NSMutableString stringWithCapacity:20];
? ? UIView *superView = self.superview;
? ? while (superView) {
? ? ? ? [spaces appendString:@"----"];
? ? ? ? superView = superView.superview;
? ? }
? ? NSLog(@"%@%@:[pointInside:withEvent:]", spaces, NSStringFromClass(self.class));
? ? BOOL pointInside = NO;
? ? if (self.pointInsideBlock) {
? ? ? ? BOOL returnSuper = NO;
? ? ? ? pointInside = ?self.pointInsideBlock(point, event, &returnSuper);
? ? ? ? if (returnSuper) {
? ? ? ? ? ? pointInside = [self st_pointInside:point withEvent:event];
? ? ? ? }
? ? } else {
? ? ? ? pointInside = [self st_pointInside:point withEvent:event];
? ? }
? ? return pointInside;
}
- (void)setHitTestBlock:(STHitTestViewBlock)hitTestBlock {
? ? objc_setAssociatedObject(self, (__bridge const void *)(STHitTestViewBlockKey), hitTestBlock, OBJC_ASSOCIATION_COPY);
}
- (STHitTestViewBlock)hitTestBlock {
? ? return objc_getAssociatedObject(self, (__bridge const void *)(STHitTestViewBlockKey));
}
- (void)setPointInsideBlock:(STPointInsideBlock)pointInsideBlock {
? ? objc_setAssociatedObject(self, (__bridge const void *)(STPointInsideBlockKey), pointInsideBlock, OBJC_ASSOCIATION_COPY);
}
- (STPointInsideBlock)pointInsideBlock {
? ? return objc_getAssociatedObject(self, (__bridge const void *)(STPointInsideBlockKey));
}
@end
@implementation UIView (STHitTest)
?
const static NSString *STHitTestViewBlockKey = @"STHitTestViewBlockKey";
const static NSString *STPointInsideBlockKey = @"STPointInsideBlockKey";
?
+ (void)load {
? ? method_exchangeImplementations(class_getInstanceMethod(self, @selector(hitTest:withEvent:)),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?class_getInstanceMethod(self, @selector(st_hitTest:withEvent:)));
? ? method_exchangeImplementations(class_getInstanceMethod(self, @selector(pointInside:withEvent:)),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?class_getInstanceMethod(self, @selector(st_pointInside:withEvent:)));
}
?
- (UIView *)st_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
? ? NSMutableString *spaces = [NSMutableString stringWithCapacity:20];
? ? UIView *superView = self.superview;
? ? while (superView) {
? ? ? ? [spaces appendString:@"----"];
? ? ? ? superView = superView.superview;
? ? }
? ? NSLog(@"%@%@:[hitTest:withEvent:]", spaces, NSStringFromClass(self.class));
? ? UIView *deliveredView = nil;
? ? // 如果有hitTestBlock的實現,則調用block
? ? if (self.hitTestBlock) {
? ? ? ? BOOL returnSuper = NO;
? ? ? ? deliveredView = self.hitTestBlock(point, event, &returnSuper);
? ? ? ? if (returnSuper) {
? ? ? ? ? ? deliveredView = [self st_hitTest:point withEvent:event];
? ? ? ? }
? ? } else {
? ? ? ? deliveredView = [self st_hitTest:point withEvent:event];
? ? }
// ? ?NSLog(@"%@%@:[hitTest:withEvent:] Result:%@", spaces, NSStringFromClass(self.class), NSStringFromClass(deliveredView.class));
? ? return deliveredView;
}
?
- (BOOL)st_pointInside:(CGPoint)point withEvent:(UIEvent *)event {
? ? NSMutableString *spaces = [NSMutableString stringWithCapacity:20];
? ? UIView *superView = self.superview;
? ? while (superView) {
? ? ? ? [spaces appendString:@"----"];
? ? ? ? superView = superView.superview;
? ? }
? ? NSLog(@"%@%@:[pointInside:withEvent:]", spaces, NSStringFromClass(self.class));
? ? BOOL pointInside = NO;
? ? if (self.pointInsideBlock) {
? ? ? ? BOOL returnSuper = NO;
? ? ? ? pointInside = ?self.pointInsideBlock(point, event, &returnSuper);
? ? ? ? if (returnSuper) {
? ? ? ? ? ? pointInside = [self st_pointInside:point withEvent:event];
? ? ? ? }
? ? } else {
? ? ? ? pointInside = [self st_pointInside:point withEvent:event];
? ? }
? ? return pointInside;
}
?
- (void)setHitTestBlock:(STHitTestViewBlock)hitTestBlock {
? ? objc_setAssociatedObject(self, (__bridge const void *)(STHitTestViewBlockKey), hitTestBlock, OBJC_ASSOCIATION_COPY);
}
?
- (STHitTestViewBlock)hitTestBlock {
? ? return objc_getAssociatedObject(self, (__bridge const void *)(STHitTestViewBlockKey));
}
?
- (void)setPointInsideBlock:(STPointInsideBlock)pointInsideBlock {
? ? objc_setAssociatedObject(self, (__bridge const void *)(STPointInsideBlockKey), pointInsideBlock, OBJC_ASSOCIATION_COPY);
}
?
- (STPointInsideBlock)pointInsideBlock {
? ? return objc_getAssociatedObject(self, (__bridge const void *)(STPointInsideBlockKey));
}
?
@end
代碼很簡單,就是利用iOS的runtime能力,在hitTest執行之前,插入了一個方法。如果有看不懂的,可以參考我以前的博客 iOS面向切面編程
現在回到我們開始提出的題目,其實題目很簡單,就是簡單的可以把題目轉換為
如果我們觸摸點的坐標 point.x < 13, 我們就讓hit-Test 返回NavigationController.view, 把所有的事件入口交給他,否則就返回super,該怎么處理怎么處理
這樣就能滿足我們的條件,即使當前的VC上面有ScrollView,但是由于點擊特定區域的時候,ScrollView根本得不到事件,所以系統會專心處理NavigationController的拖拽手勢,而不是ScrollView的事件,當沒有點擊特定區域的時候,NavigationController的手勢不會觸發,系統會專心處理ScrollView的事件,互不影響,大家可以嘗試實現,代碼量不多。
雖然iOS8新增了UIScreenEdgePanGestureRecognizer 手勢,但是單純的用這個手勢無法解決當前VC上面有ScrollView的問題,有關手勢方面的事件分發,之后的文章會對此進行說明,這里就不多說了。
當我們確定了HitTestView之后,我們的事件分發就正式開始了,如果hitTestView可以直接處理的,就處理,不能處理的,則交給 The Responder Chain/ GestureRecognizer。后續文章會對分發進行進一步說明。
附上一些測試查找hitTestView過程中打印的日志,可以觀察一下:
Objective-C
STPWindow:[hitTest:withEvent:]
----UIView:[hitTest:withEvent:]
--------STPView:[hitTest:withEvent:]
--------UICollectionView:[hitTest:withEvent:]
------------UIImageView:[hitTest:withEvent:]
------------UIImageView:[hitTest:withEvent:]
------------STDefaultRefreshControl:[hitTest:withEvent:]
------------STPFeedCell:[hitTest:withEvent:]
------------STPFeedCell:[hitTest:withEvent:]
----------------UIView:[hitTest:withEvent:]
--------------------UIImageView:[hitTest:withEvent:]
------------------------UIImageView:[hitTest:withEvent:]
------------------------UIView:[hitTest:withEvent:]
------------------------STImageView:[hitTest:withEvent:]
STPWindow:[hitTest:withEvent:]
----UIView:[hitTest:withEvent:]
--------STPView:[hitTest:withEvent:]
--------UICollectionView:[hitTest:withEvent:]
------------UIImageView:[hitTest:withEvent:]
------------UIImageView:[hitTest:withEvent:]
------------STDefaultRefreshControl:[hitTest:withEvent:]
------------STPFeedCell:[hitTest:withEvent:]
------------STPFeedCell:[hitTest:withEvent:]
----------------UIView:[hitTest:withEvent:]
--------------------UIImageView:[hitTest:withEvent:]
------------------------UIImageView:[hitTest:withEvent:]
------------------------UIView:[hitTest:withEvent:]
------------------------STImageView:[hitTest:withEvent:]
其中—-表示View的層次結構
========
ios開發 在UiView中添加點擊事件
雖然swift語言已經發布,但就目前而言,開發iosApp還是用objective c來完成,此處說的是objective c語言中的。
在app的開發過程中我們常常需要在一些非button中添加一些點擊事件,來實現我們想要的效果。比如做個下拉選項,我們希望點擊背景時選項視圖消失,或者我們點擊某個圖片(uiimageView)時跳轉到大圖頁面或者做其他操作。也許初學者會郁悶。。。這些怎么添加點擊事件呢?點擊事件不是只有button才能添加么?其實只要是繼承 uiview的空間,你都可以手動的添加一些點擊事件。
讓我們看看是怎么實現的吧。
UITapGestureRecognizer*tapGesture = [[UITapGestureRecognizer alloc]initWithTarget:selfaction:@selector(Actiondo:)];
? ? [uiview addGestureRecognizer:tapGesture];
其實理解起來也很簡單,就是新建一個UITapGestureRecognizer,這個是點擊事件,再將這個事件加到uiview中,繼承uiview的一般都有addGestureRecognizer這個方法。addGestureRecognizer方法就是用于添加點擊事件的。
我們再定義一個Actiondo的響應方法。
-(void)Actiondo:(id)sender{}
將我們需要的動作添加在其中就可以了。
好了,UITapGestureRecognizer的使用就是這樣了。趕快試一下吧!
========
UITextView 響應 鍵盤的return(完成鍵)
http://blog.sina.com.cn/u/1509658847UITextView <wbr>響應 <wbr>鍵盤的return(完成鍵)
UITextFieldDelegate代理里面響應return鍵的回調:textFieldShouldReturn:。
但是 UITextView的代理UITextViewDelegate 里面并沒有這樣的回調。
但是有別的方法可以實現:
UITextViewDelegate里面有這樣一個代理函數:
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
這個函數的最后一個參數text代表你每次輸入的的那個字,所以:
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{
? ? if ([text isEqualToString:@"\n"]){ //判斷輸入的字是否是回車,即按下return
? ? ? ? //在這里做你響應return鍵的代碼
? ? ? ? return NO; //這里返回NO,就代表return鍵值失效,即頁面上按下return,不會出現換行,如果為yes,則輸入頁面會換行
? ? }
? ? return YES;
}
========
http://blog.csdn.net/jianxin160/article/details/47753217
iOS開發系列--觸摸事件、手勢識別、搖晃事件、耳機線控
總結