iOS-觸摸事件傳遞、事件響應者鏈

前言,本文簡單了解觸摸事件傳遞和事件響應者鏈。

一、知識點簡介

1.1 iOS中的事件介紹

iOS中的事件可以分為3大類型:

  • 觸屏事件(例如點擊按鈕、通過手勢縮放圖片、拖動上下滾動頁面等)
  • 加速計事件(例如搖一搖紅包、通過旋轉(zhuǎn)設(shè)備控制賽車方向、指南針等)
  • 遠程控制事件(例如耳機的線控、外接手柄、遙控器等)

iOS處理觸屏事件,分為兩種方式:

  • 高級事件處理:利用UIKit提供的各種用戶控件或者手勢識別器來處理事件。
  • 低級事件處理:在UIView的子類中重寫觸屏回調(diào)方法,直接處理觸屏事件。

1.2 UIGestureRecognizer

UIKit中我們常用的是UIControl類實例的addTarget:action:forControlEvents:方法維護控件目標行為表,除了UIKit控件外,手勢識別器UIGestureRecognizer類的實例也可以處理觸屏事件,其內(nèi)部也使用目標行為表。

UIKit內(nèi)置了6種手勢識別器:
UITapGestureRecognizer:點擊(單擊、雙擊、三連擊等)手勢。
UIPinchGestureRecognizer:縮放手勢。
UIPanGestureRecognizer:拖拽手勢。
UISwipeGestureRecognizer:滑動手勢。
UIRotationGestureRecognizer:旋轉(zhuǎn)手勢。
UILongPressGestureRecognizer:長按手勢。

1.3 UITouch

當你用一根手指觸摸屏幕時, 會創(chuàng)建一個與之關(guān)聯(lián)的UITouch對象, 一個UITouch對象對應一根手指. 在事件中可以根據(jù)NSSet中UITouch對象的數(shù)量得出此次觸摸事件是單指觸摸還是雙指多指等等.

觸摸產(chǎn)生時所處的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
觸摸產(chǎn)生時所處的視圖
@property(nonatomic,readonly,retain) UIView   *view;
短時間內(nèi)點按屏幕的次數(shù),可以根據(jù)tapCount判斷單擊、雙擊或更多的點擊
@property(nonatomic,readonly) NSUInteger      tapCount;
記錄了觸摸事件產(chǎn)生或變化時的時間,單位是秒
@property(nonatomic,readonly) NSTimeInterval  timestamp;
當前觸摸事件所處的狀態(tài)
@property(nonatomic,readonly) UITouchPhase    phase;
// UIView是UIResponder的子類,可以覆蓋下列4個方法處理不同的觸摸事件
// 一根或者多根手指開始觸摸view,系統(tǒng)會自動調(diào)用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移動,系統(tǒng)會自動調(diào)用view的下面方法(隨著手指的移動,會持續(xù)調(diào)用該方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指離開view,系統(tǒng)會自動調(diào)用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 觸摸結(jié)束前,某個系統(tǒng)事件(例如電話呼入)會打斷觸摸過程,系統(tǒng)會自動調(diào)用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch對象

1.4 UIEvent

每產(chǎn)生一個事件, 就對應產(chǎn)生一個UIEvent. UIEvent記錄著該事件產(chǎn)生的時間, 事件的類型等等.

UIEvent幾個重要的屬性 :
事件類型
@property(nonatomic,readonly) UIEventType     type;
@property(nonatomic,readonly) UIEventSubtype  subtype;
事件產(chǎn)生的時間
@property(nonatomic,readonly) NSTimeInterval  timestamp;

1.5 響應者對象(UIResponder)

在iOS中不是任何對象都能處理事件, 只有繼承了UIResponder的對象才能接收并處理事件,我們稱為響應者對象.
UIApplication,UIViewController,UIView都繼承自UIResponder,因此他們都是響應者對象, 都能夠接收并處理事件.

繼承自UIResponder的類能處理事件是由于UIResponder內(nèi)部提供了以下方法:

觸摸事件
- (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;
加速計事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
遠程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

兩個UIView相關(guān)屬性:

  • multipleTouchEnabled:是否開啟多點觸控
  • exclusiveTouch :多個控件接受事件時的排他性

二、事件傳遞

  • 1.發(fā)生觸摸事件后,系統(tǒng)會將該事件加入到一個由UIApplication管理的隊列事件中
  • 2.UIApplication會從事件隊列中取出最前面的事件,并將事件分發(fā)下去以便處理,通常會先發(fā)送事件給應用程序的主窗口(keyWindow)
  • 3.主窗口會在視圖層次結(jié)構(gòu)中找到一個最合適的視圖來處理觸摸事件

注:一般事件的傳遞是從父控件傳遞到子控件的;如果父控件接受不到觸摸事件,那么子控件就不可能接收到觸摸事件。

示例 View

例如:

  • 點擊了綠色的View,傳遞過程如下:UIApplication->Window->白色View->綠色View
  • 點擊藍色的View,傳遞過程如下:UIApplication->Window->白色View->橙色View->藍色View

2.1 查找合適 View 步驟

應用查找到最合適的控件來處理事件,有以下準則

  • 1.首先判斷主窗口(keyWindow)自己是否能接受觸摸事件
  • 2.觸摸點是否在自己身上
  • 3.從后往前遍歷子控件,重復前面的兩個步驟(首先查找數(shù)組中最后一個元素)
  • 4.如果沒有符合條件的子控件,那么就認為自己最合適處理

詳細步驟:

  • 1.主窗口接收到應用程序傳遞過來的事件后,首先判斷自己能否接手觸摸事件。如果能,那么在判斷觸摸點在不在窗口自己身上
  • 2.如果觸摸點也在窗口身上,那么窗口會從后往前遍歷自己的子控件(遍歷自己的子控件只是為了尋找出來最合適的view)
  • 3.遍歷到每一個子控件后,又會重復上面的兩個步驟(傳遞事件給子控件,1.判斷子控件能否接受事件,2.點在不在子控件上)
  • 4.如此循環(huán)遍歷子控件,直到找到最合適的view,如果沒有更合適的子控件,那么自己就成為最合適的view。
    注意:之所以會采取從后往前遍歷子控件的方式尋找最合適的view只是為了做一些循環(huán)優(yōu)化。因為相比較之下,后添加的view在上面,降低循環(huán)次數(shù)。

判斷UIView不能接收觸摸事件的三種情況:

  • 不接受用戶交互:userInteractionEnabled = NO;
  • 隱藏:hidden = YES;
  • 透明:alpha = 0.0~0.01

2.2 hitTest:withEvent:方法

只要事件一傳遞給一個控件,這個控件就會調(diào)用他自己的hitTest:withEvent:方法尋找合適的View.
作用:
尋找并返回最合適的view(能夠響應事件的那個最合適的view)
不管這個控件能不能處理事件,也不管觸摸點在不在這個控件上,事件都會先傳遞給這個控件,隨后再調(diào)用hitTest:withEvent:方法
判斷當前View 是否滿足接收觸摸事件的條件(1.用戶交互 2. 隱藏 3. 透明度)

底層具體實現(xiàn)如下 :
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 1.判斷當前控件能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    // 2. 判斷點在不在當前控件
    if ([self pointInside:point withEvent:event] == NO) return nil;
    // 3.從后往前遍歷自己的子控件
    NSInteger count = self.subviews.count;
    for (NSInteger i = count - 1; i >= 0; i--) {
        UIView *childView = self.subviews[i];
        // 把當前控件上的坐標系轉(zhuǎn)換成子控件上的坐標系
        CGPoint childP = [self convertPoint:point toView:childView];
        UIView *fitView = [childView hitTest:childP withEvent:event];
        if (fitView) { // 尋找到最合適的view
            return fitView;
        }
    }
    // 循環(huán)結(jié)束,表示沒有比自己更合適的view
    return self;
}

如果確定最終父控件是最合適的view,那么該父控件的子控件的hitTest:withEvent:方法也是會被調(diào)用的,判斷所有子控件不合適最后返回父控件。

2.3 pointInside:withEvent:方法

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
該方法判斷觸摸點是否在控件身上, 是則返回YES, 否則返回NO

搭配hitTest:withEvent:達到目的:

  • 明明點擊的是B視圖, 卻由A視圖來響應事件
  • 穿透某控件點擊被覆蓋的下一層控件
  • 讓父控件frame之外的子控件響應觸摸事件

2.4 視圖不響應

發(fā)現(xiàn)視圖無法響應點擊事件,可以檢查下面幾項:

1、hidden = YES 視圖被隱藏
2、userInteractionEnabled = NO 不接受響應事件
3、alpha <= 0.01,透明視圖不接收響應事件
4、子視圖超出父視圖范圍
5、需響應視圖被其他視圖蓋住
6、是否重寫了其父視圖以及自身的hitTest方法
7、是否重寫了其父視圖以及自身的pointInside方法

三、事件響應

找到合適的View之后就會調(diào)用該view的touches方法要進行響應處理具體的事件,找不到最合適的view,就不會調(diào)用touches方法進行事件處理。
響應者鏈條其實就是很多響應者對象(繼承自UIResponder的對象)一起組合起來的鏈條稱之為響應者鏈條。
一般默認做法是控件將事件順著響應者鏈條向上傳遞,將事件交給上一個響應者進行處理 (即調(diào)用super的touches方法)。

判斷當前響應者的上一個響應者是誰:

  • 1.判斷當前是否是控制器的View,如果是控制器的View,上一個響應者就是控制器
  • 2.如果不是控制器的View,上一個響應者就是父控件


    事件響應視圖

3.1 響應鏈

響應鏈是“事件派發(fā)”的原則和規(guī)定,那么響應鏈是什么?顧名思義事件鏈是一個鏈條,詳細的定義如下:

  • 每條鏈是一個 鏈表狀結(jié)構(gòu),整個是一棵樹
  • 鏈表的每一個node是一個 UIResponser對象


    響應鏈

3.2 touch 響應

  • 找到最合適的view會調(diào)用touches方法處理事件
  • touches默認做法是把事件順著響應者鏈條向上拋

如果控制器不響應響應touches方法,就交給UIWindow。如果UIWindow也不響應,交給UIApplication,如果都不響應事件就作廢了。

//只要點擊控件,就會調(diào)用touchBegin,如果沒有重寫這個方法,自己處理不了觸摸事件
// 上一個響應者可能是父控件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
// 默認會把事件傳遞給上一個響應者,上一個響應者是父控件,交給父控件處理
[super touchesBegan:touches withEvent:event]; 
// 注意不是調(diào)用父控件的touches方法,而是調(diào)用父類的touches方法
// super是父類 superview是父控件 
}

3.3 觸摸事件傳遞響應過程:

完整的觸摸事件的傳遞響應過程為:
UIApplication–>UIWindow–>遞歸找到最合適處理的控件–>控件調(diào)用touches方法–>判斷是否實現(xiàn)touches方法–>沒有實現(xiàn)默認會將事件傳遞給上一個響應者–>找到上一個響應者–>找不到方法作廢
總結(jié):觸摸或者點擊一個控件,然后這個事件會從上向下(從父->子)找最合適的view處理,找到這個view之后看他能不能處理,能就處理,不能就按照事件響應鏈向上(從子->父)傳遞給父控件

事件的傳遞和響應的區(qū)別:

  • 事件的傳遞是從上到下(父控件到子控件)
  • 事件的響應是從下到上(順著響應者鏈條向上傳遞:子控件到父控件。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容