iOS 中的事件的產生和傳遞

image.png

事件的產生

  • 當有觸摸或者其他事件產生,將事件交由 IOKit.framework 處理。
  • IOKit.framework 將事件封裝成一個 IOHIDEvent 對象,并通過 mach port 傳遞給 SpringBoad
  • SpringBoard 會接收這個對象并通過 mach port 轉發給當前 App 的進程;
  • 喚醒 runloop,觸發了 source1 回調,其回調函數為 __IOHIDEventSystemClientQueueCallback()
  • source1 回調觸發 source0 回調,將接收到的 IOHIDEvent 對象封裝成 UIEvent 對象進行處理或分發。

注意:
SpringBoard 其實是一個標準的應用程序,這個應用程序用來管理 iOS 的主屏幕;
source1 是蘋果用來監聽 mach port 傳來的系統事件的,source0 是用來處理用戶事件的。
source1 收到系統事件后,會回調 source0,所以最終這些事件都是由 source0 處理的。

事件傳遞的流程

  • 當用戶點擊屏幕時,會產生一個觸摸事件,系統會將該事件加入到一個由 UIApplication 管理的事件隊列中。
  • UIApplication 會從事件隊列中取出最前面的事件,并將事件分發下去以便處理,通常先發送事件給應用程序的主窗口 keyWindow
  • 主窗口會調用 hitTest:withEvent: 方法在視圖 View 層次結構中找到一個最合適的 View 來處理觸摸事件。
  • 最終,這個觸摸事件交給主窗口的 hitTest:withEvent: 方法返回的視圖對象去處理。

注意:如果父控件不能接受觸摸事件,那么子控件就不可能接收到觸摸事件。

View 不能接收觸摸事件的三種情況:

  • 不允許交互:userInteractionEnabled = NO
  • 隱藏:如果把父控件隱藏,那么子控件也會隱藏,隱藏的控件不能接受事件;
  • 透明度:如果設置一個控件的透明度<0.01,會直接影響子控件的透明度,0.0~0.01 為透明。

注意:
默認 UIImageView 不能接受觸摸事件,因為不允許交互,即 userInteractionEnabled = NO。所以如果希望 UIImageView 可以交互,需要設置 UIImageViewuserInteractionEnabled = YES

如何找到最合適的控件來處理事件?

  • 首先判斷主窗口 keyWindow 自己是否能接受觸摸事件。
  • 調用當前視圖的 pointInside:withEvent: 方法判斷觸摸點是否在當前視圖內。
  • pointInside:withEvent: 方法返回 NO,說明觸摸點不在當前視圖內,則當前視圖的 hitTest:withEvent: 返回 nil
  • pointInside:withEvent: 方法返回 YES,說明觸摸點在當前視圖內,則遍歷當前視圖的所有子視圖 subviews,調用子視圖的 hitTest:withEvent: 方法重復前面的步驟,子視圖的遍歷順序是從上到下,即從 subviews 數組的末尾向前遍歷,直到有子視圖的 hitTest:withEvent: 方法返回非空對象或者全部子視圖遍歷完畢。
  • 若第一次有子視圖的 hitTest:withEvent: 方法返回非空對象,則當前視圖的 hitTest:withEvent: 方法就返回此對象,處理結束。
  • 若所有子視圖的 hitTest:withEvent: 方法都返回 nil,則當前視圖的 hitTest:withEvent: 方法返回當前視圖自身。

查找第一響應者

hitTest:withEvent:方法

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

控件通過重寫 hitTest:withEvent: 方法,來判斷點擊區域是否在視圖上,是則返回 YES,不是則返回 NO,尋找并返回最合適的 view (能夠響應事件的那個最合適的 view)。

應用程序接收到事件后,將事件交給 keyWindow 并轉發給根視圖,根視圖按照視圖層級逐級遍歷子視圖,并且遍歷的過程中不斷判斷視圖范圍,并最終找到第一響應者。

事件傳遞給窗口或控件的后,就遞歸調用 hitTest:withEvent: 方法尋找更合適的 view
hitTest:withEvent: 方法中,會從上到下遍歷子視圖,并調用 subViewspointInside:withEvent: 方法,通過重寫 pointInside:withEvent: 方法,返回點擊區域是否在視圖上。如果找到子視圖則不斷調用其 hitTest:withEvent: 方法,以此類推。

在 hitTest:withEvent: 方法中返回 nil 的含義:

hitTest:withEvent: 方法中返回 nil 的意思是調用當前 hitTest:withEvent: 方法的 view 不是合適的 view,子控件也不是合適的 view,如果同級的兄弟控件也沒有合適的 view,那么最合適的 view 就是父控件。

pointInside:withEvent: 方法

pointInside:withEvent: 方法判斷子控件的點在不在當前 view 上(方法調用者的坐標系上)如果返回 YES,代表點在方法調用者的坐標系上;返回 NO 代表點不在方法調用者的坐標系上,那么方法調用者也就不能處理事件。

查找第一響應者傳遞過程:

  • 如果當前 view 是控制器的 view,那么控制器就是上一個響應者,事件就傳遞給控制器;
  • 如果當前 view 不是控制器的 view,那么父視圖就是當前 view 的上一個響應者,事件就傳遞給它的父視圖。
  • 在視圖層次結構的最頂級視圖,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給 window 對象進行處理。
  • 如果 window 對象也不處理,則其將事件或消息傳遞給 UIApplication 對象。
  • 如果 UIApplication 也不能處理該事件或消息,則將其丟棄。

事件攔截

有時候想讓指定視圖來響應事件,不再向其子視圖繼續傳遞事件,可以通過重寫 hitTest:withEvent: 方法。在執行到方法后,直接將該視圖返回,而不再繼續遍歷子視圖,這樣響應者鏈的終端就是當前視圖。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return self;
}

實際開發中可能會遇到一些特殊的交互需求,需要定制視圖對于事件的響應。例如下面 Tabbar 的這種情況,中間的圓形按鈕是底部 Tabbar上的控件,而 Tabbar 是添加在控制器根視圖中的。默認情況下我們點擊圖中紅色方框中按鈕的區域,會發現按鈕并不會得到響應。

image.png

很明顯,圖中紅色方框中按鈕是添加在 Tabbar 上面的,但是圖中紅色方框中按鈕的位置又超出了 Tabbar 的區域,當點擊紅色方框區域后,會發現紅色方框得不到響應。

分析:

  • 生成的觸摸事件首先傳到了 UIWindow,然后 UIWindow 將事件傳遞給控制器的根視圖 UILayoutContainerView
  • UILayoutContainerView 判斷自己可以響應觸摸事件,然后將事件傳遞給子視圖 Tabbar
  • 子視圖 Tabbar 判斷觸摸點并不在自己的坐標范圍內,因此返回 nil
  • 這時 UILayoutContainerView 將事件傳遞其他子視圖 UINavigationTransitionViewUINavigationTransitionView 判斷自己可以響應事件,就將事件時間傳遞給其子視圖 UIViewControllerWrapperView
  • UIViewControllerWrapperView 判斷自己可以響應事件,就將事件傳遞給子視圖 UITableViewController 控制器的 TableView
  • TableView 判斷自己可以響應事件,所以 UITableViewController 控制器的 TableView 就是第一響應者;

整個過程,事件根本沒有傳遞到圖中紅色方框中按鈕;

因此我們需要做的就是修改 TabbarhitTest:withEvent: 函數里面判斷點擊位置是否在 Tabbar 坐標范圍的的判斷條件,也就是需要重寫 Tabbar
pointInside:withEvent: 方法,判斷如果當前觸摸坐標在圖中紅色方框中按鈕上面,就返回 YES,否則返回 NO;這樣一來時間就會最終傳遞到圖中紅色方框中按鈕上面,來響應事件。

// TabBar
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // 將觸摸點坐標轉換到在 circleButton 上的坐標
    CGPoint pointTemp = [self convertPoint:point toView:_circleButton];
    // 若觸摸點在 cricleButton 上則返回 YES
    if ([_circleButton pointInside:pointTemp withEvent:event]) {
        return YES;
    }
    // 否則返回默認的操作
    return [super pointInside:point withEvent:event];
}

事件轉發

在開發過程中,經常會遇到子視圖顯示范圍超出父視圖的情況,這時候可以重寫該視圖的 pointInside:withEvent: 方法,將點擊區域擴大到能夠覆蓋所有子視圖。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil;
    
    CGFloat inset = 45.0f - 78.0f;
    CGRect touchRect = CGRectInset(self.bounds, inset, inset);
    
    if (CGRectContainsPoint(touchRect, point)) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容

  • 一、概念(載錄于:http://www.cnblogs.com/EricaMIN1987_IT/p/3837436...
    yuantao123434閱讀 8,409評論 6 152
  • Http協議詳解 標簽(空格分隔): Linux 聲明:本片文章非原創,內容來源于博客園作者MIN飛翔的HTTP協...
    Sivin閱讀 5,245評論 3 82
  • http協議有http0.9,http1.0,http1.1和http2三個版本,但是現在瀏覽器使用的是htt...
    一現_閱讀 1,881評論 0 3
  • HTTP是一個屬于應用層的面向對象的協議,由于其簡捷、快速的方式,適用于分布式超媒體信息系統。它于1990年提出,...
    greenlift閱讀 2,022評論 0 7
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,787評論 18 139