iOS事件響應鏈中Hit-Test View的應用

最近又看了遍蘋果的官方文檔《Event Handling Guide for iOS》,對事件響應鏈中的hit-test view 又多了些理解,個人覺的官方文檔對這塊講的非常簡單,很多東西都是點到為止,hit-test view的知識在項目的任何地方都用到了,但自己反而感知不到,接下來我會給大家講hit-test view的原理,以及項目中能解決痛點的三個應用 。

什么叫 hit-test view?文檔說:The lowest view in the view hierarchy that contains the touch point becomes the hit-test view,我的理解是:當你點擊了屏幕上的某個view,這個動作由硬件層傳導到操作系統,然后又從底層封裝成一個事件(Event)順著view的層級往上傳導,一直要找到含有這個點擊點層級最高(文檔說是最低,我理解是視圖樹的根節點,或者最靠近你的手指的view)的view來響應事件,這個view就是hit-test view

文檔中說,決定誰hit-test view是通過不斷遞歸調用view中的 - (UIView *)hitTest: withEvent: 方法和 -(BOOL)pointInside: withEvent: 方法來實現的,文段中的這段話太好理解,于是我仿照官方文檔中這張圖做了個Demo -> Github地址

apple doucument pic

重載圖中view的方法添加相應的log便于觀察:

//in every view .m overide those methods
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"進入A_View---hitTest withEvent ---");
    UIView * view = [super hitTest:point withEvent:event];
    NSLog(@"離開A_View--- hitTest withEvent ---hitTestView:%@",view);
    return view;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    NSLog(@"A_view--- pointInside withEvent ---");
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"A_view--- pointInside withEvent --- isInside:%d",isInside);
    return isInside;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"A_touchesBegan");
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    NSLog(@"A_touchesMoved");
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    NSLog(@"A_touchesEnded");
}

點擊圖中View_D,看下會發生什么

View_D log

一是發現touchesBegan、touchesMoved、touchesEnded這些方法都是發生在找到hit-test view之后,因為touch事件是針對能響應事件的確定的某個view,比如你手指劃出了scrollview的范圍,只要你不松手繼續滑動,scrollview依然會響應滑動事件繼續滾動;二是尋找hit-test view的事件鏈傳導了兩遍,而且兩次的調用堆棧是不同的,這點我有點搞不懂,為啥需要兩遍,查閱了很多資料也不知道原因,發現真機和模擬器以及不同的系統版本之間還會有些區別(此為真機iOS9),大家可以下載我的Demo進行測試與研究。

把這個尋找的邏輯換成代碼如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        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;
}

如果有某個view的兩個子view位置重疊,根據View Programming Guide for iOS文檔中說的 Visually, the content of a subview obscures all or part of the content of its parent view. If the subview is totally opaque, then the area occupied by the subview completely obscures the corresponding area of the parent. If the subview is partially transparent, the content from the two views is blended together prior to being displayed on the screen. Each superview stores its subviews in an ordered array and the order in that array also affects the visibility of each subview. If two sibling subviews overlap each other, the one that was added last (or was moved to the end of the subview array) appears on top of the other. 那最高層(邏輯最靠近手指的)view是view subviews數組的最后一個元素,只要尋找是從數組的第一個元素開始遍歷,hit-test view的邏輯依然是有效的。

找到hit-test view后,它會有最高的優先權去響應逐級傳遞上來的Event,如它不能響應就會傳遞給它的superview,依此類推,一直傳遞到UIApplication都無響應者,這個Event就會被系統丟棄了。

Hit-test view的應用舉例:

1、擴大UIButton的響應熱區

相信大家都遇到小圖button點擊熱區太小問題,之前我是用UIButton的setImage方法來設置圖片解決,但是調起坐標就坑了,得各種計算不說,寫出的代碼還很難看不便于維護,如果我們用用hit-test view的知識你就能輕松地解決這個問題。

重載UIButton的-(BOOL)pointInside: withEvent:方法,讓Point即使落在Button的Frame外圍也返回YES。

//in custom button .m
//overide this method
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
}

CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
    CGRect hitTestingBounds = bounds;
    if (minimumHitTestWidth > bounds.size.width) {
        hitTestingBounds.size.width = minimumHitTestWidth;
        hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2;
    }
    if (minimumHitTestHeight > bounds.size.height) {
        hitTestingBounds.size.height = minimumHitTestHeight;
        hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2;
    }
    return hitTestingBounds;
}

2、子view超出了父view的bounds響應事件

項目中常常遇到button已經超出了父view的范圍但仍需可點擊的情況,比如自定義Tabbar中間的大按鈕,如下圖閑魚的app,點擊超出Tabbar bounds的區域也需要響應,此時重載父view的-(UIView *)hitTest: withEvent:方法,去掉點擊必須在父view內的判斷,然后子view就能成為 hit-test view用于響應事件了。

xiansyu
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    /**
     *  此注釋掉的方法用來判斷點擊是否在父View Bounds內,
     *  如果不在父view內,就會直接不會去其子View中尋找HitTestView,return 返回
     */
//    if ([self pointInside:point withEvent:event]) {
        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;
}

3、ScrollView page滑動

這是app store 應用的app封面預覽功能

scrollview page

上圖的交互常常見于很多海報、封面展示的app,實現這個交互的方法有很多,但選擇用scrollView來橫向滑動來做是最簡單的,讓scrollview.pageEnabel = YES,就有了翻頁的感覺,但這樣scoreView的實際可滑動區域就只有一張照片那么寬,如果想讓邊側留出的距離(藍色框部分)響應滑動事件的話應該怎么辦呢?這個時候又可以用到hit-test view的知識了,在scrollview的父view中把藍色部分的事件都傳遞給scrollView就可以了,具體看下面代碼:

//in scrollView.superView .m

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    UIView *hitTestView = [super hitTest:point withEvent:event];
    if (hitTestView) {
        hitTestView = self.scrollView;
    }
    return hitTestView;
}

總結

事件響應鏈是UI層一個非常重要的概念,想做出非常棒的交互和動畫,必須對其有一個深入的理解。我列舉的只是我在開發中遇到的一些問題,如果有其他的對事件響應鏈的應用希望大家和我一起交流探討。

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

推薦閱讀更多精彩內容

  • 好奇觸摸事件是如何從屏幕轉移到APP內的?困惑于Cell怎么突然不能點擊了?糾結于如何實現這個奇葩響應需求?亦或是...
    Lotheve閱讀 57,666評論 51 601
  • 在iOS開發中經常會涉及到觸摸事件。本想自己總結一下,但是遇到了這篇文章,感覺總結的已經很到位,特此轉載。作者:L...
    WQ_UESTC閱讀 6,063評論 4 26
  • 用戶以多種方式操縱他們的iOS設備,例如觸摸屏幕或搖動設備。 iOS會解釋用戶何時以及如何操作硬件并將此信息傳遞到...
    坤坤同學閱讀 4,018評論 7 19
  • 值得注意的事,當一個view上面有多個手勢時,touch是無序的 1事件是怎么樣產生與傳遞的?(由上至下的過程) ...
    池鵬程閱讀 2,292評論 0 8
  • 一. Hit-Testing 什么是Hit-Testing?對于觸摸事件, window首先會嘗試將事件交給事件觸...
    面糊閱讀 840評論 0 50