- 響應(yīng)者對(duì)象UIResponder
- 事件傳遞
- 事件傳遞過(guò)程
- 關(guān)于hitTest:withEvent:方法解析
- 事件響應(yīng)者鏈條
- 應(yīng)用舉例:
- 手勢(shì)的共存和互斥
- 綜合案例
- 手勢(shì)和View的點(diǎn)擊事件關(guān)系
一. 響應(yīng)者對(duì)象UIResponder
在用戶使用APP的過(guò)程中,會(huì)產(chǎn)生各種各樣的事件 膳叨,iOS中的事件可以分為3大類型 :
在iOS中不是任何對(duì)象都能處理事件的洽洁,只有繼承了UIResponder
的對(duì)象才能接收并處理事件,我們稱之為響應(yīng)者對(duì)象
菲嘴。
那么為什么繼承自UIResponder
的類就能夠接收并處理事件呢饿自?因?yàn)樵擃愔刑峁┝艘韵?個(gè)對(duì)象方法來(lái)處理觸摸事件:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
二. 事件傳遞
下面我們通過(guò)一張圖來(lái)看看iOS中事件的產(chǎn)生和傳遞過(guò)程:
- 當(dāng)發(fā)生觸摸事件后汰翠,系統(tǒng)會(huì)將該事件加入到一個(gè)由UIApplication管理的隊(duì)列事件中
- 然后UIApplication對(duì)象會(huì)從事件隊(duì)列中取出最前面的事件,并將事件分發(fā)下去以便處理昭雌,通常會(huì)先將該事件發(fā)送給應(yīng)用程序的主窗口(keyWindow)
- 主窗口會(huì)在視圖層次結(jié)構(gòu)中找到一個(gè)最合適的視圖來(lái)處理該觸摸事件
- 找到合適的視圖控件后复唤,就會(huì)調(diào)用視圖控件的touches方法來(lái)作事件的具體處理:touchesBegin... touchesMoved...touchesEnded等
- 這些touches方法默認(rèn)的做法是將事件順著響應(yīng)者鏈條向上傳遞,將事件交給上一個(gè)相應(yīng)者進(jìn)行處理
下面我們舉個(gè)例子來(lái)演示下具體的傳遞過(guò)程城豁,如圖:
一般事件的傳遞是從父控件傳遞到子控件的苟穆,如果父控件接受不到觸摸事件抄课,那么子控件就不可能接收到觸摸事件
例如:點(diǎn)擊了綠色的View唱星,傳遞過(guò)程如下:UIApplication->Window->白色View->綠色View
點(diǎn)擊藍(lán)色的View,傳遞過(guò)程如下:UIApplication->Window->白色View->橙色View->藍(lán)色View
關(guān)于hitTest:withEvent:方法
iOS系統(tǒng)檢測(cè)到手指觸摸操作時(shí)會(huì)將其放入當(dāng)前活動(dòng)
Application
的事件隊(duì)列跟磨,UIApplication會(huì)從事件隊(duì)列中取出觸摸事件并傳遞給key window
處理间聊,window對(duì)象首先會(huì)調(diào)用hitTest:withEvent:
方法, 而該方法內(nèi)部會(huì)調(diào)用pointInside:withEvent:
方法抵拘,該方法內(nèi)部通過(guò)倒敘便利的方式也就是最先便利最后加入的子視圖哎榴,從而來(lái)判斷觸摸點(diǎn)是否在該View區(qū)域內(nèi),如果pointInside
返回YES僵蛛,則表明觸摸事件發(fā)生在該View內(nèi)部尚蝌,此時(shí)系統(tǒng)會(huì)遍歷該View的所有Subview 尋找最小單位的UIView如果當(dāng)前
View.userInteractionEnabled = NO
,enabled=NO(UIControl)
或者alpha<=0.01
充尉,hidden
等情況的時(shí)候飘言,hitTest就不會(huì)調(diào)用自己的pointInside
,直接返回nil驼侠,然后系統(tǒng)就會(huì)去遍歷兄弟節(jié)點(diǎn)姿鸿。
注意:UIImageView
的userInteractionEnabled
默認(rèn)就是NO,因此UIImageView
以及它的子控件默認(rèn)是不能接收到觸摸事件的倒源。-
如果一個(gè)子視圖的區(qū)域超過(guò)父視圖的區(qū)域,比如下圖,tabBar 中間的item
正常情況下對(duì)超出tabBar區(qū)域的觸摸操作不會(huì)被識(shí)別岗宣,因?yàn)閠abBar的pointInside:withEvent:
方法會(huì)返回NO菩鲜,這樣就不會(huì)繼續(xù)向下遍歷子視圖了。當(dāng)然胳螟,我們可以重寫(xiě)pointInside:withEvent:
方法來(lái)處理這種情況昔馋,下文會(huì)詳細(xì)描述。
判斷下當(dāng)前這個(gè)點(diǎn)在不在方法調(diào)用者上旺隙,注意:這個(gè)點(diǎn)必須是方法調(diào)用者上的坐標(biāo)系绒极,才會(huì)判斷準(zhǔn)確。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; // default returns YES if point is in bounds
下面我們用代碼來(lái)模擬下這個(gè)過(guò)程:
// 作用:尋找最合適view
// point:表示方法調(diào)用者坐標(biāo)系上的點(diǎn)
// 什么時(shí)候調(diào)用:只要一個(gè)事件傳遞給一個(gè)控件,就會(huì)調(diào)用這個(gè)控件的hitTest方法蔬捷,該方法返回誰(shuí),誰(shuí)就是最合適view
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判斷下自己能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判斷下點(diǎn)在不在當(dāng)前控件上
if ([self pointInside:point withEvent:event] == NO) return nil; // 點(diǎn)不在當(dāng)前控件
// 3.從后往前遍歷自己的子控件
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
// 獲取子控件
UIView *childView = self.subviews[I];
// 把當(dāng)前坐標(biāo)系上的點(diǎn)轉(zhuǎn)換成子控件上的點(diǎn)
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) {
return fitView;
}
}
// 4.如果沒(méi)有比自己合適的子控件,最合適的view就是自己
return self;
}
三. 事件響應(yīng)者鏈條
所謂的事件響應(yīng)者鏈條就是由多個(gè)響應(yīng)者對(duì)象連接起來(lái)的鏈條垄提,大致如下:
事件的完整處理過(guò)程
- 當(dāng)用戶點(diǎn)擊屏幕后產(chǎn)生觸摸事件榔袋,系統(tǒng)先將事件對(duì)象由上往下傳遞,也就是由父控件傳遞給子控件铡俐,直到找到最合適的控件來(lái)處理這個(gè)事件凰兑。
- 找到最合適的視圖控件后,調(diào)用該控件的
touches...
系列方法來(lái)作具體的事件處理- 如果該視圖控件中調(diào)用了
[super touches...]
审丘,則將事件順著響應(yīng)者鏈條往上傳遞吏够,傳遞給上一個(gè)響應(yīng)者對(duì)象,依次類推 - 如果該控件沒(méi)有實(shí)現(xiàn)
touches...
系列方法滩报,則將事件順著響應(yīng)者鏈條往上傳遞锅知,傳遞給上一個(gè)響應(yīng)者對(duì)象,依次類推
- 如果該視圖控件中調(diào)用了
注意:
事件的傳遞是從上到下脓钾,由父控件到子控件售睹,而事件的響應(yīng)是從下到上,是順著響應(yīng)者鏈條向上傳遞可训,由子控件到父控件的昌妹。他們是相反的。
應(yīng)用舉例:
1握截、擴(kuò)大UIButton的響應(yīng)熱區(qū)
有時(shí)候因?yàn)榭丶》裳拢覀兿霐U(kuò)大他的點(diǎn)擊響應(yīng)區(qū)域,此時(shí)我們可以:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect frame = [self getScaleFrame];
return CGRectContainsPoint(frame, point);
}
- (CGRect)getScaleFrame {
CGRect rect = self.bounds;
if (rect.size.width < 40.f) {
rect.origin.x -= (40-rect.size.width)/2;
}
if (rect.size.height < 40.f) {
rect.origin.y -= (40-rect.size.height)/2;
}
rect.size.width = 40.f;
rect.size.height = 40.f;
return rect;
}
2谨胞、子view超出了父view的bounds響應(yīng)事件
項(xiàng)目中常常遇到button已經(jīng)超出了父view的范圍但仍需可點(diǎn)擊的情況固歪,比如自定義Tabbar中間的大按鈕,點(diǎn)擊超出Tabbar bounds的區(qū)域也需要響應(yīng)
//重寫(xiě)UITabBar的pointInside方法
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
// 1. 轉(zhuǎn)換點(diǎn)擊在tabbar上的坐標(biāo)點(diǎn), 到中間按鈕上
CGPoint pointInMiddleBtn = [self convertPoint:point toView:self.middleView];
// 2. 確定中間按鈕的圓心
CGPoint middleBtnCenter = CGPointMake(33, 33);
// 3. 計(jì)算點(diǎn)擊的位置距離圓心的距離
CGFloat distance = sqrt(pow(pointInMiddleBtn.x - middleBtnCenter.x, 2) + pow(pointInMiddleBtn.y - middleBtnCenter.y, 2));
// 4. 判定中間按鈕區(qū)域之外
if (distance > 33 && pointInMiddleBtn.y < 18) {
return NO;
}
return YES;
}
3畜眨、方形按鈕的內(nèi)切圓點(diǎn)擊
如下圖 是一個(gè)正方形的UIButton昼牛,但是此時(shí)我們只想讓它的內(nèi)切圓接收點(diǎn)擊事件,而4個(gè)角落是不接受點(diǎn)擊事件的
@implementation CustomButton
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.userInteractionEnabled ||[self isHidden] ||self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
//遍歷當(dāng)前對(duì)象的子視圖
__block UIView *hit = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 坐標(biāo)轉(zhuǎn)換
CGPoint vonvertPoint = [self convertPoint:point toView:obj];
//調(diào)用子視圖的hittest方法
hit = [obj hitTest:vonvertPoint withEvent:event];
// 如果找到了接受事件的對(duì)象康聂,則停止遍歷
if (hit) {
*stop = YES;
}
}];
if (hit) {
return hit;
}
else{
return self;
}
}
else{
return nil;
}
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGFloat x1 = point.x;
CGFloat y1 = point.y;
CGFloat x2 = self.frame.size.width / 2;
CGFloat y2 = self.frame.size.height / 2;
//圓的標(biāo)準(zhǔn)方程(x-a)2+(y-b)2=r2中贰健, ab為圓心,r為半徑
double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
// 67.923
if (dis <= self.frame.size.width / 2) {
return YES;
}
else{
return NO;
}
}
@end
手勢(shì)代理方法
// 是否允許同時(shí)支持多個(gè)手勢(shì)恬汁,默認(rèn)只支持一個(gè)手勢(shì)伶椿,要調(diào)用此方法注意設(shè)置代理
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
// 是否允許開(kāi)始觸發(fā)手勢(shì)
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
return NO;
}
// 是否允許接收手機(jī)的觸摸(可以控制觸摸的范圍)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
//獲取當(dāng)前的觸摸點(diǎn)
CGPoint currentP = [touch locationInView:self.imageView];
//在圖片的左半?yún)^(qū)域可以接受觸摸
if (currentP.x < self.imageView.bounds.size.width * 0.5) {
return YES;
}else {
return NO;
}
}
四. 手勢(shì)的共存和互斥
首先我們來(lái)看看下面這段代碼:
- (void)viewDidLoad {
[super viewDidLoad];
GSViewOne *viewOne = [[GSViewOne alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
viewOne.backgroundColor = [UIColor redColor];
[self.view addSubview:viewOne];
GSViewTwo *viewTwo = [[GSViewTwo alloc] initWithFrame:CGRectMake(20, 20, 160, 160)];
viewTwo.backgroundColor = [UIColor yellowColor];
[viewOne addSubview:viewTwo];
//添加手勢(shì)
GSGestureOne *gestureOne = [[GSGestureOne alloc] initWithTarget:self action:@selector(panOne)];
[viewOne addGestureRecognizer:gestureOne];
GSGestureTwo *gestureTwo = [[GSGestureTwo alloc] initWithTarget:self action:@selector(panTwo)];
[viewTwo addGestureRecognizer:gestureTwo];
}
-(void)panOne{
NSLog(@"panOne--redView");
}
-(void)panTwo{
NSLog(@"panTwo--yellowView");
}
效果圖如下:
手勢(shì)共存
當(dāng)我們的手指在黃色View上拖拽的時(shí)候發(fā)現(xiàn)只識(shí)別了黃色區(qū)域的手勢(shì),那么現(xiàn)在有一個(gè)需求氓侧,當(dāng)手指在黃色區(qū)域拖拽的時(shí)候我要黃色和紅色區(qū)域的手勢(shì)都識(shí)別該如何實(shí)現(xiàn)脊另?
此時(shí)我們只需要實(shí)現(xiàn)UIGestureRecognizerDelegate
協(xié)議,實(shí)現(xiàn)如下方法即可:
//允許手勢(shì)共存,只要有一個(gè)手勢(shì)返回了YES约巷,那么就是共存
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
return YES;
}
手勢(shì)互斥
當(dāng)我們手指在黃色區(qū)域拖拽的時(shí)候我希望紅色區(qū)域手勢(shì)識(shí)別而黃色區(qū)域手勢(shì)不識(shí)別 偎痛,此時(shí)就用到了手勢(shì)互斥。
//gestureTwo的響應(yīng)需要gestureOne響應(yīng)失敗
[gestureTwo requireGestureRecognizerToFail:gestureOne];
或者是用代理方法也可以:
///otherGestureRecognizer它要識(shí)別独郎,需要gestureRecognizer被響應(yīng)失敗
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
五. 手勢(shì)和View的點(diǎn)擊事件關(guān)系
現(xiàn)在有一個(gè)案例踩麦,BaseVC中添加了一個(gè)手勢(shì)
@implementation BaseVC
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *tapGes = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapClick)];
[self.view addGestureRecognizer:tapGes];
}
-(void)tapClick{
NSLog(@"%s",__func__);
}
@end
ViewController
繼承自BaseVC
枚赡,在ViewController
中添加了一個(gè)tableView
,并且實(shí)現(xiàn)了didSelectRowAtIndexPath:
方法
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
NSLog(@"%s",__func__);
}
運(yùn)行起來(lái)之后谓谦,點(diǎn)擊Cell贫橙,發(fā)現(xiàn)只執(zhí)行了基類的點(diǎn)擊手勢(shì),而沒(méi)有執(zhí)行Cell的點(diǎn)擊事件反粥,這是因?yàn)槭裁丛蚰兀?/p>
這是由于手勢(shì)是大哥卢肃,點(diǎn)擊事件是小弟,可以理解為手勢(shì)優(yōu)于點(diǎn)擊事件才顿。其實(shí)是因?yàn)槭謩?shì)有一個(gè)cancelsTouchesInView
屬性莫湘,該屬性默認(rèn)值為YES
,表示識(shí)別手勢(shì)之后娜膘,是否取消view的touch事件逊脯,我們只需設(shè)置該屬性為NO即可优质。
// default is YES. causes touchesCancelled:withEvent: or pressesCancelled:withEvent: to be sent to the
//view for all touches or presses recognized as part of this gesture immediately before the action method is called.
@property(nonatomic) BOOL cancelsTouchesInView;
tapGes.cancelsTouchesInView = NO;//識(shí)別手勢(shì)之后竣贪,是否取消view的touch事件,默認(rèn)值為YES
但是當(dāng)點(diǎn)擊Cell的時(shí)候我們只想執(zhí)行Cell的點(diǎn)擊事件而不想執(zhí)行父類的手勢(shì)事件巩螃,該如何操作呢演怎?
我們只需要實(shí)現(xiàn)手勢(shì)的代理方法即可:
// called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch.
// return NO to prevent the gesture recognizer from seeing this touch
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
if ([touch.view isKindOfClass:[UITableView class]]) {
return YES;
}
return NO;
}