iOS 點(diǎn)擊事件分發(fā)機(jī)制

本文將簡單介紹 iOS 的點(diǎn)擊事件( TouchEvents )分發(fā)機(jī)制和一些使用場景约郁。詳解請(qǐng)看參考部分筐赔。

從以下兩個(gè)方面介紹:

1. 尋找 hit-TestView 的過程(事件的傳遞過程)
2. 響應(yīng)鏈(事件的響應(yīng)過程)

一些應(yīng)用場景:

  1. 一個(gè)內(nèi)容是圓形的按鈕(指定只允許視圖的 frame 內(nèi)某個(gè)區(qū)域可以響應(yīng)事件)
  2. tabBar 上中間凸起的按鈕(讓超出父視圖邊界的子視圖區(qū)域也能響應(yīng)事件)

開始

尋找 hit-TestView 的過程的總結(jié)


在 iOS 中阅束,當(dāng)產(chǎn)生一個(gè) touch 事件之后(點(diǎn)擊屏幕),通過 hit-Testing 找到觸摸點(diǎn)所在的 View( hit-TestView )。尋找過程總結(jié)如下(默認(rèn)情況下):

尋找順序如下:

1. 從視圖層級(jí)最底層的 window 開始遍歷它的子 View犁柜。
2. 默認(rèn)的遍歷順序是按照 UIView 中 Subviews 的逆順序。
3. 找到 hit-TestView 之后堂淡,尋找過程就結(jié)束了馋缅。

確定一個(gè) View 是不是 hit-TestView 的過程如下:

1. 如果 View 的 userInteractionEnabled = NO扒腕,enabled = NO( UIControl ),或者 alpha <= 0.01萤悴, hidden = YES 等情況的時(shí)候瘾腰,直接返回 nil(不再往下判斷)。
2. 如果觸摸點(diǎn)不在 view 中覆履,直接返回 nil蹋盆。
3. 如果觸摸點(diǎn)在 view 中,逆序遍歷它的子 View 硝全,重復(fù)上面的過程栖雾。
4. 如果 view 的 子view 都返回 nil(都不是 hit-TestVeiw ),那么返回自身(自身是 hit-TestView )柳沙。

UIView 提供兩個(gè)方法來來確定 hit-TestView:

// 返回一個(gè) hit-TestView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

// 判斷觸摸點(diǎn)是否在 view 中
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; // default returns YES if point is in bounds 

hitTest:withEvent: 方法的具體實(shí)現(xiàn)可以寫成這樣:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    //1
    if (self.alpha <= 0.01 || !self.userInteractionEnabled || self.hidden) {
        return nil;
    }
    //2
    if (![self pointInside:point withEvent:event]) {
        return nil;
    }
    //3
    NSEnumerator *enumerator = [self.subviews reverseObjectEnumerator];
    for (UIView *subview in enumerator) {
        UIView *hitTestView = [subview hitTest:point withEvent:event];
        if (hitTestView) {
            return hitTestView;
        }
    }
    //4
    return self;
}

看完了理論岩灭,再結(jié)合實(shí)際,這樣就好理解了赂鲤。

以下講解基于這樣的視圖層級(jí)結(jié)構(gòu)

視圖層級(jí)結(jié)構(gòu).png
+-UIWindow
  +-MainView
    +-RedView
    | +-UIButton
    | +-UIButtonLabel
    +-YellowView
      +-UILabel

下面是測試過程中的一些日志(請(qǐng)結(jié)合上面的總結(jié)來分析):
ps:在實(shí)際項(xiàng)目中點(diǎn)擊一次視圖會(huì)打印兩次下面的信息中間插入一次 UIStatusBarWindow 的信息噪径,目前也不知道什么原因,如果有知道的請(qǐng)分享出來数初,非常感謝找爱!

點(diǎn)擊紅色 View 時(shí):

UIWindow:[hitTest:withEvent:]
----MainView:[hitTest:withEvent:]
--------YellowView:[hitTest:withEvent:]
--------RedView:[hitTest:withEvent:]
------------UIButton:[hitTest:withEvent:]
hit-TestView is RedView !

分析:

1. 先使用了 YellowView 的 [hitTest:withEvent:] 方法可以看出:默認(rèn)的遍歷順序是按照 UIView(MainView) 中 Subviews 的逆順序。
2. 當(dāng)判斷 YellowView 是不是 hit-TestView 的時(shí)候泡孩,判斷觸摸點(diǎn)不在 YellowView 上就不會(huì)再遍歷它的子 View(UILabel) 了车摄。
3. 觸摸點(diǎn)在 RedView 上,所以會(huì)繼續(xù)遍歷它的子 View( UIButton )仑鸥,觸摸點(diǎn)不在 UIButton 上吮播,所以返回 nil ( UIButton 不是 hit-TestView ),所以返回它本身( 是 hit-TestView )眼俊。

點(diǎn)擊灰色 button 時(shí):

UIWindow:[hitTest:withEvent:]
----MainView:[hitTest:withEvent:]
--------YellowView:[hitTest:withEvent:]
--------RedView:[hitTest:withEvent:]
------------UIButton:[hitTest:withEvent:]
----------------UIButtonLabel:[hitTest:withEvent:]
hit-TestView is UIButton !

根據(jù)上面的分析意狠,觸摸點(diǎn)在 RedView 上,所以會(huì)繼續(xù)遍歷它的子 View( UIButton )疮胖,觸摸點(diǎn)在 UIButton 上环戈,所以返回它本身( 是 hit-TestView )。

點(diǎn)擊黃色 View 時(shí):

UIWindow:[hitTest:withEvent:]
----MainView:[hitTest:withEvent:]
--------YellowView:[hitTest:withEvent:]
------------UILabel:[hitTest:withEvent:]
hit-TestView is YellowView !

分析:

觸摸點(diǎn)在 YellowView 上澎灸,遍歷它的子 View( UILabel )院塞,觸摸點(diǎn)不在 UILabel 上,所以返回 nil性昭,所以 YellowView 是 hit-TestView拦止。找到 hit-TestView 后,就不再檢查 RedView 了糜颠。

點(diǎn)擊 label 時(shí):

UIWindow:[hitTest:withEvent:]
UIWindow pointInside:1
----MainView:[hitTest:withEvent:]
MainView pointInside:1
--------YellowView:[hitTest:withEvent:]
YellowView pointInside:1
------------UILabel:[hitTest:withEvent:]
hit-TestView is YellowView !

分析:

觸摸點(diǎn)在 YellowView 上创泄,所以遍歷它的子 View( UILabel )艺玲,但是 UILabel 的 userInteractionEnabled = NO括蝠,所以返回 nil鞠抑,這個(gè)時(shí)候其實(shí)還沒有判斷觸摸點(diǎn)是不是在 UILabel上。

響應(yīng)鏈


找到 hit-TestView 之后忌警,事件就交給它來處理搁拙,hit-TestView 就是 firstResponder(第一響應(yīng)者),如果它無法響應(yīng)事件(不處理事件)法绵,則把事件交給它的 nextResponder(下一個(gè)響應(yīng)者)箕速,直到有處理事件的響應(yīng)者或者結(jié)束(傳遞到 AppDelegate 為止)。這一系列的響應(yīng)者和事件的傳遞方向就是響應(yīng)鏈(很形象)朋譬。在響應(yīng)鏈中盐茎,所有響應(yīng)者的基類都是 UIResponder,也就是說所有可以響應(yīng)事件的類都是 UIResponder 的子類徙赢,UIApplication/UIView/UIViewController 都是 UIResponder 的子類字柠。

ps: View 處理事件的方式有手勢或者重寫 touchesEvent 方法或者利用系統(tǒng)封裝好的組件( UIControls )。

只要知道 nextResponder 是什么狡赐,就可以確定響應(yīng)鏈了窑业。

nextResponder 查找過程如下:

1. UIView 的 nextResponder 是直接管理它的 UIViewController (也就是 VC.view.nextResponder = VC ),如果當(dāng)前 View 不是 ViewController 直接管理的 View枕屉,則 nextResponder 是它的 superView( view.nextResponder = view.superView )常柄。
2. UIViewController 的 nextResponder 是它直接管理的 View 的 superView( VC.nextResponder = VC.view.superView )。
3. UIWindow 的 nextResponder 是 UIApplication 搀擂。
4. UIApplication 的 nextResponder 是 AppDelegate西潘。

下面是測試過程中的一些日志:

點(diǎn)擊紅色 View 時(shí):

------------------The Responder Chain------------------
RedView
|
MainView
|
ViewController
|
UIWindow
|
UIApplication
|
AppDelegate
------------------The Responder Chain------------------

分析:

1. RedView 不是 UIViewController 管理的 View,所以它的 nextResponder 是它的 superView( MainView )哨颂。
2. MainView 是 UIViewController 管理的 View喷市,所以它的 nextResponder 是管理它的 ViewController。
3. ViewController 的 nextResponder 是它管理的 MainView 的superView( UIWindow )咆蒿。
4. UIWindow 的 nextResponder 是 UIApplication东抹。
5. UIApplication 的 nextResponder 是 AppDelegate。

一般來說沃测,某個(gè) UIResponder 的子類想要自己處理一些事件缭黔,就需要重寫它的這些方法:

- (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;

響應(yīng)鏈上的某個(gè)對(duì)象處理事件之后可以選擇讓事件傳遞繼續(xù)下去或者終止,如果需要讓事件繼續(xù)傳遞下去則需要在 touchesBegan 方法里面蒂破,調(diào)用父類對(duì)應(yīng)的方法:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // Responding to Touch Events
    [super touchesBegan:touches withEvent:event];
}

下面分享一個(gè)實(shí)際開發(fā)中的應(yīng)用場景


場景:自定義一個(gè)這樣的 tabBar馏谨,中間有個(gè)凸起一丟丟的 item。

自定義 tabBar.png

UI的實(shí)現(xiàn):自定義一個(gè)大小和 tabBar 一樣的 View 覆蓋在 tabBar 上附迷,然后然后中間的 item 超出自定義 View 的邊界惧互,讓自定義的 View 的 clipsToBounds 為 NO哎媚,把超出邊界的部分也顯示出來。

分析:

根據(jù)尋找 hit-TestView 過程的原理可以知道喊儡,如果點(diǎn)擊超出邊界的部分(凸起的那一丟丟)是不能響應(yīng)事件的拨与。

解決過程:

1. 打印view的層級(jí)

+-UIWindow
  +-UILayoutContainerView
    +-UITransitionView
    | +-UIViewControllerWrapperView
    | +-UILayoutContainerView
    | +-UINavigationTransitionView
    | | +-UIViewControllerWrapperView
    | | +-UIView
    | +-UINavigationBar
    | +-_UINavigationBarBackground
    | | +-_UIBackdropView
    | | | +-_UIBackdropEffectView
    | | | +-UIView
    | | +-UIImageView
    | +-UINavigationItemView
    | | +-UILabel
    | +-_UINavigationBarBackIndicatorView
    +-MSCustomTabBar
      +-_UITabBarBackgroundView
      | +-_UIBackdropView
      | +-_UIBackdropEffectView
      | +-UIView
      +-UITabBarButton
      +-UITabBarButton
      +-UITabBarButton
      +-UITabBarButton
      +-UIImageView
      +-MSTabBarView
        +-UIButton
        | +-UIImageView
        +-MSVerticalCenterButton
        | +-UIImageView
        | +-UIButtonLabel
        +-MSVerticalCenterButton
        | +-UIImageView
        | +-UIButtonLabel
        +-MSVerticalCenterButton
        | +-UIImageView
        | +-UIButtonLabel
        +-MSVerticalCenterButton
          +-UIImageView
          +-UIButtonLabel

分析:(有點(diǎn)長,不過只要看 MSCustomTabBar 那部分就可以了)

  • MSTabBarView 就是自定義覆蓋在 MSCustomTabBar 上面的 View艾猜,它的子 ViewUIButton 就是中間凸起一丟丟的 item买喧。
  • 如果我們點(diǎn)擊了 tabBar 的內(nèi)部,尋找 hit-TestView 的時(shí)候是會(huì)查詢自定義的 MSTabBarView 的匆赃,從而它的子 View 也會(huì)被查詢淤毛,所以只要觸摸點(diǎn)在 view 的范圍內(nèi)就可以響應(yīng)事件了,所以沒有任何問題算柳。
  • 如果我們點(diǎn)擊了凸起的那一丟丟部分低淡,尋找 hit-TestView 的時(shí)候,查詢到 MSCustomTabBar 之后瞬项,由于觸摸點(diǎn)不在它的內(nèi)部蔗蹋,所以不會(huì)查詢它的子 View( MSTabBarView ),所以凸起的那一丟丟是響應(yīng)不了事件的滥壕。所以我們需要重寫 MSCustomTabBar 的 [hitTest:withEvent:] 方法纸颜。

分析 view 的層級(jí)主要是為了確定在哪里重寫 [hitTest:withEvent:] 方法。

2. 重寫 [hitTest:withEvent:] 方法胁孙,讓超出 tabBar 的那部分也能響應(yīng)事件

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 先使用默認(rèn)的方法來尋找 hit-TestView
    UIView *result = [super hitTest:point withEvent:event];
    // 如果 result 不為 nil,說明觸摸事件發(fā)生在 tabbar 里面称鳞,直接返回就可以了
    if (result) {
        return result;
    }
    // 到這里說明觸摸事件不發(fā)生在 tabBar 里面
    // 這里遍歷那些超出的部分就可以了涮较,不過這么寫比較通用。
    for (UIView *subview in self.tabBarView.subviews) {
        // 把這個(gè)坐標(biāo)從tabbar的坐標(biāo)系轉(zhuǎn)為subview的坐標(biāo)系
        CGPoint subPoint = [subview convertPoint:point fromView:self];
        result = [subview hitTest:subPoint withEvent:event];
        // 如果事件發(fā)生在subView里就返回
        if (result) {
            return result;
        }
    }
    return nil;
}

分析:

如果觸摸點(diǎn)在 tabBar 里面的時(shí)候冈止,使用默認(rèn)方法就可以找到 hit-TestView 了狂票,所以先使用 [super hitTest:point withEvent:event] (因?yàn)槲覀兪侵貙懛椒ǎ允褂?super 就是使用原始的方法)來尋找熙暴,如果找不到闺属,說明觸摸點(diǎn)不在 tabBar 里面,這個(gè)時(shí)候就需要我們手動(dòng)的判斷觸摸點(diǎn)在不在超出的那一丟丟里面了周霉。(其實(shí)只要判斷凸起的 View 就可以了掂器,不過遍歷所有 子View 比較通用,如果有多個(gè)凸起的 view 也可以這么寫)俱箱,先把坐標(biāo)轉(zhuǎn)換為 子View 的坐標(biāo)(這樣才能使用默認(rèn)的 [pointInside:withEvent:] 方法來判斷觸摸點(diǎn)是否在 view 里面)国瓮,然后遍歷 子View 調(diào)用默認(rèn)的 [hitTest:withEvent:] 方法,如果觸摸點(diǎn)在 view 的內(nèi)部,就能找到 hit-TestView乃摹,如果遍歷完所有 子View 都沒有找到 hit-TestView 說明觸摸點(diǎn)也不在凸起的那一丟丟里面禁漓,然后返回 nil 就可以了。

分享一個(gè)demo


非矩形區(qū)域的點(diǎn)擊:比如一個(gè)圓角為寬度一半的Button孵睬,只有點(diǎn)擊圓形區(qū)域才會(huì)響應(yīng)事件播歼。

圓形的 button.png

分析:

因?yàn)橛|摸點(diǎn)在 View 內(nèi),想要限制 view 內(nèi)的點(diǎn)擊區(qū)域肪康,所以重寫 button 的 [pointInside:withEvent:] 這個(gè)方法荚恶。如下:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    // 圓形區(qū)域的半徑
    CGFloat maxRadius = CGRectGetWidth(self.frame)/2;
    // 觸摸點(diǎn)相對(duì)圓心的坐標(biāo)
    CGFloat xOffset = point.x - maxRadius;
    CGFloat yOffset = point.y - maxRadius;
    // 觸摸點(diǎn)的半徑
    CGFloat radius = sqrt(xOffset * xOffset + yOffset * yOffset);

    return radius <= maxRadius;
}

demo 比較簡單,稍微動(dòng)手一下就可以掌握了磷支。

參考:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市食寡,隨后出現(xiàn)的幾起案子雾狈,更是在濱河造成了極大的恐慌,老刑警劉巖抵皱,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件善榛,死亡現(xiàn)場離奇詭異,居然都是意外死亡呻畸,警方通過查閱死者的電腦和手機(jī)移盆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伤为,“玉大人咒循,你說我怎么就攤上這事〗视蓿” “怎么了叙甸?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長位衩。 經(jīng)常有香客問我裆蒸,道長,這世上最難降的妖魔是什么糖驴? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任僚祷,我火速辦了婚禮,結(jié)果婚禮上贮缕,老公的妹妹穿的比我還像新娘辙谜。我一直安慰自己,他們只是感情好跷睦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布筷弦。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪烂琴。 梳的紋絲不亂的頭發(fā)上爹殊,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音奸绷,去河邊找鬼梗夸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛号醉,可吹牛的內(nèi)容都是我干的反症。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼畔派,長吁一口氣:“原來是場噩夢啊……” “哼铅碍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起线椰,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤胞谈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后憨愉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烦绳,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年配紫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了径密。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡躺孝,死狀恐怖享扔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情括细,我是刑警寧澤伪很,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站奋单,受9級(jí)特大地震影響锉试,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜览濒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一呆盖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贷笛,春花似錦应又、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尤筐。三九已至,卻和暖如春洞就,著一層夾襖步出監(jiān)牢的瞬間盆繁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工旬蟋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留油昂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓倾贰,卻偏偏與公主長得像冕碟,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子匆浙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容