(八)解釋栗子
本文系轉載,原文地址為iOS觸控事件全家桶
現在,把膠捲回放到本章節開頭的場景。給你一杯咖啡的時間看看能不能解釋得通那幾個現象了,不說了泡咖啡去了...
我肥來了!
先看現象二,短按 cell無法響應,日誌如下:
-[GLTableView touchesBegan:withEvent:]
backview taped
-[GLTableView touchesCancelled:withEvent:]
這個日誌和上面離散型手勢Demo中列印的日誌完全一致。短按後,BackView上的手勢識別器先接收到事件,之後事件傳遞給hit-tested view,作為響應者鏈中一員的GLTableView的 touchesBegan:withEvent:
被呼叫;而後手勢識別器成功識別了點選事件,action執行,同時通知Application取消響應鏈中的事件響應,GLTableView的 touchesCancelled:withEvent:
被呼叫。
因為事件被取消了,因此Cell無法響應點選。
再看現象三,長按cell能夠響應,日誌如下:
-[GLTableView touchesBegan:withEvent:]
-[GLTableView touchesEnded:withEvent:]
cell selected!
長按的過程中,一開始事件同樣被傳遞給手勢識別器和hit-tested view,作為響應鏈中一員的GLTableView的 touchesBegan:withEvent:
被呼叫;此後在長按的過程中,手勢識別器一直在識別手勢,直到一定時間後手勢識別失敗,才將事件的響應權完全交給響應鏈。當觸控結束的時候,GLTableView的 touchesEnded:withEvent:
被呼叫,同時Cell響應了點選。
OK,現在回到現象一。按照之前的分析,快速點選cell,講道理不管是表現還是日誌都應該和現象二一致才對。然而日誌僅僅列印了手勢識別器的action執行結果。分析一下原因:GLTableView的 touchesBegan
沒有呼叫,說明事件沒有傳遞給hit-tested view。那只有一種可能,就是事件被某個手勢識別器攔截了。目前已知的手勢識別器攔截事件的方法,就是設定 delaysTouchesBegan
為YES,在手勢識別器未識別完成的情況下不會將事件傳遞給hit-tested view。然後事實上並沒有進行這樣的設定,那麼問題可能出在別的手勢識別器上。
Window的 sendEvent:
打個斷點檢視event上的touch物件維護的手勢識別器陣列:
捕獲可疑物件:UIScrollViewDelayedTouchesBeganGestureRecognizer
,光看名字就覺得這貨脫不了干係。從類名上猜測,這個手勢識別器大概會延遲事件向響應鏈的傳遞。github上找到了該私有類的標頭檔案:
@interface UIScrollViewDelayedTouchesBeganGestureRecognizer : UIGestureRecognizer {
UIView<UIScrollViewDelayedTouchesBeganGestureRecognizerClient> * _client;
struct CGPoint {
float x;
float y;
} _startSceneReferenceLocation;
UIDelayedAction * _touchDelay;
}
- (void).cxx_destruct;
- (id)_clientView;
- (void)_resetGestureRecognizer;
- (void)clearTimer;
- (void)dealloc;
- (void)sendDelayedTouches;
- (void)sendTouchesShouldBeginForDelayedTouches:(id)arg1;
- (void)sendTouchesShouldBeginForTouches:(id)arg1 withEvent:(id)arg2;
- (void)touchesBegan:(id)arg1 withEvent:(id)arg2;
- (void)touchesCancelled:(id)arg1 withEvent:(id)arg2;
- (void)touchesEnded:(id)arg1 withEvent:(id)arg2;
- (void)touchesMoved:(id)arg1 withEvent:(id)arg2;
@end
有一個_touchDelay變數,大概是用來控制延遲事件傳送的。另外,方法列表裡有個 sendTouchesShouldBeginForDelayedTouches:
方法,聽名字似乎是在一段時間延遲後向響應鏈傳遞事件用的。為一探究竟,我建立了一個類hook了這個方法:
//TouchEventHook.m
+ (void)load{
Class aClass = objc_getClass("UIScrollViewDelayedTouchesBeganGestureRecognizer");
SEL sel = @selector(hook_sendTouchesShouldBeginForDelayedTouches:);
Method method = class_getClassMethod([self class], sel);
class_addMethod(aClass, sel, class_getMethodImplementation([self class], sel), method_getTypeEncoding(method));
exchangeMethod(aClass, @selector(sendTouchesShouldBeginForDelayedTouches:), sel);
}
- (void)hook_sendTouchesShouldBeginForDelayedTouches:(id)arg1{
[self hook_sendTouchesShouldBeginForDelayedTouches:arg1];
}
void exchangeMethod(Class aClass, SEL oldSEL, SEL newSEL) {
Method oldMethod = class_getInstanceMethod(aClass, oldSEL);
Method newMethod = class_getInstanceMethod(aClass, newSEL);
method_exchangeImplementations(oldMethod, newMethod);
}
斷點看一下點選cell後 hook_sendTouchesShouldBeginForDelayedTouches:
呼叫時的資訊:
可以看到這個手勢識別器的 _touchDelay 變數中,儲存了一個計時器,以及一個長得很像延遲時間間隔的變數m_delay。現在,可以推測該手勢識別器截斷了事件並延遲0.15s才傳送給hit-tested view。為驗證猜測,我分別在Window的 sendEvent:
,hook_sendTouchesShouldBeginForDelayedTouches:
以及TableView的 touchesBegan:
中列印時間戳,若猜測成立,則應當前兩者的呼叫時間相差0.15s左右,後兩者的呼叫時間很接近。短按Cell後列印結果如下(不能快速點選,否則還沒過延遲時間觸控就結束了,無法驗證猜測):
-[GLWindow sendEvent:]呼叫時間戳 :
525252194779.07ms
-[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]呼叫時間戳 :
525252194930.91ms
-[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]呼叫時間戳 :
525252194931.24ms
-[GLTableView touchesBegan:withEvent:]呼叫時間戳 :
525252194931.76ms
因為有兩個 UIScrollViewDelayedTouchesBeganGestureRecognizer
,所以 hook_sendTouchesShouldBeginForDelayedTouches
調了兩次,兩次的時間很接近。可以看到,結果完全符合猜測。
這樣就都解釋得通了。現象一由於點選後,UIScrollViewDelayedTouchesBeganGestureRecognizer
攔截了事件並延遲了0.15s傳送。又因為點選時間比0.15s短,在傳送事件前觸控就結束了,因此事件沒有傳遞到hit-tested view,導致TableView的 touchBegin
沒有呼叫。而現象二,由於短按的時間超過了0.15s,手勢識別器攔截了事件並經過0.15s後,觸控還未結束,於是將事件傳遞給了hit-tested view,使得TableView接收到了事件。因此現象二的日誌雖然和離散型手勢Demo中的日誌一致,但實際上前者的hit-tested view是在觸控後延遲了約0.15s左右才接收到觸控事件的。
至於現象四 ,你現在應該已經覺得理所當然了才對。
相關文章
- 「責任鏈模式」栗子模式
- Net 呼叫 Graph API 的小栗子API
- Redis系列(八):釋出與訂閱Redis
- 舉個小栗子來聊下效能優化優化
- Isito 入門(八):金絲雀釋出
- 八大排序演算法(解釋+程式碼+結果+演算法優化)排序演算法優化
- 舉個栗子看如何做MySQL 核心深度優化MySql優化
- 筆記八:URI Search 詳解筆記
- ES 筆記八:URI Search 詳解筆記
- HDR解釋
- 你不知道的閉包原理【三個栗子徹底理解】
- 70個react-native flex佈局栗子,肯定有你要的ReactFlex
- 舉個栗子!Tableau技巧(36):快速製作巢狀條形圖巢狀
- Selenium用法詳解 -- selenium八大定位詳解
- QT從入門到入土(八)——專案打包和釋出QT
- jackson學習之八:常用方法註解
- MyBatis(八):MyBatis外掛機制詳解MyBatis
- Flutter 佈局(八)- Stack、IndexedStack、GridView詳解FlutterIndexView
- 一個基於 gin+ grpc + etcd 等框架開發的小栗子RPC框架
- linux load解釋Linux
- epoll的解釋
- 【Linux】getline解釋Linux
- ChatGPT的解釋ChatGPT
- OCVMax 工具解釋
- Python Mixin解釋Python
- 演算法(八):圖解KNN演算法演算法圖解KNN
- 創新實訓(八)——題目相關的邏輯處理解釋
- JavaScript註釋:單行註釋和多行註釋詳解JavaScript
- 電影名解釋
- MIPI圖解簡釋圖解
- Rc+RefCell解釋
- 歌曲字尾解釋
- npm命令及解釋NPM
- dd命令的解釋
- spring IOC 通俗解釋Spring
- (八)Flutter 和 Native 之間的通訊詳解Flutter
- 從八個方面讓你快速瞭解cordova(一)
- 回溯法(排列樹)解決八(N)皇后問題