iOS之hitTest

前言

我負(fù)責(zé)努力氯质,其余交給運(yùn)氣募舟。

寫這篇文章,是因?yàn)橹皩懥艘黄绾谓鉀Qbutton點(diǎn)擊范圍過小的文章闻察,然后評論區(qū)小伙伴說hitTest也可以拱礁,然后我就查了一下hitTest,發(fā)現(xiàn)真的有其牛逼之處辕漂,所以整理一下呢灶。

一、什么是hitTest

官方文檔中介紹(若理解翻譯的不對還請指正):- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

  • Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.
    自我理解:返回所能包含point的view和view.subviews中最后的一個(gè)view钉嘹。

  • point:A point specified in the receiver’s local coordinate system (bounds).
    自我理解:在接收器的局部坐標(biāo)系(界)中指定的點(diǎn)鸯乃。

  • event:The event that warranted a call to this method. If you are calling this method from outside your event-handling code, you may specify nil.
    自我理解:此方法可以正常響應(yīng)的事件。如果從觸發(fā)事件之外調(diào)用此方法跋涣,則可以指定為nil缨睡。

  • Return Value:The view object that is the farthest descendent of the current view and contains point. Returns nil if the point lies completely outside the receiver’s view hierarchy.
    自我理解:所能包含point的view和view.subviews中最后的一個(gè)view。如果point完全位于視圖層次結(jié)構(gòu)之外仆潮,則返回nil

總的來說就是:該方法會被系統(tǒng)調(diào)用(可重寫)宏蛉,在視圖的層次結(jié)構(gòu)中尋找到一個(gè)最適合的 view (理解為最上層view)來響應(yīng)觸摸事件,如果返回為nil性置,即事件有可能被丟棄产舞。

二、hitTest的調(diào)用順序

觸摸事件尋找最佳響應(yīng)者,即hitTest 的調(diào)用順序大致如下:

touch(UIEvent)->UIApplication->UIWindow->window.subviews->...->view
  1. 當(dāng)App接收觸摸事件時(shí)樟蠕,主線程的runloop被喚醒,觸發(fā)source1回調(diào)屏歹。source1回調(diào)又觸發(fā)了一個(gè)source0回調(diào),將接收到的觸摸事件(IOHIDEvent對象)封裝成UIEvent對象之碗,此時(shí)APP將正式開始對于觸摸事件的響應(yīng)蝙眶。source0回調(diào)將觸摸事件添加到UIApplication的事件隊(duì)列中。
  2. UIApplication會從事件隊(duì)列中取出最早的事件進(jìn)行分發(fā)處理褪那,首先將事件傳遞給窗口對象(UIWindow)幽纷,如果有多個(gè)UIWindow對象,則先選擇最后加上的UIWindow對象博敬。
  3. UIWindow會調(diào)用其hitTest:withEvent:方法在視圖(UIView)層次結(jié)構(gòu)中找到一個(gè)最合適的UIView來處理觸摸事件友浸。
三、觸摸事件的傳遞順序

通過hitTest我們已經(jīng)找到了最佳響應(yīng)者偏窝,下面要做的事就是讓這個(gè)最佳響應(yīng)者響應(yīng)觸摸事件收恢。這個(gè)最佳響應(yīng)者對于觸摸事件擁有決定權(quán),它可以決定是自己獨(dú)自響應(yīng)這個(gè)事件祭往,也可以自己響應(yīng)之后還把它傳遞給其他響應(yīng)者伦意。

事件傳遞順序大致為:

view -> superView ...- > UIViewController.view -> UIViewController -> UIWindow -> UIApplication -> 事件丟棄

文字說明:

  • 1、 首先由 view 來嘗試處理事件硼补,如果他處理不了驮肉,事件將被傳遞到他的父視圖superview
  • 2、superview 也嘗試來處理事件括勺,如果他處理不了缆八,繼續(xù)傳遞他的父視圖
    UIViewcontroller.view
  • 3、UIViewController.view嘗試來處理該事件疾捍,如果處理不了,將把該事件傳遞給UIViewController
  • 4栏妖、UIViewController嘗試處理該事件乱豆,如果處理不了,將把該事件傳遞給主窗口Window
  • 5吊趾、主窗口Window嘗試來處理該事件宛裕,如果處理不了,將傳遞給應(yīng)用單例Application
  • 6论泛、如果Application也處理不了揩尸,則該事件將會被丟棄。

注:
響應(yīng)者對于事件的響應(yīng)和傳遞都是在touchesBegan:withEvent:這個(gè)方法中完成的屁奏。該方法默認(rèn)的實(shí)現(xiàn)是將該方法沿著響應(yīng)鏈往下傳遞岩榆。
響應(yīng)者對于接收到的事件有三種操作:

  • 1.默認(rèn)的操作。不攔截,事件會沿著默認(rèn)的響應(yīng)鏈自動(dòng)往下傳遞勇边。
  • 2.攔截犹撒,不再往下分發(fā)事件,重寫touchesBegan:withEvent:方法粒褒,不調(diào)用父類的touchesBegan:withEvent:方法识颊。
  • 3.不攔截,繼續(xù)往下分發(fā)事件奕坟,重新touchesBegan:withEvent:方法祥款,并調(diào)用父類的touchesBegan:withEvent:方法。
四月杉、hitTest的實(shí)現(xiàn)思路

首先看一下系統(tǒng)的hitTest是怎么調(diào)用的镰踏,看代碼:

#import "ViewController.h"
#import "AView.h"
#import "BView.h"
#import "CView.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    AView* aView = [[AView alloc] init];
    aView.frame = CGRectMake(100, 100, 100, 100);
    aView.backgroundColor = [UIColor orangeColor];
    
    BView* bView = [[BView alloc] init];
    bView.frame = CGRectMake(100, 100, 80, 80);
    bView.backgroundColor = [UIColor blueColor];
    
    CView* cView = [[CView alloc] init];
    cView.frame = CGRectMake(100, 100, 60, 60);
    cView.backgroundColor = [UIColor redColor];
    
    [self.view addSubview:aView];
    [self.view addSubview:bView];
    [self.view addSubview:cView];
    
    for (UIView* hitView in self.view.subviews) {
        NSLog(@"for %@",[hitView class]);
    }
}

@end
#import "AView.h"

@implementation AView

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    NSLog(@"-----hitTest star AView-----");
    UIView* view = [super hitTest:point withEvent:event];
    NSLog(@"-----hitTest end AView-----");
    return view;
}

@end

AView、BView沙合、CView都是繼承UIView奠伪,都重寫了它們的hitTest。在A首懈、B绊率、C之外點(diǎn)擊,運(yùn)行結(jié)果如下:

運(yùn)行結(jié)果

我們可以看到:for in循環(huán)的打印結(jié)果究履,因?yàn)?code>subviews是一個(gè)數(shù)組滤否,所以有序,順序?yàn)?code>addSubview決定最仑;而hitTest打印結(jié)果很明顯藐俺,官方文檔說是尋找最遠(yuǎn)View,看打印結(jié)果我理解是就是從subviews最后一個(gè)開始找泥彤,也就是最上層的view(雖然subviews可以說是同一層級欲芹,因?yàn)槎荚?code>view上,但是后添加的確實(shí)會覆蓋先添加的view吟吝,所以個(gè)人認(rèn)為哪怕subviews都屬于view層級菱父,但是他們之間依然是后添加的相對來說在最上層。)

且常見的視圖不響應(yīng)事件不外乎如下幾種情況:

1剑逃、view.userInteractionEnabled = NO;
2浙宜、view.hidden = YES;
3、view.alpha < 0.05;
4蛹磺、view 超出 superview 的 bounds;

那么hitTest 就可根據(jù)上面 結(jié)果 大概模擬下 hitTest 方法的大概實(shí)現(xiàn):

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 如果交互未打開粟瞬,或者透明度小于0.05 或者 視圖被隱藏
    if (self.userInteractionEnabled == NO || self.alpha < 0.05 || self.hidden == YES)
    {
        return nil;
    }
    // 如果 touch 的point 在 self 的bounds 內(nèi)
    if ([self pointInside:point withEvent:event])
    {
        NSInteger count = self.subviews.count;
        for ( int i = 0; i < count; I++)
        {
            UIView* subView = self.subviews[count - 1 - I];
            //進(jìn)行坐標(biāo)轉(zhuǎn)化
            CGPoint coverPoint = [subView convertPoint:point fromView:self];
            // 調(diào)用子視圖的 hitTest 重復(fù)上面的步驟。找到了萤捆,返回hitTest view ,沒找到返回有自身處理
            UIView *hitTestView = [subView hitTest:coverPoint withEvent:event];
            if (hitTestView)
            {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

很多文章裙品,直接用for in 遍歷 subviews俗批,應(yīng)該是不對的。步驟文字說明:

  • 1清酥、首先在當(dāng)前視圖的hitTest 方法中調(diào)用pointInside 方法判斷觸摸點(diǎn)是否在當(dāng)前視圖內(nèi)
  • 2扶镀、若pointInside 方法返回NO,說明觸摸點(diǎn)不在當(dāng)前視圖內(nèi)焰轻,則當(dāng)前視圖的hitTest 返回nil 臭觉,該視圖不處理該事件
  • 3、若pointInside 方法返回YES辱志,說明觸摸點(diǎn)在當(dāng)前視圖內(nèi)蝠筑,則從最上層的子視圖開始(即從subviews 數(shù)組的末尾向前遍歷),遍歷當(dāng)前視圖的所有子視圖揩懒,調(diào)用子視圖的hitTest 方法重復(fù)步驟1-3
  • 4什乙、直到有子視圖的hitTest 方法返回非空對象或者全部子視圖遍歷完畢
  • 5、若第一次有子視圖的hitTest 方法返回非空對象已球,則當(dāng)前視圖的hitTest 方法就返回此對象臣镣,處理結(jié)束
  • 6、若所有子視圖的hitTest 方法都返回nil智亮,則當(dāng)前視圖的hitTest 方法返回當(dāng)前視圖本身忆某,最終由該對象處理觸摸事件

代碼文字描述的可能比較復(fù)雜,下面圖文再描述一遍阔蛉,如圖:

視圖

A弃舒、B橘黃色View在白色View上,C状原、D在A上聋呢,E、F在B上:
層次結(jié)構(gòu)圖

addSubview順序?yàn)锳颠区、B削锰、C、D瓦呼、E喂窟、F;

結(jié)果:
點(diǎn)擊白色view區(qū)域:B->A
點(diǎn)擊F:B->F
點(diǎn)擊E:B->F->E
點(diǎn)擊C:B->A->D->C
點(diǎn)擊D:B->A->D

結(jié)論與之前相同央串,hitTest 一直是在找包含觸點(diǎn)的最上層View(subviews最后一個(gè):最后add的View)

五、hitTest的運(yùn)用場景
1碗啄、事件穿透

我們可以讓上層響應(yīng)事件的同時(shí)质和,下層view同時(shí)響應(yīng)。好像不太能碰得到稚字,暫不舉例說明(感覺情況有點(diǎn)多...不同層次結(jié)果可能解決方法是不一樣的饲宿,有需要的可以留言...)厦酬。

2、子視圖超出父視圖 范圍
我是盜圖小能手

類似與上圖:發(fā)布按鈕已然已經(jīng)超出tabbar的范圍瘫想,那么該按鈕是如何響應(yīng)點(diǎn)擊事件的仗阅?
解決辦法:

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


     //將當(dāng)前tabbar的觸摸點(diǎn)轉(zhuǎn)換坐標(biāo)系,轉(zhuǎn)換到中間按鈕的身上国夜,生成一個(gè)新的點(diǎn)
     CGPoint newP = [self convertPoint:point toView:self.centerBtn];

      //判斷如果這個(gè)新的點(diǎn)是在中間按鈕身上减噪,那么處理點(diǎn)擊事件最合適的view就是中間按鈕
      if ( [self.centerBtn pointInside:newP withEvent:event]) 
      {
            return self.centerBtn;
       }


    return [super hitTest:point withEvent:event];

}//重寫hitTest方法,去監(jiān)聽中間按鈕的點(diǎn)擊车吹,目的是為了讓凸出的部分點(diǎn)擊也有反應(yīng)
總結(jié):

hitTest其實(shí)最牛逼的地方在于筹裕,我們可以更好的了解一個(gè)事件觸發(fā)后從App一直到響應(yīng)的過程;我們也可以針對觸點(diǎn)重寫hitTest窄驹,讓其在一定范圍內(nèi)響應(yīng)某些事件朝卒;最后就是解決明明在觸點(diǎn)下但是超出父視圖事件不響應(yīng)的問題。(有的時(shí)候發(fā)現(xiàn)很多東西乐埠,都是知其然不知其所以然抗斤,希望自己和大家,慢慢的探索丈咐,做到知其然知其所以然... 所以若有問題瑞眼,歡迎并感謝大家指正)

提問:

最上面的輸出結(jié)果大家也看到了,hitTest實(shí)際上走了兩遍扯罐,不知道為什么负拟,大家有知道的請留言哈...

參考:

參考文章1
參考文章2
參考文章3 iOS中觸摸事件傳遞和響應(yīng)原理
官方文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市歹河,隨后出現(xiàn)的幾起案子掩浙,更是在濱河造成了極大的恐慌,老刑警劉巖秸歧,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厨姚,死亡現(xiàn)場離奇詭異,居然都是意外死亡键菱,警方通過查閱死者的電腦和手機(jī)谬墙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來经备,“玉大人拭抬,你說我怎么就攤上這事∏置桑” “怎么了造虎?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長纷闺。 經(jīng)常有香客問我算凿,道長份蝴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任氓轰,我火速辦了婚禮婚夫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘署鸡。我一直安慰自己案糙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布储玫。 她就那樣靜靜地躺著侍筛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪撒穷。 梳的紋絲不亂的頭發(fā)上匣椰,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天,我揣著相機(jī)與錄音端礼,去河邊找鬼禽笑。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蛤奥,可吹牛的內(nèi)容都是我干的佳镜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼凡桥,長吁一口氣:“原來是場噩夢啊……” “哼蟀伸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起缅刽,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤啊掏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后衰猛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體迟蜜,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年啡省,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了娜睛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,146評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡卦睹,死狀恐怖畦戒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情结序,我是刑警寧澤兢交,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站笼痹,受9級特大地震影響配喳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凳干,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一晴裹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧救赐,春花似錦涧团、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至预厌,卻和暖如春阿迈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背轧叽。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工苗沧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人炭晒。 一個(gè)月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓待逞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親网严。 傳聞我的和親對象是個(gè)殘疾皇子识樱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評論 2 356

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