iOS中觸摸事件詳解

觸摸事件的生命周期

當(dāng)我們手指觸碰屏幕的那一刻进统,一個觸摸事件便產(chǎn)生了。經(jīng)過進(jìn)程間通信猴蹂,觸摸事件被傳遞到合適的應(yīng)用之中,在該應(yīng)用內(nèi)部觸摸事件歷經(jīng)坎坷楣嘁,最終被釋放掉磅轻。
整個過程如下圖所示:


觸摸事件生命周期 圖片來源(http://qingmo.me/2017/03/04/FlowOfUITouch/)

如上圖所示珍逸,觸摸事件分為兩大階段:

系統(tǒng)響應(yīng)階段

  1. 手指觸摸屏幕,屏幕感應(yīng)到觸碰后聋溜,將事件交給IOKit處理
  2. IOKit將觸摸事件封裝成一個IOHIDEvent對象谆膳,并通過mach port傳遞給SpringBord進(jìn)程

mach port是各個進(jìn)程的端口,各進(jìn)程通過它來進(jìn)行進(jìn)程間通信
SpringBord是一個系統(tǒng)進(jìn)程撮躁,可以理解為桌面系統(tǒng)漱病。它用來統(tǒng)一管理系統(tǒng)接收到的觸摸事件

  1. SpringBoard進(jìn)程因接收到觸摸事件,觸發(fā)了其主線程runloop的source1事件源的回調(diào)把曼。
    此時SpringBoard會根據(jù)當(dāng)前桌面的狀態(tài)杨帽,判斷應(yīng)該由誰處理此次觸摸事件。因為事件發(fā)生時嗤军,你可能正在桌面上翻頁注盈,也可能正在刷微博。若是前者(即前臺無APP運行)叙赚,則觸發(fā)SpringBoard本身主線程runloop的source0事件源的回調(diào)老客,將事件交由桌面系統(tǒng)去消耗;若是后者(即有app正在前臺運行)震叮,則將觸摸事件通過IPC傳遞給前臺APP進(jìn)程胧砰,接下來的事情便是APP內(nèi)部對于觸摸事件的響應(yīng)了。

app響應(yīng)階段

  1. APP進(jìn)程的mach port接受到SpringBoard進(jìn)程傳遞來的觸摸事件冤荆,主線程的runloop被喚醒朴则,觸發(fā)了source1回調(diào)
  2. source1回調(diào)又觸發(fā)了一個source0回調(diào),將接收到的IOHIDEvent對象封裝成UIEvent對象钓简,此時APP將正式開始對于觸摸事件的響應(yīng)
  3. source0回調(diào)內(nèi)部將觸摸事件添加到UIApplication對象的事件隊列中。事件出隊后汹想,UIApplication開始一個尋找最佳響應(yīng)者的過程外邓,這個過程又稱hit-testing
  4. 尋找到最佳響應(yīng)者后,接下來的事情便是事件在響應(yīng)鏈中的傳遞及響應(yīng)古掏。事實上损话,事件除了被響應(yīng)者消耗,還能被手勢識別器或是target-action模式捕捉并消耗掉槽唾,其中涉及到事件響應(yīng)的優(yōu)先級問題
  5. 觸摸事件歷經(jīng)坎坷丧枪,要么被某個響應(yīng)對象捕獲后釋放,要么最終也沒能找到能夠響應(yīng)的對象庞萍,然后釋放

觸摸對象UITouch拧烦、事件UIEvent、響應(yīng)者UIResponder

UITouch

簡單理解钝计,一根手指對應(yīng)一個UITouch對象

  • 更準(zhǔn)確一點是恋博,一根手指觸摸一次屏幕齐佳,就產(chǎn)生一個UITouch對象。多根手指同時觸摸屏幕债沮,會產(chǎn)生多個UITouch對象
  • 多個手指先后觸摸,系統(tǒng)會根據(jù)觸摸的位置判斷是否更新同一個UITouch對象疫衩。若兩個手指一前一后觸摸同一個位置(即雙擊)硅蹦,那么第一次觸摸時生成一個UITouch對象,第二次觸摸更新這個UITouch對象(UITouch對象的 tap count 屬性值從1變成2)闷煤;若兩個手指一前一后觸摸的位置不同提针,將會生成兩個UITouch對象,兩者之間沒有聯(lián)系曹傀。
  • 每個UITouch對象記錄了觸摸的一些信息辐脖,包括觸摸時間、位置皆愉、階段嗜价、所處的視圖、窗口等信息

UIEvent

觸摸事件

  • 觸摸的目的是生成觸摸事件供響應(yīng)者響應(yīng)幕庐,一個觸摸事件對應(yīng)一個UIEvent對象久锥,其中的 type 屬性標(biāo)識了事件的類型(之前說過事件不只是觸摸事件)。
  • UIEvent對象中包含了觸發(fā)該事件的觸摸對象的集合异剥,因為一個觸摸事件可能是由多個手指同時觸摸產(chǎn)生的瑟由。觸摸對象集合通過 allTouches 屬性獲取。

UIResponder

每個響應(yīng)者都是一個UIResponder對象冤寿,所有派生自UIResponder的對象歹苦,都具有響應(yīng)事件的能力:

  • UIApplication
  • UIViewController
  • UIView(包含UIWindow)
  • AppDelegate

響應(yīng)者之所以能夠響應(yīng)事件,是因為UIResponder提供了四個響應(yīng)事件的方法:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

這幾個方法在響應(yīng)者對象接收到事件的時候會被調(diào)用督怜,方法內(nèi)部用來做出響應(yīng)殴瘦。默認(rèn)的實現(xiàn)是不做處理,并將事件傳遞給響應(yīng)鏈中的上一個結(jié)點号杠。

尋找最佳響應(yīng)者(Hit-Testing)

觸摸事件產(chǎn)生后蚪腋,會經(jīng)歷一個尋找最佳響應(yīng)者的過程,目的是找到一個具備最高優(yōu)先級響應(yīng)權(quán)的響應(yīng)對象姨蟋,這個過程叫做Hit-Testing屉凯,那個命中的最佳響應(yīng)者稱為hit-tested view。

事件自上而下的傳遞

如文章開頭的圖所示眼溶,觸摸事件被添加到UIApplication的事件隊列中等待處理悠砚,出隊后,應(yīng)用首先將該事件交給自己的主窗口(當(dāng)前應(yīng)用最后顯示的窗口)偷仿,詢問其能否響應(yīng)事件哩簿。若窗口能響應(yīng)事件宵蕉,則傳遞給子視圖詢問是否能響應(yīng),子視圖若能響應(yīng)則繼續(xù)詢問子視圖节榜。子視圖詢問的順序是優(yōu)先詢問后添加的子視圖羡玛,即子視圖數(shù)組中靠后的視圖(因為后添加的視圖在層級的上面,可以減少遍歷次數(shù))宗苍。整體過程如下所示:

UIApplication ——> UIWindow ——> 子視圖 ——> ... ——> 子視圖 

Hit-Testing本質(zhì)

視圖如何判斷能否響應(yīng)事件稼稿?以及視圖如何將事件傳遞給子視圖呢?

首先要知道的是讳窟,以下幾種狀態(tài)的視圖無法響應(yīng)事件:

  • 不允許交互:userInteractionEnabled = NO
  • 隱藏:hidden = YES 如果父視圖隱藏让歼,那么子視圖也會隱藏,隱藏的視圖無法接收事件
  • 透明度:alpha < 0.01 如果設(shè)置一個視圖的透明度<0.01丽啡,會直接影響子視圖的透明度谋右。alpha:0.0~0.01為透明。

每個UIView對象都有一個方法hitTest: withEvent: 补箍,這個方法是Hit-Testing過程中最核心的存在改执,其作用是返回觸摸事件的最佳響應(yīng)者,同時又作為事件傳遞的橋梁坑雅。
該方法返回一個UIView對象辈挂,默認(rèn)實現(xiàn)為:

  • 若當(dāng)前視圖無法響應(yīng)事件,則返回nil
  • 若當(dāng)前視圖可以響應(yīng)事件裹粤,但無子視圖可以響應(yīng)事件终蒂,則返回自身作為當(dāng)前視圖層次中的事件響應(yīng)者
  • 若當(dāng)前視圖可以響應(yīng)事件,同時有子視圖可以響應(yīng)遥诉,則返回子視圖層次中的事件響應(yīng)者

一開始UIApplication將事件UIEvent作為參數(shù)傳遞給UIWindow的 hitTest:withEvent: 方法拇泣,UIWindow的 hitTest:withEvent: 方法在執(zhí)行時若判斷本身能響應(yīng)事件,則調(diào)用子視圖的 hitTest:withEvent: 將事件傳遞給子視圖并詢問子視圖上的最佳響應(yīng)者突那。最終UIWindow返回一個視圖層次中的響應(yīng)者視圖給UIApplication挫酿,這個視圖就是hit-testing的最佳響應(yīng)者。

注意理解愕难,這里最佳響應(yīng)者還是返回給了UIApplication的,整個流程就是事件先從UIApplication分發(fā)下來惫霸,得到最佳響應(yīng)者之后又將該響應(yīng)者返回給UIApplication猫缭。

根據(jù)上面的結(jié)論,hitTest:withEvent:方法實現(xiàn)如下

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    //3種狀態(tài)無法響應(yīng)事件
     if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil; 
    //觸摸點若不在當(dāng)前視圖上則無法響應(yīng)事件
    if ([self pointInside:point withEvent:event] == NO) return nil; 
    //從后往前遍歷子視圖數(shù)組 
    int count = (int)self.subviews.count; 
    for (int i = count - 1; i >= 0; i--) 
    { 
        // 獲取子視圖
        UIView *childView = self.subviews[i]; 
        // 坐標(biāo)系的轉(zhuǎn)換,把觸摸點在當(dāng)前視圖上坐標(biāo)轉(zhuǎn)換為在子視圖上的坐標(biāo)
        CGPoint childP = [self convertPoint:point toView:childView]; 
        //詢問子視圖層級中的最佳響應(yīng)視圖
        UIView *fitView = [childView hitTest:childP withEvent:event]; 
        if (fitView) 
        {
            //如果子視圖中有更合適的就返回
            return fitView; 
        }
    } 
    //沒有在子視圖中找到更合適的響應(yīng)視圖壹店,那么自身就是最合適的
    return self;
}

值得注意的是 pointInside:withEvent: 這個方法猜丹,用于判斷觸摸點是否在自身坐標(biāo)范圍內(nèi)。默認(rèn)實現(xiàn)是若在坐標(biāo)范圍內(nèi)則返回YES硅卢,否則返回NO射窒。
我們可以修改這個方法來完成一些特殊的需求藏杖,如下:


特殊需求

中間的原型按鈕是底部Tabbar上的控件,而Tabbar是添加在控制器根視圖中的脉顿。默認(rèn)情況下我們點擊圖中紅色方框中按鈕的區(qū)域蝌麸,會發(fā)現(xiàn)按鈕并不會得到響應(yīng)。
上圖對應(yīng)的視圖層級結(jié)構(gòu)大致為:

RootView
└── TableView
└── TabBar
    └── CircleButton

點擊紅色方框區(qū)域后艾疟,生成的觸摸事件首先傳到UIWindow来吩,然后傳到控制器的根視圖即RootView。RootView經(jīng)判斷可以響應(yīng)觸摸事件蔽莱,而后將事件傳給了子控件TabBar弟疆。問題就出在這里,因為觸摸點不在TabBar的坐標(biāo)范圍內(nèi)盗冷,因此TabBar無法響應(yīng)該觸摸事件怠苔,hitTest:withEvent: 直接返回了nil。而后RootView就會詢問TableView是否能夠響應(yīng)仪糖,事實上是可以的柑司,因此事件最終被TableView消耗。整個過程乓诽,事件根本沒有傳遞到圓形按鈕帜羊。

根據(jù)hitTest:withEvent:方法內(nèi)部的實現(xiàn),事件傳遞到TabBar時鸠天,TabBar的 hitTest:withEvent: 被調(diào)用讼育,但是 pointInside:withEvent: 會返回NO,如此一來 hitTest:withEvent: 返回了nil稠集。既然如此奶段,可以重寫TabBard的 pointInside:withEvent: ,判斷當(dāng)前觸摸坐標(biāo)是否在子視圖CircleButton的坐標(biāo)范圍內(nèi)剥纷,若在痹籍,則返回YES,反之返回NO晦鞋。這樣一來點擊紅色區(qū)域蹲缠,事件最終會傳遞到CircleButton,CircleButton能夠響應(yīng)事件悠垛,最終事件就由CircleButton響應(yīng)了线定。同時點擊紅色方框以外的非TabBar區(qū)域的情況下,因為TabBar無法響應(yīng)事件确买,會按照預(yù)期由TableView響應(yīng)斤讥。代碼如下:

//TabBar中重寫pointInside方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    //將觸摸點坐標(biāo)轉(zhuǎn)換到在CircleButton上的坐標(biāo)
    CGPoint pointTemp = [self convertPoint:point toView:_CircleButton];
    //若觸摸點在CricleButton上則返回YES
    if ([_CircleButton pointInside:pointTemp withEvent:event]) {
        return YES;
    }
    //否則返回默認(rèn)的操作
    return [super pointInside:point withEvent:event];
}

事件的響應(yīng)及在響應(yīng)鏈中的傳遞

經(jīng)歷Hit-Testing后,UIApplication已經(jīng)知道事件的最佳響應(yīng)者是誰了湾趾,接下來要做的事情就是:

  1. 將事件傳遞給最佳響應(yīng)者響應(yīng)
  2. 事件沿著響應(yīng)鏈傳遞

事件響應(yīng)之前

因為最佳響應(yīng)者具有最高的事件響應(yīng)優(yōu)先級芭商,因此UIApplication會先將事件傳遞給它供其響應(yīng)派草。首先,UIApplication將事件通過 sendEvent: 傳遞給事件所屬的window铛楣,window同樣通過 sendEvent: 再將事件傳遞給hit-tested view近迁,即最佳響應(yīng)者。過程如下:

UIApplication ——> UIWindow ——> hit-tested view

假設(shè)視圖結(jié)構(gòu)為:

rootView
└── redView
└── blueView

在blueView的touchBegan方法打斷點蛉艾,點擊blueView后函數(shù)調(diào)用棧如下:


函數(shù)調(diào)用

那么問題又來了钳踊。這個過程中,假如應(yīng)用中存在多個window對象勿侯,UIApplication是怎么知道要把事件傳給哪個window的拓瞪?window又是怎么知道哪個視圖才是最佳響應(yīng)者的呢?

這兩個過程都是傳遞事件的過程助琐,涉及的方法都是 sendEvent: 祭埂,而該方法的參數(shù)(UIEvent對象)是唯一貫穿整個經(jīng)過的線索,那么就可以大膽猜測必然是該觸摸事件對象上綁定了這些信息兵钮。事實上之前在介紹UITouch的時候就說過touch對象保存了觸摸所屬的window及view蛆橡,而event對象又綁定了touch對象,如此一來掘譬,就說得通了泰演。怎么驗證猜測是否為真呢?
自定義一個Window類葱轩,重寫 sendEvent: 方法睦焕,捕捉該方法調(diào)用時參數(shù)event的狀態(tài),如下圖所示:


事件

這兩個屬性是什么時候綁定到touch對象上的呢靴拱?應(yīng)該是在hit-testing過程中垃喊。其實hit-testing本質(zhì)上做的事情,也就是將這些信息綁定到touch對象上袜炕。

事件的響應(yīng)

前面介紹UIResponder的時候說過本谜,每個響應(yīng)者必定都是UIResponder對象,通過4個響應(yīng)觸摸事件的方法來響應(yīng)事件偎窘。每個UIResponder對象默認(rèn)都已經(jīng)實現(xiàn)了這4個方法乌助,但是默認(rèn)不對事件做任何處理,單純只是將事件沿著響應(yīng)鏈傳遞陌知。若要截獲事件進(jìn)行自定義的響應(yīng)操作眷茁,就要重寫相關(guān)的方法。例如纵诞,通過重寫 touchesMoved: withEvent: 方法實現(xiàn)簡單的視圖拖動。

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

每個響應(yīng)觸摸事件的方法都會接收兩個參數(shù)培遵,分別對應(yīng)觸摸對象集合事件對象浙芙。視圖(UIView)作為響應(yīng)者對象登刺,本身已經(jīng)實現(xiàn)了 touchesMoved: withEvent: 方法,具體怎么實現(xiàn)的蘋果沒有開源給我們嗡呼。因此如果想定義自己的響應(yīng)行為纸俭,必須創(chuàng)建一個自定義視圖(繼承自UIView),然后重寫那幾個方法南窗。
這里以touchMoved方法為例揍很,實現(xiàn)視圖隨著手指移動而移動的響應(yīng)事件:

//MovedView
//重寫touchesMoved方法(觸摸滑動過程中持續(xù)調(diào)用)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //獲取觸摸對象
    UITouch *touch = [touches anyObject];
    //獲取前一個觸摸點位置
    CGPoint prePoint = [touch previousLocationInView:self];
    //獲取當(dāng)前觸摸點位置
    CGPoint curPoint = [touch locationInView:self];
    //計算偏移量
    CGFloat offsetX = curPoint.x - prePoint.x;
    CGFloat offsetY = curPoint.y - prePoint.y;
    //相對之前的位置偏移視圖
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

每個響應(yīng)者都有權(quán)決定是否執(zhí)行對事件的響應(yīng),只要重寫相關(guān)的觸摸事件方法即可万伤。

事件沿著響應(yīng)鏈傳遞

前面一直在提最佳響應(yīng)者窒悔,之所以稱之為“最佳”,是因為其具備響應(yīng)事件的最高優(yōu)先權(quán)敌买。最佳響應(yīng)者首先接收到事件简珠,然后便擁有了對事件的絕對控制權(quán):即它可以選擇獨吞這個事件,也可以將這個事件往下傳遞給其他響應(yīng)者虹钮,這個由響應(yīng)者構(gòu)成的視圖鏈就稱之為響應(yīng)鏈聋庵。

響應(yīng)者對于事件的操作方式:

響應(yīng)者對于接收到的事件有3種操作:

  • 不攔截,默認(rèn)操作:事件會自動沿著默認(rèn)的響應(yīng)鏈往下傳遞
  • 攔截芙粱,不再往下分發(fā)事件:重寫 touchesBegan:withEvent: 進(jìn)行事件處理祭玉,不調(diào)用父類的 touchesBegan:withEvent:
  • 攔截,繼續(xù)往下分發(fā)事件:重寫 touchesBegan:withEvent: 進(jìn)行事件處理春畔,同時調(diào)用父類的 touchesBegan:withEvent: 將事件往下傳遞
響應(yīng)鏈中的事件傳遞規(guī)則

每一個響應(yīng)者對象(UIResponder對象)都有一個 nextResponder 方法脱货,用于獲取響應(yīng)鏈中當(dāng)前對象的下一個響應(yīng)者。因此拐迁,一旦事件的最佳響應(yīng)者確定了蹭劈,這個事件所處的響應(yīng)鏈就確定了。

對于響應(yīng)者對象线召,默認(rèn)的 nextResponder 實現(xiàn)如下:

  • UIView
    若視圖是控制器的根視圖铺韧,則其nextResponder為控制器對象;否則缓淹,其nextResponder為父視圖哈打。
  • UIViewController
    若控制器的視圖是window的根視圖,則其nextResponder為窗口對象讯壶;若控制器是從別的控制器present出來的料仗,則其nextResponder為presenting view controller。
  • UIWindow
    nextResponder為UIApplication對象伏蚊。
  • UIApplication
    若當(dāng)前應(yīng)用的app delegate是一個UIResponder對象立轧,且不是UIView斑粱、UIViewController或app本身煤傍,則UIApplication的nextResponder為app delegate。

事件的三徒弟UIResponder、UIGestureRecognizer娜庇、UIControl

iOS中俭厚,除了UIResponder能夠響應(yīng)事件丧靡,手勢識別器综膀、UIControl同樣具備對事件的處理能力。當(dāng)這幾者同時存在于某一場景下的時候葛躏,事件又是怎么處理的呢澈段?

測試場景

app界面如下


測試場景

界面代碼如下

- (void)viewDidLoad {
    [super viewDidLoad];
    //底部是一個綁定了單擊手勢的backView
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(actionTapView)];
    [_backView addGestureRecognizer:tap];
    //上面是一個常規(guī)的tableView
    _tableMain.tableFooterView = [UIView new];
    //還有一個和tableView同級的button
    [_button addTarget:self action:@selector(buttonTap) forControlEvents:UIControlEventTouchUpInside];
}

- (void)actionTapView{
    NSLog(@"backview taped");
}

- (void)buttonTap {
    NSLog(@"button clicked!");
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    NSLog(@"cell selected!");
}

這個時候我們點擊cell,會發(fā)現(xiàn)點不動舰攒,或者說點了沒有任何反應(yīng)败富。但是如果我們長按cell一段時間,發(fā)現(xiàn)cell又可以被點擊芒率。點擊下面的button囤耳,一切如常。為什么會發(fā)生這種情況呢偶芍?

為了測試結(jié)果充择,我們自定義相關(guān)的控件類,均重寫4個響應(yīng)觸摸事件的方法用來打印日志(每個重寫的觸摸事件方法都調(diào)用了父類的方法以保證事件默認(rèn)傳遞邏輯)匪蟀。

觀察各種情況下的日志現(xiàn)象:
  • 現(xiàn)象一 快速點擊cell
backview taped
  • 現(xiàn)象二 短按cell
-[GLTableView touchesBegan:withEvent:]
backview taped
-[GLTableView touchesCancelled:withEvent:]
  • 現(xiàn)象三 長按cell
-[GLTableView touchesBegan:withEvent:]
-[GLTableView touchesEnded:withEvent:]
cell selected!
  • 現(xiàn)象四 點擊button
-[GLButton touchesBegan:withEvent:]
-[GLButton touchesEnded:withEvent:]
button clicked!

想要了解到底發(fā)生了什么椎麦,就不得不提到觸摸事件響應(yīng)的二徒弟,手勢識別器UIGestureRecognizer

手勢識別器UIGestureRecognizer

事實上材彪,手勢分為離散型手勢(discrete gestures)和持續(xù)型手勢(continuous gesture)观挎。系統(tǒng)提供的離散型手勢包括點按手勢([UITapGestureRecognizer]和輕掃手勢([UISwipeGestureRecognizer],其余均為持續(xù)型手勢段化。

兩者主要區(qū)別在于狀態(tài)變化過程:

  • 離散型:
    識別成功:Possible —> Recognized
    識別失斷医荨:Possible —> Failed
  • 持續(xù)型:
    完整識別:Possible —> Began —> [Changed] —> Ended
    不完整識別:Possible —> Began —> [Changed] —> Cancel
離散型手勢

先拋開上面的場景,看一個簡單的demo显熏。


離散型手勢

控制器的根視圖上添加了一個YellowView雄嚣,并給根視圖綁定了一個單擊手勢識別器。

// HuViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(actionTap)];
    [self.view addGestureRecognizer:tap];
}
- (void)actionTap{
    NSLog(@"View Taped");
}

單擊YellowView喘蟆,日志打印如下:

-[YellowView touchesBegan:withEvent:]
View Taped
-[YellowView touchesCancelled:withEvent:]

從日志上看出YellowView最后Cancel了對觸摸事件的響應(yīng)缓升,而正常應(yīng)當(dāng)是觸摸結(jié)束后,YellowView的 touchesEnded:withEvent: 的方法被調(diào)用才對蕴轨。另外港谊,期間還執(zhí)行了手勢識別器綁定的action 。
官方文檔給出了這樣的解釋:

A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEndedproperties.

大致含義是橙弱,Window在將事件傳遞給hit-tested view(最佳響應(yīng)者)之前歧寺,會先將事件傳遞給相關(guān)的手勢識別器并由手勢識別器優(yōu)先識別燥狰。若手勢識別器成功識別了事件,就會取消hit-tested view對事件的響應(yīng)成福;若手勢識別器沒能識別事件碾局,hit-tested view才完全接手事件的響應(yīng)權(quán)。即手勢識別器比UIResponder具有更高的事件響應(yīng)優(yōu)先級奴艾!

按照這個解釋,Window在將事件傳遞給hit-tested view即YellowView之前内斯,先傳遞給了控制器根視圖上的手勢識別器蕴潦。手勢識別器成功識別了該事件,通知Application取消YellowView對事件的響應(yīng)俘闯。

然而看日志潭苞,卻是YellowView的 touchesBegan:withEvent: 先調(diào)用了,既然手勢識別器先響應(yīng)真朗,不應(yīng)該上面的action先執(zhí)行嗎此疹?其實這里存在一個認(rèn)知錯誤,手勢識別器的action的調(diào)用時機(jī)并不是手勢識別器接收到事件的時機(jī)遮婶,而是手勢識別器成功識別事件后的時機(jī)蝗碎,即手勢識別器的狀態(tài)變?yōu)閁IGestureRecognizerStateRecognized。因此從該日志中并不能看出事件是優(yōu)先傳遞給手勢識別器的旗扑,那該怎么證明Window先將事件傳遞給了手勢識別器蹦骑?

要解決這個問題,只要知道手勢識別器是如何接收事件的臀防,然后在接收事件的方法中打印日志對比調(diào)用時間先后即可眠菇。說出來你可能不信,手勢識別器對于事件的響應(yīng)也是通過這4個熟悉的方法來實現(xiàn)的袱衷。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

注意捎废,雖然手勢識別器通過這幾個方法來響應(yīng)事件,但它并不是UIResponder的子類致燥,這幾個方法的聲明在 UIGestureRecognizerSubclass.h 中登疗,跟UIResponder中的方法實現(xiàn)并不是一回事。

這樣一來篡悟,我們便可以自定義一個單擊手勢識別器的類谜叹,重寫這幾個方法來監(jiān)聽手勢識別器接收事件的時機(jī)。創(chuàng)建一個UITapGestureRecognizer的子類搬葬,重寫響應(yīng)事件的方法荷腊,每個方法中調(diào)用父類的實現(xiàn),并替換demo中的手勢識別器急凰。另外需要在.m文件中引入 import <UIKit/UIGestureRecognizerSubclass.h> 女仰,因為相關(guān)方法聲明在該頭文件中猜年。

// HuTapGestureRecognizer (繼承自UITapGestureRecognizer)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    [super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    [super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    [super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    [super touchesCancelled:touches withEvent:event];
}

現(xiàn)在,再次點擊YellowView疾忍,日志如下:

-[HuTapGestureRecognizer touchesBegan:withEvent:]
-[YellowView touchesBegan:withEvent:]
-[HuTapGestureRecognizer touchesEnded:withEvent:]
View Taped
-[YellowView touchesCancelled:withEvent:]

很明顯乔外,確實是手勢識別器先接收到了事件。之后手勢識別器成功識別了手勢一罩,執(zhí)行了action杨幼,再由Application取消了YellowView對事件的響應(yīng)。

Window怎么知道要把事件傳遞給哪些手勢識別器聂渊?

之前探討過Application怎么知道要把event傳遞給哪個Window差购,以及Window怎么知道要把event傳遞給哪個hit-tested view的問題,答案是這些信息都保存在event所綁定的touch對象上汉嗽。手勢識別器也是一樣的欲逃,event綁定的touch對象上維護(hù)了一個手勢識別器數(shù)組,里面的手勢識別器是在hit-testing的過程中收集的饼暑。打個斷點看一下touch上綁定的手勢識別器數(shù)組:

手勢識別器捕捉

Window先將事件傳遞給這些手勢識別器稳析,再傳給hit-tested view。一旦有手勢識別器成功識別了手勢弓叛,Application就會取消hit-tested view對事件的響應(yīng)彰居。

持續(xù)型手勢

將上面Demo中視圖綁定的單擊手勢識別器用滑動手勢識別器(UIPanGestureRecognizer)替換。

- (void)viewDidLoad {
    [super viewDidLoad];
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(actionPan)];
    [self.view addGestureRecognizer:pan];
}
- (void)actionPan{
    NSLog(@"View panned");
}

在YellowView上執(zhí)行一次滑動邪码,日志打印如下:

-[YellowView touchesBegan:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesCancelled:withEvent:]
View panned
View panned
View panned
...

在一開始滑動的過程中裕菠,手勢識別器處在識別手勢階段,滑動產(chǎn)生的連續(xù)事件既會傳遞給手勢識別器又會傳遞給YellowView闭专,因此YellowView的 touchesMoved:withEvent: 在開始一段時間內(nèi)會持續(xù)調(diào)用奴潘;當(dāng)手勢識別器成功識別了該滑動手勢時,手勢識別器的action開始調(diào)用影钉,同時通知Application取消YellowView對事件的響應(yīng)画髓。之后僅由滑動手勢識別器接收事件并響應(yīng),YellowView不再接收事件平委。

另外奈虾,在滑動的過程中,若手勢識別器未能識別手勢廉赔,則事件在觸摸滑動過程中會一直傳遞給hit-tested view肉微,直到觸摸結(jié)束。

總結(jié)一下手勢識別器與UIResponder對于事件響應(yīng)的聯(lián)系:

當(dāng)觸摸發(fā)生或者觸摸的狀態(tài)發(fā)生變化時蜡塌,Window都會傳遞事件尋求響應(yīng)碉纳。

  • Window先將綁定了觸摸對象的事件傳遞給觸摸對象上綁定的手勢識別器,再發(fā)送給觸摸對象對應(yīng)的hit-tested view馏艾。
  • 手勢識別器識別手勢期間劳曹,若觸摸對象的觸摸狀態(tài)發(fā)生變化奴愉,事件都是先發(fā)送給手勢識別器再發(fā)送給hit-test view。
  • 手勢識別器若成功識別了手勢铁孵,則通知Application取消hit-tested view對于事件的響應(yīng)锭硼,并停止向hit-tested view發(fā)送事件;
  • 若手勢識別器未能識別手勢蜕劝,而此時觸摸并未結(jié)束檀头,則停止向手勢識別器發(fā)送事件,僅向hit-test view發(fā)送事件熙宇。
  • 若手勢識別器未能識別手勢鳖擒,且此時觸摸已經(jīng)結(jié)束,則向hit-tested view發(fā)送end狀態(tài)的touch事件以停止對事件的響應(yīng)烫止。
手勢識別器的3個屬性
@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;

cancelsTouchesInView

默認(rèn)為YES。表示當(dāng)手勢識別器成功識別了手勢之后戳稽,會通知Application取消響應(yīng)鏈對事件的響應(yīng)馆蠕,并不再傳遞事件給hit-test view。若設(shè)置成NO惊奇,表示手勢識別成功后不取消響應(yīng)鏈對事件的響應(yīng)互躬,事件依舊會傳遞給hit-test view。

demo中設(shè)置: pan.cancelsTouchesInView = NO

滑動時日志如下:

-[YellowView touchesBegan:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesMoved:withEvent:]
View panned
View panned
-[YellowView touchesMoved:withEvent:]
View panned
-[YellowView touchesMoved:withEvent:]
...

即便滑動手勢識別器識別了手勢颂郎,Application也會依舊發(fā)送事件給YellowView吼渡。

delaysTouchesBegan

默認(rèn)為NO。默認(rèn)情況下手勢識別器在識別手勢期間乓序,當(dāng)觸摸狀態(tài)發(fā)生改變時寺酪,Application都會將事件傳遞給手勢識別器和hit-tested view;若設(shè)置成YES替劈,則表示手勢識別器在識別手勢期間寄雀,截斷事件,即不會將事件發(fā)送給hit-tested view陨献。

設(shè)置 pan.delaysTouchesBegan = YES

日志如下:

View panned
View panned
View panned
View panned
...

因為滑動手勢識別器在識別期間盒犹,事件不會傳遞給YellowView,因此期間YellowView的 touchesBegan:withEvent:touchesMoved:withEvent: 都不會被調(diào)用眨业;而后滑動手勢識別器成功識別了手勢急膀,也就獨吞了事件,不會再傳遞給YellowView龄捡。因此只打印了手勢識別器成功識別手勢后的action調(diào)用卓嫂。

delaysTouchesEnded

默認(rèn)為NO。默認(rèn)情況下當(dāng)手勢識別器未能識別手勢時墅茉,若此時觸摸已經(jīng)結(jié)束命黔,則會立即通知Application發(fā)送狀態(tài)為end的touch事件給hit-tested view以調(diào)用 touchesEnded:withEvent: 結(jié)束事件響應(yīng)呜呐;若設(shè)置為YES,則會在手勢識別失敗時悍募,延遲一小段時間(0.15s)再調(diào)用響應(yīng)者的 touchesEnded:withEvent:蘑辑。

總結(jié):手勢識別器比響應(yīng)鏈具有更高的事件響應(yīng)優(yōu)先級。

大徒弟—UIControl

UIControl是系統(tǒng)提供的能夠以target-action模式處理觸摸事件的控件坠宴,iOS中UIButton洋魂、UISegmentedControl、UISwitch等控件都是UIControl的子類喜鼓。當(dāng)UIControl跟蹤到觸摸事件時副砍,會向其上添加的target發(fā)送事件以執(zhí)行action。值得注意的是庄岖,UIConotrol是UIView的子類豁翎,因此本身也具備UIResponder應(yīng)有的身份。

關(guān)于UIControl隅忿,此處介紹兩點:

  • target-action執(zhí)行時機(jī)及過程
  • 觸摸事件優(yōu)先級

UIControl作為能夠響應(yīng)事件的控件心剥,必然也需要待事件交互符合條件時才去響應(yīng),因此也會跟蹤事件發(fā)生的過程背桐。不同于UIControl以及UIGestureRecognizer通過 touches 系列方法跟蹤优烧,UIControl有其獨特的跟蹤方式:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;

乍一看,這4個方法和UIResponder的那4個方法幾乎吻合链峭,只不過UIControl只能接收單點觸控畦娄,因此接收的參數(shù)是單個UITouch對象。這幾個方法的職能也和UIResponder一致弊仪,用來跟蹤觸摸的開始熙卡、滑動、結(jié)束撼短、取消再膳。不過,UIControl本身也是UIResponder曲横,因此同樣有 touches 系列的4個方法喂柒。
事實上,UIControl的 Tracking 系列方法是在 touch 系列方法內(nèi)部調(diào)用的禾嫉。比如 beginTrackingWithTouch 是在 touchesBegan 方法內(nèi)部調(diào)用的灾杰, 因此它雖然也是UIResponder,但 touches 系列方法的默認(rèn)實現(xiàn)和UIResponder本類還是有區(qū)別的熙参。

當(dāng)UIControl跟蹤事件的過程中艳吠,識別出事件交互符合響應(yīng)條件,就會觸發(fā)target-action進(jìn)行響應(yīng)孽椰。UIControl控件通過 addTarget:action:forControlEvents: 添加事件處理的target和action昭娩,當(dāng)事件發(fā)生時凛篙,UIControl通知target執(zhí)行對應(yīng)的action。說是“通知”其實很籠統(tǒng)栏渺,事實上這里有個action傳遞的過程呛梆。當(dāng)UIControl監(jiān)聽到需要處理的交互事件時,會調(diào)用 sendAction:to:forEvent: 將target磕诊、action以及event對象發(fā)送給全局應(yīng)用填物,Application對象再通過 sendAction:to:from:forEvent: 向target發(fā)送action。


target-action過程

因此霎终,可以通過重寫UIControl的 sendAction:to:forEvent: 或 sendAction:to:from:forEvent: 自定義事件執(zhí)行的target及action滞磺。

另外,若不指定target莱褒,即 addTarget:action:forControlEvents: 時target傳空击困,那么當(dāng)事件發(fā)生時,Application會在響應(yīng)鏈上從上往下尋找能響應(yīng)action的對象广凸。

觸摸事件優(yōu)先級

當(dāng)原本關(guān)系已經(jīng)錯綜復(fù)雜的UIGestureRecognizer和UIResponder之間又冒出一個UIControl沛励,觸摸事件的優(yōu)先級又成了什么樣呢?
UIControl會阻止父視圖上的手勢識別器行為炮障,也就是UIControl處理事件的優(yōu)先級比UIGestureRecognizer高,但前提是相比于父視圖上的手勢識別器坤候。

測試場景:在BlueView上添加一個button胁赢,同時給button添加一個target-action事件。

  • 示例一:在BlueView上添加點擊手勢識別器
  • 示例二:在button上添加手勢識別器

測試結(jié)果:示例一中白筹,button的target-action響應(yīng)了單擊事件智末;示例二中,BlueView上的手勢識別器響應(yīng)了事件徒河。過程日志打印如下:

//示例一
-[CLTapGestureRecognizer touchesBegan:withEvent:]
-[CLButton touchesBegan:withEvent:]
-[CLButton beginTrackingWithTouch:withEvent:]
-[CLTapGestureRecognizer touchesEnded:withEvent:] after called state = 5
-[CLButton touchesEnded:withEvent:]
-[CLButton endTrackingWithTouch:withEvent:]
按鈕點擊行為
//示例二
-[CLTapGestureRecognizer touchesBegan:withEvent:]
-[CLButton touchesBegan:withEvent:]
-[CLButton beginTrackingWithTouch:withEvent:]
-[CLTapGestureRecognizer touchesEnded:withEvent:] after called state = 3
手勢觸發(fā)行為
-[CLButton touchesCancelled:withEvent:]
-[CLButton cancelTrackingWithEvent:]

原因分析:點擊button后系馆,事件先傳遞給手勢識別器,再傳遞給作為hit-tested view存在的button(UIControl本身也是UIResponder顽照,這一過程和普通事件響應(yīng)者無異)由蘑。示例一中,由于button阻止了父視圖BlueView中的手勢識別器的識別代兵,導(dǎo)致手勢識別器識別失斈崮稹(狀態(tài)為failed 枚舉值為5),button完全接手了事件的響應(yīng)權(quán)植影,事件最終由button響應(yīng)裳擎;示例二中,button未阻止其本身綁定的手勢識別器的識別思币,因此手勢識別器先識別手勢并識別成功(狀態(tài)為ended 枚舉值為3)鹿响,而后通知Application取消響應(yīng)鏈對事件的響應(yīng)羡微,因為 touchesCancelled 被調(diào)用,同時 cancelTrackingWithEvent 跟著調(diào)用惶我,因此button的target-action得不到執(zhí)行妈倔。

結(jié)論:UIControl比其父視圖上的手勢識別器具有更高的事件響應(yīng)優(yōu)先級。

解惑

回到本節(jié)開始時的測試?yán)又衼碇腹拢姆N現(xiàn)象得到了解釋启涯。
先看現(xiàn)象二,短按 cell無法響應(yīng)恃轩,日志如下:

-[GLTableView touchesBegan:withEvent:]
backview taped
-[GLTableView touchesCancelled:withEvent:]

這個日志和上面離散型手勢Demo中打印的日志完全一致结洼。短按后,BackView上的手勢識別器先接收到事件叉跛,之后事件傳遞給hit-tested view松忍,作為響應(yīng)者鏈中一員的GLTableView的 touchesBegan:withEvent: 被調(diào)用;而后手勢識別器成功識別了點擊事件筷厘,action執(zhí)行鸣峭,同時通知Application取消響應(yīng)鏈中的事件響應(yīng),GLTableView的 touchesCancelled:withEvent: 被調(diào)用酥艳。

因為事件被取消了摊溶,因此Cell無法響應(yīng)點擊。

再看現(xiàn)象三充石,長按cell能夠響應(yīng)莫换,日志如下:

-[GLTableView touchesBegan:withEvent:]
-[GLTableView touchesEnded:withEvent:]
cell selected!

長按的過程中,一開始事件同樣被傳遞給手勢識別器和hit-tested view骤铃,作為響應(yīng)鏈中一員的GLTableView的 touchesBegan:withEvent: 被調(diào)用拉岁;此后在長按的過程中,手勢識別器一直在識別手勢惰爬,直到一定時間后手勢識別失敗喊暖,才將事件的響應(yīng)權(quán)完全交給響應(yīng)鏈。當(dāng)觸摸結(jié)束的時候撕瞧,GLTableView的 touchesEnded:withEvent: 被調(diào)用陵叽,同時Cell響應(yīng)了點擊。

現(xiàn)在回到現(xiàn)象一风范。按照之前的分析咨跌,快速點擊cell,講道理不管是表現(xiàn)還是日志都應(yīng)該和現(xiàn)象二一致才對硼婿。然而日志僅僅打印了手勢識別器的action執(zhí)行結(jié)果锌半。分析一下原因:GLTableView的 touchesBegan 沒有調(diào)用,說明事件沒有傳遞給hit-tested view。那只有一種可能刊殉,就是事件被某個手勢識別器攔截了殉摔。目前已知的手勢識別器攔截事件的方法,就是設(shè)置 delaysTouchesBegan為YES记焊,在手勢識別器未識別完成的情況下不會將事件傳遞給hit-tested view逸月。然后事實上并沒有進(jìn)行這樣的設(shè)置,那么問題可能出在別的手勢識別器上遍膜。

其實是蘋果內(nèi)部的機(jī)制造成了這種現(xiàn)象碗硬,細(xì)節(jié)請查看此處。大概理解為:某一個類攔截了事件并延遲了0.15s發(fā)送瓢颅。又因為點擊時間比0.15s短恩尾,在發(fā)送事件前觸摸就結(jié)束了,因此事件沒有傳遞到hit-tested view挽懦,導(dǎo)致TableView的 touchBegin 沒有調(diào)用翰意。而現(xiàn)象二,由于短按的時間超過了0.15s信柿,手勢識別器攔截了事件并經(jīng)過0.15s后冀偶,觸摸還未結(jié)束,于是將事件傳遞給了hit-tested view渔嚷,使得TableView接收到了事件进鸠。因此現(xiàn)象二的日志雖然和離散型手勢Demo中的日志一致,但實際上前者的hit-tested view是在觸摸后延遲了約0.15s左右才接收到觸摸事件的形病。

總結(jié)

  • 觸摸發(fā)生時堤如,系統(tǒng)內(nèi)核生成觸摸事件,先由IOKit處理封裝成IOHIDEvent對象窒朋,通過IPC傳遞給系統(tǒng)進(jìn)程SpringBoard,而后再傳遞給前臺APP處理蝗岖。
  • 事件傳遞到APP內(nèi)部時被封裝成開發(fā)者可見的UIEvent對象侥猩,先經(jīng)過hit-testing尋找第一響應(yīng)者,而后由Window對象將事件傳遞給hit-tested view抵赢,并開始在響應(yīng)鏈上的傳遞欺劳。
  • UIRespnder、UIGestureRecognizer铅鲤、UIControl都可以對觸摸事件作出響應(yīng)划提,UIGestureRecognizer優(yōu)先級高于UIRespnder,而UIControl又高于父控件的UIGestureRecognizer邢享。如果是同級控件鹏往,那么UIControl可以看做是一個普通的UIRespnder來對待,即優(yōu)先級低于UIRespnder骇塘。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末伊履,一起剝皮案震驚了整個濱河市韩容,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌唐瀑,老刑警劉巖群凶,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異哄辣,居然都是意外死亡请梢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門力穗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來毅弧,“玉大人,你說我怎么就攤上這事睛廊⌒握妫” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵超全,是天一觀的道長咆霜。 經(jīng)常有香客問我,道長嘶朱,這世上最難降的妖魔是什么蛾坯? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮疏遏,結(jié)果婚禮上脉课,老公的妹妹穿的比我還像新娘。我一直安慰自己财异,他們只是感情好倘零,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著戳寸,像睡著了一般呈驶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上疫鹊,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天袖瞻,我揣著相機(jī)與錄音,去河邊找鬼拆吆。 笑死聋迎,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的枣耀。 我是一名探鬼主播霉晕,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了娄昆?” 一聲冷哼從身側(cè)響起佩微,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎萌焰,沒想到半個月后哺眯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡扒俯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年奶卓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撼玄。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡夺姑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出掌猛,到底是詐尸還是另有隱情盏浙,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布荔茬,位于F島的核電站废膘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏慕蔚。R本人自食惡果不足惜丐黄,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望孔飒。 院中可真熱鬧灌闺,春花似錦、人聲如沸坏瞄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸠匀。三九已至接校,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間狮崩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工鹿寻, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留睦柴,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓毡熏,卻偏偏與公主長得像坦敌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

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

  • 在iOS開發(fā)中經(jīng)常會涉及到觸摸事件狱窘。本想自己總結(jié)一下杜顺,但是遇到了這篇文章,感覺總結(jié)的已經(jīng)很到位蘸炸,特此轉(zhuǎn)載躬络。作者:L...
    WQ_UESTC閱讀 6,011評論 4 26
  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的?困惑于Cell怎么突然不能點擊了搭儒?糾結(jié)于如何實現(xiàn)這個奇葩響應(yīng)需求穷当?亦或是...
    Lotheve閱讀 57,148評論 51 599
  • 在開發(fā)過程中,大家或多或少的都會碰到令人頭疼的手勢沖突問題淹禾,正好前兩天碰到一個類似的bug馁菜,于是借著這個機(jī)會了解了...
    閆仕偉閱讀 5,333評論 2 23
  • 系統(tǒng)響應(yīng)階段 1.手指觸碰屏幕,屏幕感受到觸摸后铃岔,將事件交由IOKit來處理汪疮。 2.IOKIT將觸摸事件封裝成IO...
    雪山飛狐_91ae閱讀 7,374評論 4 37
  • -- iOS事件全面解析 概覽 iPhone的成功很大一部分得益于它多點觸摸的強(qiáng)大功能,喬布斯讓人們認(rèn)識到手機(jī)其實...
    翹楚iOS9閱讀 2,961評論 0 13