iOS的事件分為三類:觸摸事件(手勢(shì)操作),運(yùn)動(dòng)事件(搖一搖),遠(yuǎn)程控制事件(耳機(jī)線控),本文主要整理的是觸摸事件,對(duì)其它兩種就不多做介紹了,感興趣的同學(xué)可以自己查閱資料。
一. 關(guān)于UIResponder
- 如果一個(gè)類繼承于UIResponder那么這個(gè)類就是響應(yīng)者,就能處理觸摸事件。
UIApplication、AppDelegate、UIWindow、UIViewController、UIView都直接或間接繼承于UIResponder,所以都是響應(yīng)者,都能處理觸摸事件。 - 為什么說(shuō)繼承了UIResponder就能夠處理事件?
因?yàn)閁IResponder實(shí)現(xiàn)了如下方法來(lái)處理事件:
//一根或者多根手指開始觸摸view,系統(tǒng)會(huì)自動(dòng)調(diào)用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//一根或者多根手指在view上移動(dòng)時(shí),系統(tǒng)會(huì)自動(dòng)調(diào)用(可能多次調(diào)用)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//一根或者多根手指離開view,系統(tǒng)會(huì)自動(dòng)調(diào)用
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//觸摸結(jié)束前,某個(gè)系統(tǒng)事件( 如電話呼)會(huì)打斷觸摸過(guò)程,系統(tǒng)會(huì)自動(dòng)調(diào)用
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
注意:
- 一次完整的觸摸過(guò)程中,只會(huì)產(chǎn)生一個(gè)事件對(duì)象,4個(gè)觸摸方法都是同一個(gè)event參數(shù)。
- 如果兩根手指同時(shí)觸摸一個(gè)view,那么view只會(huì)調(diào)用一次touchesBegan:withEvent:方法,touches參數(shù)中裝著2個(gè)UITouch對(duì)象。
- 如果這兩根手指一前一后分開觸摸同一個(gè)view,那么view會(huì)分別調(diào)用兩次touchesBegan:withEvent:方法,并且每次調(diào)用時(shí)的touches參數(shù)中只包含一個(gè)UITouch對(duì)象。
二. UITouch和UIEvent介紹
1. UITouch
① UITouch的屬性
- (NSSet<UITouch *> *)touches中存放的都是UITouch對(duì)象,它是一個(gè)NSSet集合。
- UITouch對(duì)象它就是用來(lái)保存手指相關(guān)聯(lián)的信息,包括位置,時(shí)間觸摸產(chǎn)生時(shí)所處的窗口,觸摸的View,點(diǎn)擊的次數(shù)觸摸,階段等信息。
- 每一個(gè)手指對(duì)應(yīng)著一個(gè)UITouch對(duì)象。
- 這個(gè)UITouch是系統(tǒng)自動(dòng)幫我們創(chuàng)建的,當(dāng)手指移動(dòng)時(shí),系統(tǒng)會(huì)更新同一個(gè)UITouch對(duì)象,使它能夠一直保存該手指在的觸摸位置,當(dāng)手指離開屏幕時(shí),系統(tǒng)會(huì)銷毀相應(yīng)的UITouch對(duì)象。
//觸摸產(chǎn)生時(shí)所處的窗口
@property(nonatomic,readonly,retain)UIWindow *window
//觸摸產(chǎn)生時(shí)所處的視圖
@property(nonatomic,readonly,retain)UIView *view
//短時(shí)間內(nèi)點(diǎn)按屏幕的次數(shù),可以根據(jù)tapCount判斷單擊、雙擊或更多的點(diǎn)擊
@property(nonatomic,readonly)NSUInteger tapCount;
//記錄觸摸事件產(chǎn)生或變化時(shí)的時(shí)間,單位是秒
@property(nonatomic,readonly)NSTimeInterval timestamp;
//當(dāng)前觸摸事件所處的狀態(tài)
@property(nonatomic,readonly)UITouchPhase phase;
② UITouch的方法
//返回值表示觸摸在view上的位置
//這里返回的位置是針對(duì)view的坐標(biāo)系的(以view的左上角為原點(diǎn)(0, 0)
//若調(diào)用時(shí)傳的view參數(shù)為nil的話,返回的是觸摸點(diǎn)是在UIWindow的位置
-(CGPoint)locationInView:(UIView*)view;
//該方法記錄了前一個(gè)觸摸點(diǎn)的位置
- (CGPoint)previousLocationInView:(UIView*)view;
2. UIEvent
每產(chǎn)生一個(gè)事件,就會(huì)產(chǎn)生一個(gè)UIEvent對(duì)象,UIEvent稱為事件對(duì)象,記錄事件產(chǎn)生的時(shí)刻和類型。
常見(jiàn)屬性
//事件類型
@property(nonatomic,readonly) UIEventType type;
@property(nonatomic,readonly) UIEventSubtype subtype;
//事件產(chǎn)生的時(shí)間
@property(nonatomic,readonly)NSTimeIntervaltimestamp;
三. 尋找最佳響應(yīng)者,響應(yīng)者鏈條
觸摸事件從產(chǎn)生到處理完成一共經(jīng)過(guò)兩步:
- 尋找事件的最佳響應(yīng)者
- 通過(guò)響應(yīng)者鏈條尋找最終處理者,直到有處理者處理這個(gè)事件
1. 尋找事件的最佳響應(yīng)者
繼承于UIResponder的類就是響應(yīng)者,一個(gè)頁(yè)面上通常會(huì)有許許多多個(gè)這種類型的對(duì)象,都可以對(duì)點(diǎn)擊事件作出響應(yīng)。為了避免沖突,這就需要有一個(gè)先后順序,也就是響應(yīng)的優(yōu)先級(jí),hitTest方法的目的就是找到具有最高優(yōu)先級(jí)的響應(yīng)對(duì)象,就是最佳響應(yīng)者。
下面先用MJ老師的一張圖來(lái)解釋如何尋找事件的最佳響應(yīng)者:

尋找最佳響應(yīng)者的順序?yàn)?
UIApplication -> UIWindow -> UIView
發(fā)生觸摸事件后,系統(tǒng)會(huì)將該事件加入到一個(gè)由UIApplication管理的事件隊(duì)列中。UIApplication會(huì)從事件隊(duì)列中取出最前面的事件并將事件分發(fā)給應(yīng)用程序的keyWindow,keyWindow判斷自己可以接收觸摸事件并且觸摸點(diǎn)在自己身上,就會(huì)從后往前遍歷子控件,然后重復(fù)以上步驟,直到找到最佳響應(yīng)者。
那么系統(tǒng)內(nèi)部究竟是如何找到最合適的視圖呢?
我們發(fā)現(xiàn)UIView中包括以下兩個(gè)方法:
這兩個(gè)方法用于尋找最佳響應(yīng)者,所以說(shuō)凡是繼承于UIView的類都有可能成為最佳響應(yīng)者。
//這個(gè)方法返回的就是最佳響應(yīng)者
//只要一個(gè)事件,傳遞給一個(gè)控件時(shí), 就會(huì)調(diào)用這個(gè)控件的hitTest方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//用于判斷觸摸的點(diǎn)是否在自己身上
//point:必須是方法調(diào)用者的坐標(biāo)系
//hitTest方法底層會(huì)調(diào)用這個(gè)方法,判斷點(diǎn)在不在控件上
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
hitTest方法內(nèi)部干的事情就是下面??
- 判斷自己是否能接收觸摸事件, 如果是
- 判斷觸摸點(diǎn)是否在自己身上, 如果是
- 從后往前遍歷子控件,重復(fù)前面的兩個(gè)步驟
- 如果沒(méi)有符合條件的子控件,那么hitTest就返回自己最適合處理
UIView 不接收觸摸事件的三種情況:
不接收用戶交互 userInteractionEnabled=NO
隱藏 hidden=YES.
透明 alpha=0.0 ~ 0.01UIImageView的userInteractionEnabled默認(rèn)就是NO,因此UIImageView以及它的子控件默認(rèn)是不能接收觸摸事件的
既然我們都知道hitTest內(nèi)部做了什么,那么我們可以嘗試實(shí)現(xiàn)hitTest方法,代碼如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 先判斷視圖是否處于不能響應(yīng)事件的3種狀態(tài)
if (self.userInteractionEnabled == NO || self.hidden || self.alpha < 0.01) {
return nil;
}
// 判斷觸摸點(diǎn)是否在視圖的坐標(biāo)范圍內(nèi)
if ([self pointInside:point withEvent:event] == NO) {
return nil;
}
// 從后向前遍歷視圖的子視圖
for (int i = (int)self.subviews.count - 1; i >= 0; i--) {
UIView *subView = self.subviews[I];
// 坐標(biāo)轉(zhuǎn)換,把觸摸點(diǎn)的位置轉(zhuǎn)換為子視圖坐標(biāo)系下的坐標(biāo)
CGPoint subPoint = [self convertPoint:point toView:subView];
// 對(duì)子視圖進(jìn)行Hit-Testing
UIView *subHTView = [subView hitTest:subPoint withEvent:event];
// 如果子視圖有最佳響應(yīng)者,返回該最佳響應(yīng)者視圖,結(jié)束循環(huán)
if (subHTView) {
return subHTView;
}
}
// 如果子視圖中沒(méi)有最佳響應(yīng)者,返回自己
return self;
}
我們通過(guò)這段代碼還可以解釋另外一種現(xiàn)象,子視圖超出了父視圖的范圍,點(diǎn)擊子視圖在父視圖之外的部分沒(méi)有反應(yīng)。這是因?yàn)樵谶M(jìn)行hitTest的時(shí)候,父視圖雖然能響應(yīng)事件,但是點(diǎn)并不不父視圖內(nèi),直接就返回nil了,自然不會(huì)再去詢問(wèn)子視圖是否能夠響應(yīng)事件。
事件的最佳響應(yīng)者找到之后,下面就是通過(guò)響應(yīng)者鏈條尋找事件的最終處理者。
2. 通過(guò)響應(yīng)者鏈條尋找事件的最終處理者
① 什么是響應(yīng)者鏈條
最佳響應(yīng)者的nextResponder是下一個(gè)響應(yīng)者,下一個(gè)響應(yīng)者的nextResponder是下下一個(gè)響應(yīng)者,一直到AppDelegate,他們就組成了響應(yīng)者鏈條。
對(duì)于響應(yīng)者對(duì)象,默認(rèn)的nextResponder實(shí)現(xiàn)如下??
UIView:若視圖是控制器的根視圖,則其nextResponder為控制器對(duì)象;否則,其nextResponder為父視圖。
UIViewController:若控制器的視圖是window的根視圖,則其nextResponder為窗口對(duì)象;若控制器是從別的控制器present出來(lái)的,則其nextResponder為presenting view controller。
UIWindow:nextResponder為UIApplication對(duì)象
UIApplication:若當(dāng)前應(yīng)用的app delegate是一個(gè)UIResponder對(duì)象,且不是UIView、UIViewController或app本身,則UIApplication的nextResponder為app delegate
AppDelegate:nextResponder為nil
他們就組成了響應(yīng)鏈條:
View -> UIViewController -> UIWindow -> UIApplication -> AppDelegate

上圖是官網(wǎng)對(duì)于響應(yīng)鏈的示例展示,若觸摸發(fā)生在UITextField上,則事件的傳遞順序是:
UITextField->UIView->UIView->UIViewController->UIWindow->UIApplication->UIApplicationDelegate。
圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的,則其nextResponder為UIViewController對(duì)象;若是直接add在UIWindow上的,則其nextResponder為UIWindow對(duì)象。
可以用以下方式打印一個(gè)響應(yīng)鏈中的每一個(gè)響應(yīng)對(duì)象,在最佳響應(yīng)者的 touchBegin:withEvent: 方法中調(diào)用即可(別忘了調(diào)用父類的方法)。
- (void)printResponderChain
{
UIResponder *responder = self;
printf("%s",[NSStringFromClass([responder class]) UTF8String]);
while (responder.nextResponder) {
responder = responder.nextResponder;
printf(" --> %s",[NSStringFromClass([responder class]) UTF8String]);
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self printResponderChain];
[super touchesBegan:touches withEvent:event];
}
以上一節(jié)tabBar上凸起的圓形按鈕的案例為例,重寫CircleButton的 touchBegin:withEvent:方法,點(diǎn)擊原型按鈕的任意區(qū)域,打印出的完整響應(yīng)鏈如下:
CircleButton --> CustomeTabBar --> UIView --> UIViewController --> UIViewControllerWrapperView --> UINavigationTransitionView --> UILayoutContainerView --> UINavigationController --> UIWindow --> UIApplication --> AppDelegate
另外如果有需要,完全可以重寫響應(yīng)者的 nextResponder 方法來(lái)自定義響應(yīng)鏈。但是這個(gè)屬性值只讀的,不允許重寫。
② 如何找到最終處理者
經(jīng)歷hitTest后,UIApplication已經(jīng)知道事件的最佳響應(yīng)者是誰(shuí)了,接下來(lái)要做的兩件事情就是:
- 將事件傳遞給最佳響應(yīng)者響應(yīng)處理。如果最佳響應(yīng)者不處理的話:??
- 通過(guò)最佳響應(yīng)者的nestResponder將事件沿著響應(yīng)鏈傳遞,直到有UIResponder對(duì)象對(duì)此事件負(fù)責(zé)。
首先:UIApplication如何把事件傳遞給最佳響應(yīng)者?
因?yàn)樽罴秧憫?yīng)者具有最高的事件響應(yīng)優(yōu)先級(jí),因此UIApplication會(huì)先將事件傳遞給它供其響應(yīng)。首先,UIApplication將事件通過(guò) sendEvent: 傳遞給事件所屬的window,window同樣通過(guò) sendEvent: 再將事件傳遞給hit-tested view,即最佳響應(yīng)者。過(guò)程如下:
UIApplication ——> UIWindow ——> hit-tested view
如下,可以發(fā)現(xiàn)UIApplication和UIWindow里面的確有sendEvent方法:
//UIApplication里面的sendEvent方法
- (void)sendEvent:(UIEvent *)event;
//UIWindow里面的sendEvent方法
- (void)sendEvent:(UIEvent *)event; // called by UIApplication to dispatch events to views inside the window
以尋找事件的最佳響應(yīng)者一節(jié)中點(diǎn)擊視圖E為例,在EView的 touchesBegan:withEvent: 上斷點(diǎn)查看調(diào)用棧就能看清這一過(guò)程:

那么問(wèn)題又來(lái)了。這個(gè)過(guò)程中,假如應(yīng)用中存在多個(gè)window對(duì)象,UIApplication是怎么知道要把事件傳給哪個(gè)window的?window又是怎么知道哪個(gè)視圖才是最佳響應(yīng)者的呢?
其實(shí)簡(jiǎn)單思考一下,這兩個(gè)過(guò)程都是傳遞事件的過(guò)程,涉及的方法都是 sendEvent: ,而該方法的參數(shù)(UIEvent對(duì)象)是唯一貫穿整個(gè)經(jīng)過(guò)的線索,那么就可以大膽猜測(cè)必然是該觸摸事件對(duì)象上綁定了這些信息。事實(shí)上之前在介紹UITouch的時(shí)候就說(shuō)過(guò)touch對(duì)象保存了觸摸所屬的window及view,而event對(duì)象又綁定了touch對(duì)象,如此一來(lái),是不是就知道是哪個(gè)window了。要是不信的話,那就自定義一個(gè)Window類,重寫 sendEvent: 方法,捕捉該方法調(diào)用時(shí)參數(shù)event的狀態(tài),答案就顯而易見(jiàn)了。

至于這兩個(gè)屬性是什么時(shí)候綁定到touch對(duì)象上的,必然是在hit-testing的過(guò)程中唄,仔細(xì)想想hit-testing干的不就是這個(gè)事兒?jiǎn)帷?/p>
其次:如果最佳響應(yīng)者不處理事件怎么辦?
如果最佳響應(yīng)者能處理事件,那肯定把事件交給最佳響應(yīng)者處理,如果它不處理,再順著響應(yīng)者鏈條往上尋找其他處理者(其他博客都說(shuō)響應(yīng)者鏈?zhǔn)峭聦ふ?,其?shí)順序都是一個(gè)意思)。
- 找到最佳響應(yīng)者之后,如果這個(gè)view可以處理就會(huì)給這個(gè)view處理,如果這個(gè)view沒(méi)處理。
- 如果view的控制器存在就傳遞給view的控制器處理;如果控制器不存在,則將其傳遞給它的父視圖。
- 在視圖層次結(jié)構(gòu)的最頂級(jí)視圖,如果也不能處理收到的事件,則其將事件傳遞給window對(duì)象進(jìn)行處理。
- 如果window對(duì)象也不能處理 ,則其將事件傳遞給UIApplication對(duì)象處理。
- 如果UIApplication也不能處理該事件,則其將事件傳遞給AppDelegate,如果AppDelegate也不能處理,則將事件丟棄。
就是將事件順著響應(yīng)者鏈條向上傳遞,將事件交給上一個(gè)響應(yīng)者進(jìn)行處理,找到處理事件的最終處理者之后他就會(huì)調(diào)用touchesBegan等方法來(lái)進(jìn)行事件處理。
③ 如何處理事件
響應(yīng)者對(duì)于事件的攔截以及傳遞都是通過(guò)touchesBegan:withEvent:方法控制的,該方法的默認(rèn)實(shí)現(xiàn)是將事件沿著默認(rèn)的響應(yīng)鏈往上傳遞。如果你在不同的UIResponder對(duì)象上面都聲明了touchMoved方法,那么這些對(duì)象都可以執(zhí)行該方法,因?yàn)閠ouchesBegan方法默認(rèn)是把事件沿著響應(yīng)鏈傳遞的。如果只想讓一個(gè)對(duì)象響應(yīng)touchesMoved方法,需要重寫touchesBegan方法以攔截事件。
響應(yīng)者對(duì)于接收到的事件有3種操作:
- 不攔截,默認(rèn)操作
事件會(huì)自動(dòng)沿著默認(rèn)的響應(yīng)鏈向上傳遞。 - 攔截,不再往上分發(fā)事件
重寫touchesBegan:withEvent:方法進(jìn)行事件處理,不調(diào)用父類的touchesBegan:withEvent:方法。 - 攔截,繼續(xù)往上分發(fā)事件
重寫touchesBegan:withEvent:進(jìn)行事件處理,同時(shí)調(diào)用父類的touchesBegan:withEvent:將事件往上傳遞。
四. hitTest驗(yàn)證和應(yīng)用
1. 驗(yàn)證hitTest方法調(diào)用順序
新建HTView繼承于UIView,重寫hitTest:withEvent:方法。
HTView代碼如下:
@interface HTView : UIView
@property (nonatomic, strong) NSString *name; //視圖的名字
@end
@implementation HTView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"進(jìn)入%@視圖-%s", self.name, __func__);
UIView *view = [super hitTest:point withEvent:event];
NSLog(@"離開%@視圖-%s", self.name, __func__);
return view;
}
@end
ViewController代碼如下:
#import "ViewController.h"
#import "HTView.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet HTView *aView;
@property (weak, nonatomic) IBOutlet HTView *bView;
@property (weak, nonatomic) IBOutlet HTView *cView;
@property (weak, nonatomic) IBOutlet HTView *dView;
@property (weak, nonatomic) IBOutlet HTView *eView;
@property (weak, nonatomic) IBOutlet HTView *fView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.aView.name = @"A";
self.bView.name = @"B";
self.cView.name = @"C";
self.dView.name = @"D";
self.eView.name = @"E";
self.fView.name = @"F";
}
@end
如圖:
如圖所示,A視圖上面添加了子視圖B和C,B上面添加了子視圖D,C上面添加了子視圖E和F。
點(diǎn)擊E后打印如下:

由打印結(jié)果可以看出
- 事件首先傳遞給視圖A,A判斷自身能響應(yīng)事件并且點(diǎn)在自己內(nèi)
- 繼續(xù)從后向前遍歷A的子視圖,因?yàn)镃比B后添加,因此首先傳遞給C
- C判斷自身能響應(yīng)事件,并且點(diǎn)在自己內(nèi)。繼續(xù)從后向前遍歷C的子視圖,因?yàn)镕比E后添加,因此首先傳遞給F
- F判斷自身不能響應(yīng)事件(因?yàn)辄c(diǎn)不在F內(nèi)),C又將事件傳遞給E
- E判斷自身能響應(yīng)事件,同時(shí)E已經(jīng)沒(méi)有子視圖,因此最終E就是最佳響應(yīng)者
這里有兩個(gè)問(wèn)題:
- 為什么要從后往前遍歷? 因?yàn)楹竺娴囊晥D一般都是后添加的,一般在后面,這樣遍歷節(jié)省時(shí)間。
- 為什么一直打印兩次? 這個(gè)我也不知道為什么一直都是打印兩次,就這樣記吧。
2. hitTest應(yīng)用
應(yīng)用①:
view上添加一個(gè)button和一個(gè)藍(lán)色view, 藍(lán)色view遮擋在按鈕的上面
點(diǎn)擊View時(shí), View接收事件,當(dāng)發(fā)現(xiàn)點(diǎn)擊的點(diǎn)在按鈕的位置時(shí), 讓底部的按鈕處理事件

實(shí)現(xiàn)藍(lán)色View的touchBegain方法,先監(jiān)聽UIView的點(diǎn)擊. 并去實(shí)現(xiàn)UIView的hitTest方法, 在hitTest方法當(dāng)中通過(guò)把當(dāng)前點(diǎn)轉(zhuǎn)換成按鈕所在的坐標(biāo)系
CGPoint btnP = [self convertPoint:point toView:self.btn];
轉(zhuǎn)換過(guò)后查看當(dāng)前點(diǎn)在不在按鈕上,如果在按鈕上,就直接返回按鈕, 如果不在按鈕上,保持系統(tǒng)默認(rèn)做法
藍(lán)色view里面實(shí)現(xiàn)代碼:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//判斷當(dāng)前點(diǎn)在不在按鈕上.
//把當(dāng)前點(diǎn)轉(zhuǎn)換成按鈕所在的坐標(biāo)系
CGPoint btnP = [self convertPoint:point toView:self.btn];
if ([self.btn pointInside:btnP withEvent:event]) {
return self.btn;
}else{
return [super hitTest:point withEvent:event];
}
}
應(yīng)用②:
對(duì)話框按鈕內(nèi)部添加一個(gè)云朵按鈕, 添加的云朵按鈕在父控件的外面,如圖:
要求:
1.按鈕可以隨著手指拖動(dòng)而拖動(dòng)
2.讓超過(guò)按鈕的子控件也能夠響應(yīng)事件,一般情況下,當(dāng)一個(gè)控件超過(guò)他的父控件的時(shí)候,是不能夠接收事件的.
實(shí)現(xiàn)思路:
第一步:先辦到讓對(duì)話框按鈕能夠跟隨著手指移動(dòng)而移動(dòng)
實(shí)現(xiàn)對(duì)話框按鈕的touchesMoved方法,在touchesMoved方法當(dāng)中,獲得當(dāng)前手指所在的點(diǎn).以前上一個(gè)點(diǎn)
分別計(jì)算X軸的偏移量以及Y軸的偏移量
然后修改當(dāng)前按鈕的transform讓按鈕辦到能夠跟隨著手指移動(dòng)而移動(dòng)
第二步, 實(shí)現(xiàn)對(duì)話框按鈕的hitTest方法
在該方法當(dāng)中去判斷當(dāng)前的點(diǎn)在不在按鈕的子控件上
如果在按鈕的子控件上.就返回按鈕的子控件如果不在的話, 就保持系統(tǒng)的默認(rèn)做法
對(duì)話框按鈕.m 文件
#import "chatBtn.h"
@implementation chatBtn
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//判斷當(dāng)前點(diǎn)在不在popBtn身上
//把當(dāng)前點(diǎn)轉(zhuǎn)換popBtn身上的點(diǎn)
CGPoint popBtnP = [self convertPoint:point toView:self.popBtn];
if ( [self.popBtn pointInside:popBtnP withEvent:event]) {
return self.popBtn;
}else{
return [super hitTest:point withEvent:event];
}
}
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//1.獲取UITouch
UITouch *touch = [touches anyObject];
//2.獲取當(dāng)前手指的點(diǎn),上一個(gè)手指的點(diǎn)
CGPoint curP = [touch locationInView:self];
CGPoint preP = [touch previousLocationInView:self];
//3.計(jì)算偏移量
CGFloat offsetX = curP.x - preP.x;
CGFloat offsetY = curP.y - preP.y;
//4.平移
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}
@end
應(yīng)用③:
如果碰到這種需求怎么辦?比如說(shuō)tabBar中間的按鈕凸起
點(diǎn)擊凸起區(qū)域后,生成的觸摸事件首先傳到UIWindow,然后傳到控制器的根視圖即RootView。RootView經(jīng)判斷可以響應(yīng)觸摸事件,而后將事件傳給了子控件TabBar。問(wèn)題就出在這里,因?yàn)橛|摸點(diǎn)不在TabBar的坐標(biāo)范圍內(nèi),因此TabBar無(wú)法響應(yīng)該觸摸事件,hitTest:withEvent: 直接返回了nil。而后RootView就會(huì)詢問(wèn)TableView是否能夠響應(yīng),事實(shí)上是可以的,因此事件最終被TableView消耗。整個(gè)過(guò)程,事件根本沒(méi)有傳遞到圓形按鈕。
有問(wèn)題就會(huì)有解決策略。經(jīng)過(guò)分析,發(fā)現(xiàn)原因是hit-Testing的過(guò)程中,事件在傳遞到TabBar的時(shí)候沒(méi)能繼續(xù)往CircleButton傳,因?yàn)辄c(diǎn)擊區(qū)域坐標(biāo)不在Tabbar的坐標(biāo)范圍內(nèi),因此Tabbar被識(shí)別成了無(wú)法響應(yīng)事件。既然如此,我們可以修改事件hit-Testing的過(guò)程,當(dāng)點(diǎn)擊紅色方框區(qū)域時(shí)讓事件流向原型按鈕。事件傳遞到TabBar時(shí),TabBar的 hitTest:withEvent: 被調(diào)用,但是 pointInside:withEvent: 會(huì)返回NO,如此一來(lái) hitTest:withEvent: 返回了nil。既然如此,可以重寫TabBard的 pointInside:withEvent: ,判斷當(dāng)前觸摸坐標(biāo)是否在子視圖CircleButton的坐標(biāo)范圍內(nèi),若在,則返回YES,反之返回NO。這樣一來(lái)點(diǎn)擊紅色區(qū)域,事件最終會(huì)傳遞到CircleButton,CircleButton能夠響應(yīng)事件,最終事件就由CircleButton響應(yīng)了。同時(shí)點(diǎn)擊紅色方框以外的非TabBar區(qū)域的情況下,因?yàn)門abBar無(wú)法響應(yīng)事件,會(huì)按照預(yù)期由TableView響應(yīng)。代碼如下:
//tabBar
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
//將觸摸點(diǎn)坐標(biāo)轉(zhuǎn)換到在circleButton上的坐標(biāo)
CGPoint subPoint = [self convertPoint:point toView:self.circleButton];
//若觸摸點(diǎn)circleButton上則返回YES
if ([self.circleButton pointInside:subPoint withEvent:event]) {
return YES;
}
//否則返回默認(rèn)的操作
return [super pointInside:point withEvent:event];
}
五. 總結(jié)
- hitTest:withEvent:和pointInside:withEvent:是UIView的方法,用來(lái)尋找最佳響應(yīng)者的。
- sendEvent:是UIApplication和UIWindow的方法,用來(lái)把事件傳遞給最佳響應(yīng)者。
- touchesBegan:withEvent:等方法是UIResponder的方法,UIApplication、AppDelegate、UIWindow、UIViewController、UIView都直接或間接繼承于UIResponder,所以都是響應(yīng)者,都能處理觸摸事件。
Demo地址:https://github.com/iamkata/touch
本文部分參考以下鏈接,如有侵權(quán)請(qǐng)聯(lián)系刪除。
iOS觸摸事件處理
iOS觸摸事件全家桶