深入iOS事件處理層次及原理分析、響應(yīng)鏈

1. iOS事件有哪一些

運(yùn)動(dòng)事件

  • 傳感器、計(jì)數(shù)器、陀螺儀

遠(yuǎn)程控制事件

  • 線控耳機(jī)

觸摸事件

  • 本文核心分析

2. 事件傳遞和響應(yīng)

2.1 原理分析

  • 當(dāng)我們手指觸碰到屏幕的時(shí)候,事件傳遞和響應(yīng)的流程是怎么樣的呢
  • 事件的流程圖
流程.png
  • IOKit.framework 為系統(tǒng)內(nèi)核的庫(kù)
  • SpringBoard.app 相當(dāng)于手機(jī)的桌面
  • Source1 主要接收系統(tǒng)的消息
  • Source0 - UIApplication - UIWindow
  • 從UIWindow 開(kāi)始步驟,見(jiàn)下圖
window流程.png
  • 比如我們?cè)趕elf.view 上依次添加view1、view2、view3(3個(gè)view是同級(jí)關(guān)系),那么系統(tǒng)用hitTest以及pointInside時(shí)會(huì)先從view3開(kāi)始便利,如果pointInside返回YES就繼續(xù)遍歷view3的subviews(如果view3沒(méi)有子視圖,那么會(huì)返回view3),如果pointInside返回NO就開(kāi)始便利view2。
  • 反序遍歷,最后一個(gè)添加的subview開(kāi)始。也算是一種算法優(yōu)化

2.2 HitTest 、pointInside

  • 上一段層級(jí)關(guān)系的簡(jiǎn)單示例代碼
EOCLightGrayView *grayView = [[EOCLightGrayView alloc] initWithFrame:CGRectMake(50.f, 100.f, 260.f, 200.f)];
    
    redView = [[EOCRedView alloc] initWithFrame:CGRectMake(0.f, 0.f, 120.f, 100.f)];
    
    EOCBlueView *blueView = [[EOCBlueView alloc] initWithFrame:CGRectMake(140.f, 100.f, 100.f, 100.f)];
    
    EOCYellowView *yellowView = [[EOCYellowView alloc] initWithFrame:CGRectMake(50.f, 360.f, 200.f, 200.f)];
    
    [self.view addSubview:grayView];
    [grayView addSubview:redView];
    [grayView addSubview:blueView];
    [self.view addSubview:yellowView];
布局.png
打印.png
  • 點(diǎn)擊red,由于yellowgrey 同級(jí),yellowgrey 后添加,所以先打印yellow,由于觸摸點(diǎn)不在yellow內(nèi),打印grey,然后遍歷grey,打印他的兩個(gè)subviews
  • 通過(guò)在HitTest返回nil,pointInside并沒(méi)有執(zhí)行,我們可以得知,pointInside調(diào)用順序你在HitTest之后的。
  • pointInside 的 參數(shù) :(CGPoint)poinit 的值是以自身為坐標(biāo)系的,判斷點(diǎn)是否view內(nèi)的范圍是以view自身的bounds為范圍,而非frame
  • 如果在greyhitTest返回[super hitTest:point event:event],則會(huì)執(zhí)行gery.subviews的遍歷(subviews 的 hitTest 與 pointInside),greypointInside 是判斷觸摸點(diǎn)是否在greybounds內(nèi)(不準(zhǔn)確),greyhitTest 是判斷是否需要遍歷他的subviews.
  • pointInside 只是在執(zhí)行hitTest時(shí),會(huì)在hitTest內(nèi)部調(diào)用的一個(gè)方法
  • pointInside 只是輔助hitTest的關(guān)系
  • hitTest是一個(gè)遞歸函數(shù)

2.3 hitTest 內(nèi)部實(shí)現(xiàn)代碼還原

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    ///hitTest:判斷pointInside,是不是在view里?是的話(huà),遍歷,不是的話(huà)返回nil;假設(shè)我就是點(diǎn)擊灰色的,返回的是自己;
    NSLog(@"%s",__func__);
    
    NSArray *subViews = [[self.subviews reverseObjectEnumerator] allObjects];
    UIView *tmpView = nil;
    for (UIView *view in subViews) {
        
        CGPoint convertedPoint = [self convertPoint:point toView:view];
        if ([view pointInside:convertedPoint withEvent:event]) {
            
            tmpView = view;
            break;
            
        }
        
    }
    
    if (tmpView) {
        return tmpView;
    } else if([self pointInside:point withEvent:event]) {
        
        return self;
    } else {
        
        return nil;
        
    }
    
    
    return [self hitTest:point event:event];  //redView
    
    ///這里是hitTest的邏輯
    
    ///alpha(<=0.01)、userInterActionEnabled(NO)、hidden(YES)  pointInside返回的為NO
    
}

2.4 實(shí)戰(zhàn)之?dāng)U大button點(diǎn)擊區(qū)域

bigbutton.png
  • 紅色為button
  • 藍(lán)色為放大后的目標(biāo)點(diǎn)擊區(qū)域
  • 稍微注意是在bounds的基礎(chǔ)上修改
  • button 內(nèi)部的 hitTest 通過(guò) pointInside 的確認(rèn),來(lái)決定是否返回自己
@implementation EOCCustomButton

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"%s", __func__);
   
    //擴(kuò)大它的響應(yīng)范圍
    CGRect frame = [self getScaleFrame];
    return CGRectContainsPoint(frame, point);
    
   //  return [super pointInside:point withEvent:event];
}

- (CGRect)getScaleFrame {
    
    CGRect rect = self.bounds;
    
    if (rect.size.width < 40.f) {
        rect.origin.x -= (40-rect.size.width)/2;
    }
    
    if (rect.size.height < 40.f) {
        rect.origin.y -= (40-rect.size.height)/2;
    }
    
    rect.size.width = 40.f;
    rect.size.height = 40.f;
    
    return rect;
}

2.5 UIRespond 與 響應(yīng)鏈的組成

  • 當(dāng)我們通過(guò)hitTest找到視圖后,我們產(chǎn)生的touch事件,他是怎么一層層響應(yīng)的?
respondch.png
  • 響應(yīng)鏈?zhǔn)峭ㄟ^(guò) nextResponder 屬性組成的一個(gè)鏈表
  • 點(diǎn)擊的 view 有 superView ,nextResponder 就是 superView ;
  • view.nextResponder .nextResponder 是viewController 或者是 view.superView. view
  • view.nextResponder .nextResponder. nextResponder 是 UIWindow (非嚴(yán)謹(jǐn),便于理解)
  • view.nextResponder .nextResponder. nextResponder. nextResponder 是UIApplication 、UIAppdelate、直到nil (非嚴(yán)謹(jǐn),便于理解)
  • touch事件就是根據(jù)響應(yīng)鏈的關(guān)系來(lái)層層調(diào)用(我們重寫(xiě)touch 要記得 super 調(diào)用,不然響應(yīng)鏈會(huì)中斷
  • 比如我們監(jiān)聽(tīng)self.viewtouch事件,也是因?yàn)?code>subviews的touch都在同一個(gè)響應(yīng)鏈里

3. 手勢(shì)事件

3.1 手勢(shì) 與 hitTest 的關(guān)系

  • 相同上面的學(xué)習(xí)我們可以推測(cè)出,手勢(shì)的響應(yīng)也得必須經(jīng)過(guò)hitTest先找到視圖才能觸發(fā)(已驗(yàn)證)

3.2 手勢(shì)與 觸摸事件的關(guān)系

  • touch事件是UIView內(nèi)部的東西,而手勢(shì)疊加上去的觸摸事件
  • subview會(huì)響應(yīng)superview的手勢(shì), 但是同級(jí)的subview不會(huì)響應(yīng)

3.3 系統(tǒng)如何分辨手勢(shì)種類(lèi)

  • 首先我們想在手勢(shì)中調(diào)用 touches 方法必須要導(dǎo)入
    #import <UIKit/UIGestureRecognizerSubclass.h>
    因?yàn)?code>gesture繼承的是NSObject 而不是 UIRespond
  • 通過(guò)嘗試不調(diào)用 tap手勢(shì)touchesBegan ,發(fā)現(xiàn)tap手勢(shì)無(wú)法響應(yīng)
  • 通過(guò)嘗試調(diào)用touchesBegan ,但是不調(diào)用 pan手勢(shì)touchesMoved ,發(fā)現(xiàn)pan手勢(shì)無(wú)法響應(yīng)
  • 我們通過(guò)UITouch的實(shí)例,可以看到里面有很多屬性,比如點(diǎn)擊的次數(shù),上次的位置等,結(jié)合這個(gè)屬性系統(tǒng)與touches方法就可以判斷出你使用的是什么手勢(shì)

3.4 手勢(shì)與view的touches事件的關(guān)系

  • 首先通過(guò)觸摸事件,先響應(yīng)touchesBegan 以及 touchesMoved,直到手勢(shì)被識(shí)別出來(lái),調(diào)用touchesCancelled,全權(quán)交給手勢(shì)處理。
  • 但是我們可以改變這種關(guān)系
下面是系統(tǒng)的默認(rèn)設(shè)置
 tapGesture.delaysTouchesBegan = NO;   
///是否延遲view的touch事件識(shí)別;如果延遲了(YES),并且手勢(shì)也識(shí)別到了,touch事件會(huì)被拋棄

 tapGesture.cancelsTouchesInView = YES;   
///識(shí)別手勢(shì)之后,是否取消view的touch事件
// 如果為NO, touchesCancelled 不會(huì)調(diào)用,取而代之的是手勢(shì)結(jié)束后touchesEnd

4. button事件

4.1 系統(tǒng)是如何分辨UIControlEvent

  • 我們還是通過(guò)button內(nèi)部的touches來(lái)實(shí)踐
  • 實(shí)踐過(guò)程略,與手勢(shì)同理
  • 比如說(shuō) touchUpInside,通過(guò)查看堆棧調(diào)用,我們發(fā)現(xiàn)在touchesEnd后完成對(duì)touchUpInside的識(shí)別,然后再調(diào)起sendAction:方法

5. 觸摸事件的運(yùn)用

  • 上文已經(jīng)講過(guò)一個(gè)button點(diǎn)擊范圍擴(kuò)大的案例,再講一個(gè)案例
case2.png
  • 與上個(gè)例子不同的是,當(dāng)我們點(diǎn)擊黑色的時(shí)候,因?yàn)樵?code>greyview的外面,別說(shuō)響應(yīng)黑色button了,我們直接不會(huì)響應(yīng)greyview了,怎么辦?
  • 一種是在self.view-pointInside 返回 YES, 不過(guò)這種在交互復(fù)雜的場(chǎng)景不存在實(shí)用性
  • 我們可以重寫(xiě) self.view-hitTest, 把當(dāng)前觸摸的點(diǎn)分別轉(zhuǎn)化為subviews上的坐標(biāo)系的點(diǎn),在用subviewspointinside判斷此點(diǎn),然后返回對(duì)應(yīng)的subviews
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 在iOS開(kāi)發(fā)中經(jīng)常會(huì)涉及到觸摸事件。本想自己總結(jié)一下,但是遇到了這篇文章,感覺(jué)總結(jié)的已經(jīng)很到位,特此轉(zhuǎn)載。作者:L...
    WQ_UESTC閱讀 6,250評(píng)論 4 26
  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的?困惑于Cell怎么突然不能點(diǎn)擊了?糾結(jié)于如何實(shí)現(xiàn)這個(gè)奇葩響應(yīng)需求?亦或是...
    Lotheve閱讀 59,572評(píng)論 51 604
  • 值得注意的事,當(dāng)一個(gè)view上面有多個(gè)手勢(shì)時(shí),touch是無(wú)序的 1事件是怎么樣產(chǎn)生與傳遞的?(由上至下的過(guò)程) ...
    池鵬程閱讀 2,670評(píng)論 0 8
  • 在開(kāi)發(fā)過(guò)程中,大家或多或少的都會(huì)碰到令人頭疼的手勢(shì)沖突問(wèn)題,正好前兩天碰到一個(gè)類(lèi)似的bug,于是借著這個(gè)機(jī)會(huì)了解了...
    閆仕偉閱讀 5,683評(píng)論 2 23
  • iOS中有三類(lèi)事件:UIEventTypeTouches觸摸事件、 UIEventTypeMotion “動(dòng)作”事...
    WeiHing閱讀 37,401評(píng)論 7 69

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