目錄
先說一下事件處理里的被處理者:事件
一、iOS里的事件
二、Flutter里的事件
然后說一下事件處理里的處理者:響應(yīng)者
三霎俩、iOS里的響應(yīng)者和響應(yīng)者鏈
四坏为、Flutter里的響應(yīng)者和響應(yīng)者數(shù)組
然后再說一下響應(yīng)者具體是怎么處理事件的
五、iOS里的尋找第一響應(yīng)者锭沟、事件傳遞和事件響應(yīng)
六、Flutter里的尋找第一響應(yīng)者、事件分發(fā)和事件響應(yīng)
最后補充一下原始指針事件和手勢同時存在時會怎樣
七辽剧、iOS里的手勢
八、Flutter里的手勢
iOS和Flutter里的事件處理
iOS和Flutter里的事件處理實例
一税产、iOS里的事件
iOS里的事件分三類:
- 觸摸事件(本篇我們主要研究一下觸摸事件)
- 加速計事件
- 遠(yuǎn)程控制事件
UITouch
我們的一根手指觸摸屏幕怕轿,系統(tǒng)就會為其創(chuàng)建一個對應(yīng)的UITouch
對象偷崩,多根手指觸摸屏幕,系統(tǒng)就會為其創(chuàng)建多個對應(yīng)的UITouch
對象撞羽。每個UITouch
對象內(nèi)部都存儲著對應(yīng)手指觸摸屏幕的時間阐斜、位置、力度诀紊、所在的window
谒出、所在的view
等信息。當(dāng)手指離開屏幕一小段時間后邻奠,系統(tǒng)判定對應(yīng)的UITouch
對象不會再更新笤喳,就會銷毀它。
@interface UITouch : NSObject
// 時間
@property (nonatomic,readonly) NSTimeInterval timestamp;
// 當(dāng)前觸摸的點在[view]坐標(biāo)系統(tǒng)下的位置
- (CGPoint)locationInView:(nullable UIView *)view;
// 上一個觸摸的點在[view]坐標(biāo)系統(tǒng)下的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;
// 力度
@property (nonatomic,readonly) CGFloat force API_AVAILABLE(ios(9.0));
// 所在的window
@property (nullable,nonatomic,readonly,strong) UIWindow *window;
// 所在的view
@property (nullable,nonatomic,readonly,strong) UIView *view;
// 尋找第一響應(yīng)者的過程中碌宴,該數(shù)組會搜集第一響應(yīng)者h(yuǎn)itTested view上添加的手勢杀狡、hitTested view父視圖上添加的手勢、...贰镣、直到window上添加的手勢捣卤,這一串view的手勢都會被依次添加到這個數(shù)組里
@property (nullable,nonatomic,readonly,copy) NSArray<UIGestureRecognizer *> *gestureRecognizers API_AVAILABLE(ios(3.2));
@end
UIEvent
創(chuàng)建UITouch
對象的時候,系統(tǒng)也會創(chuàng)建一個UIEvent
對象八孝。UIEvent
對象內(nèi)部存儲著當(dāng)前觸摸事件的類型以及觸發(fā)當(dāng)前觸摸事件的UITouch
對象集合(因為一個觸摸事件可能是由多根手指共同觸發(fā)的)董朝。銷毀UITouch
對象的時候,也會銷毀UIEvent
對象干跛。
@interface UIEvent : NSObject
// 當(dāng)前觸摸事件的類型
@property (nonatomic,readonly) UIEventType type API_AVAILABLE(ios(3.0));
// 觸發(fā)當(dāng)前觸摸事件的UITouch對象集合
@property (nonatomic, readonly, nullable) NSSet<UITouch *> *allTouches;
@end
二子姜、Flutter里的事件
Flutter里也有相應(yīng)的觸摸事件,叫PointerEvent
及其子類PointerDownEvent
楼入、PointerMoveEvent
哥捕、PointerCancelEvent
、PointerUpEvent
嘉熊,它們內(nèi)部存儲的東西和iOS差不多遥赚,如觸摸屏幕的時間、位置(相對于全局坐標(biāo)系統(tǒng)阐肤,如有需要我們得自己轉(zhuǎn)換成局部坐標(biāo))凫佛、力度、當(dāng)前觸摸事件的類型等孕惜。
abstract class PointerEvent with Diagnosticable {
const PointerEvent({
// 當(dāng)前原始指針事件的唯一標(biāo)識
this.pointer = 0,
// 觸摸屏幕的時間
this.timeStamp = Duration.zero,
// 觸摸屏幕的位置
this.position = Offset.zero,
// 觸摸屏幕的力度
this.pressure = 1.0,
this.pressureMin = 1.0,
this.pressureMax = 1.0,
// 當(dāng)前觸摸事件的類型
this.kind = PointerDeviceKind.touch,
// 兩次原始指針移動事件(PointerMoveEvent)的距離
this.delta = Offset.zero,
// 是否正摸著屏幕
final bool down;
...
});
}
class PointerDownEvent extends PointerEvent {
...
}
class PointerMoveEvent extends PointerEvent {
...
}
class PointerUpEvent extends PointerEvent {
...
}
class PointerCancelEvent extends PointerEvent {
...
}
三愧薛、iOS里的響應(yīng)者和響應(yīng)者鏈
響應(yīng)者
iOS里并非所有的對象都能傳遞和響應(yīng)事件,只有繼承自UIResponder
的才行衫画,這類對象被稱之為響應(yīng)者毫炉。我們常見的UIApplication
、UIViewController
削罩、UIView
都繼承自UIResponder
瞄勾,所以它們都能傳遞和響應(yīng)事件费奸。注意這里的傳遞是指尋找第一響應(yīng)者階段父視圖通過hitTest
方法把事件傳遞給子視圖以及事件傳遞階段UIApplication
、window
通過sendEvent
方法把事件精準(zhǔn)地傳遞給第一響應(yīng)者进陡,響應(yīng)是指事件響應(yīng)階段UIResponder
通過touchesBegan愿阐、touchesMoved、touchesEnded四濒、touchesCancelled
四個方法來響應(yīng)事件换况。
響應(yīng)者鏈
實際開發(fā)中职辨,我們的屏幕上肯定不止一個view
盗蟆,也就是說不止一個響應(yīng)者,而這眾多的響應(yīng)者之間會通過nextResponder
屬性串起來形成一個叫響應(yīng)者鏈的東西舒裤,這個鏈的形成時機是我們把view
層級的代碼寫好后就形成了(可以在addSubview:
之后打印驗證)喳资,不需要等到hitTest
的時候。也就是說當(dāng)我們把一個view
添加到它的父視圖上后腾供,該view
的nextResponder
就已經(jīng)指向了它的父視圖仆邓;父視圖的nextResponder
又會指向rootViewController
的view
;rootViewController
的view
的nextResponder
又會指向rootViewController
伴鳖;rootViewController
的nextResponder
又會指向window
节值;window
的nextResponder
又會指向UIApplication
。
四榜聂、Flutter里的響應(yīng)者和響應(yīng)者數(shù)組
響應(yīng)者
Flutter里也并非所有的對象都能傳遞和響應(yīng)事件搞疗,只有真正渲染在屏幕上的東西——即RenderObject
(相當(dāng)于iOS里的UIView
)才行,我們也把它們稱之為響應(yīng)者须肆,同時也只有一個特殊的RenderObject
——RenderPointerListener
(對應(yīng)的渲染對象Widget為Listener
)才能響應(yīng)事件(iOS里是所有的UIView
都能響應(yīng)事件)匿乃。注意這里的傳遞是指尋找第一響應(yīng)者階段父視圖通過hitTest
方法把事件(準(zhǔn)確地說是點擊的位置)傳遞給子視圖,響應(yīng)是指事件響應(yīng)階段RenderPointerListener/Listener
通過onPointerDown豌汇、onPointerMove幢炸、onPointerUp、onPointerCancel
四個方法來響應(yīng)事件拒贱。
這里我們回顧一個知識點:
——Widget
------------ComponentWidget(組件Widget)
————————StatelessWidget
————————StatefulWidget
————RenderObjectWidget(渲染對象Widget)
————————SingleChildRenderObjectWidget
————————MultiChildRenderObjectWidgetWidget可以分為兩類:組件Widget和渲染對象Widget宛徊。
- 組件Widget是指那些僅僅起到包裝其它Widget的作用、Flutter Framework并不會為它們創(chuàng)建對應(yīng)的
RenderObject
的Widget逻澳,例如我們常用的Container岩调、Text、Image赡盘、ListView号枕、GridView、PageView陨享、自定義的Widget等葱淳,總之但凡是繼承自StatelessWidget或StatefulWidget的Widget都是組件Widget钝腺。- 渲染對象Widget是指那些Flutter Framework會為它們創(chuàng)建對應(yīng)的
RenderObject
的Widget,例如我們常用的SizedBox赞厕、Row艳狐、Column等,總之但凡是繼承自RenderObjectWidget的Widget都是渲染對象Widget皿桑。也就是說毫目,組件Widget肯定都不是響應(yīng)者,因為它們壓根兒都沒真正渲染在屏幕上诲侮,只有渲染對象Widget才是響應(yīng)者(準(zhǔn)確地說是它們對應(yīng)的
RenderObject
才是響應(yīng)者)镀虐,因為它們才會被轉(zhuǎn)換成RenderObject
真正渲染在屏幕上。因此沟绪,如果我們想重寫某些Widget的
hitTest
方法刮便,就不能繼承自組件Widget,因為組件Widget根本就沒有hitTest
方法绽慈,而必須繼承自渲染對象Widget恨旱,它對應(yīng)的RenderObject
才有hitTest
方法。同時RenderObject
是個抽象類坝疼,真正渲染在屏幕上的東西其實是它的子類RenderBox
搜贤,而RenderBox
又是個抽象類,真正渲染在屏幕上的東西其實又是它的子類:單個子對象的時候钝凶,就用RenderProxyBox
或RenderShiftedBox
仪芒,它倆的主要區(qū)別是前者沒有跟布局相關(guān)的屬性,后者有跟布局相關(guān)的屬性腿椎;多個子對象的時候桌硫,就用ContainerRenderObjectMixin
,我們可以根據(jù)實際情況給渲染對象Widgetcreate
不同的RenderObject
啃炸。
響應(yīng)者數(shù)組
這里和iOS稍有不同铆隘,iOS里是響應(yīng)者鏈,F(xiàn)lutter里是響應(yīng)者數(shù)組南用,不過兩者的用途差不多膀钠。
iOS里的響應(yīng)者鏈?zhǔn)侵副姸嗟捻憫?yīng)者之間會通過nextResponder
屬性串起來形成一個鏈,當(dāng)前響應(yīng)者的nextResponder
就是下一個響應(yīng)者裹虫,這個鏈的形成時機是我們把view
層級的代碼寫好后就形成了肿嘲,不需要等到hitTest
的時候。Flutter里的響應(yīng)者數(shù)組是指眾多的響應(yīng)者會按順序放在一個數(shù)組里筑公,當(dāng)前響應(yīng)者在數(shù)組里的下一個元素就是下一個響應(yīng)者雳窟,這個數(shù)組的形成時機是hitTest
的時候(這里的形成時機是指數(shù)組把響應(yīng)者全都add
進(jìn)去,不是指數(shù)組本身的創(chuàng)建)匣屡。
五封救、iOS里的尋找第一響應(yīng)者拇涤、事件傳遞和事件響應(yīng)
有了前兩節(jié)的理論知識,我們就來回答一個問題“手指觸摸屏幕后誉结,發(fā)生了什么”鹅士,一共分三步:
- 第一步:尋找第一響應(yīng)者
- 第二步:事件傳遞
- 第三步:事件響應(yīng)
尋找第一響應(yīng)者
手指觸摸屏幕后,就發(fā)生了一個觸摸事件惩坑,但是這個時候屏幕上可能會有很多個響應(yīng)者掉盅,也就是說可能會有很多個view
,那到底該由誰來響應(yīng)這個觸摸事件呢以舒?因此第一步就是要尋找一個最適合響應(yīng)該觸摸事件的響應(yīng)者——第一響應(yīng)者firstResponder
趾痘。
尋找第一響應(yīng)者的過程涉及到一個關(guān)鍵方法hitTest
,尋找第一響應(yīng)者的過程可以說就是一個遞歸調(diào)用hitTest
的過程:
/// @return 當(dāng)前view所在層級的第一響應(yīng)者
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 如果當(dāng)前view不能響應(yīng)事件:當(dāng)前view不允許用戶交互 || 當(dāng)前view隱藏了 || 當(dāng)前view的透明度小于等于0.01
// 那么當(dāng)前view不能作為第一響應(yīng)者
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
return nil;
}
// 如果觸摸的點不在當(dāng)前view身上
// 那么當(dāng)前view不能作為第一響應(yīng)者
if ([self pointInside:point withEvent:event] == NO) {
return nil;
}
// 如果過了前兩關(guān)稀轨,則代表當(dāng)前view有可能作為第一響應(yīng)者扼脐,但不一定就是它
// 還得倒序遍歷它的子視圖岸军,優(yōu)先它的子視圖做第一響應(yīng)者
for (int i = self.subviews.count - 1; i >= 0; i--) {
// 獲取子視圖
UIView *childView = self.subviews[I];
// 把觸摸點的坐標(biāo)轉(zhuǎn)換到子視圖的坐標(biāo)系統(tǒng)下
CGPoint convertedPoint = [self convertPoint:point toView:childView];
// 調(diào)用子視圖的hitTest方法奋刽,把觸摸事件傳遞給子視圖,尋找子視圖這一層級的第一響應(yīng)者
UIView *fitView = [childView hitTest:convertedPoint withEvent:event];
if (fitView) { // 如果最終找到了就返回
return fitView;
}
}
// 只有當(dāng)前view沒有子視圖 || 它的子視圖都不能響應(yīng)事件 || 觸摸的點都不在它的子視圖身上時
// 當(dāng)前view才直接作為第一響應(yīng)者
return self;
}
-
手指觸摸屏幕后艰赞,就發(fā)生了一個觸摸事件佣谐,系統(tǒng)會先把這個觸摸事件放進(jìn)
UIApplication
管理的一個事件隊列里,等輪到處理該觸摸事件時方妖,UIApplication
就會把該觸摸事件出列狭魂,并把該觸摸事件倒序傳遞給應(yīng)用程序的window
;(注意:因為這個時候才剛開始尋找第一響應(yīng)者党觅,所以觸摸事件還不知道它所在的window
是哪個雌澄,因此UIApplication
還不能精準(zhǔn)地給某個特定的window
傳遞事件,而是按顯示順序倒序地給很多個window
都傳遞) -
window
接收到觸摸事件后杯瞻,就會調(diào)用自己的hitTest
方法镐牺,如果發(fā)現(xiàn)自己不能響應(yīng)事件或者觸摸的點不在自己身上,UIApplication
就會把觸摸事件傳遞給其它的window
魁莉,通常情況下觸摸事件最終會被傳遞給keyWindow
睬涧,keyWindow
也會調(diào)用自己的hitTest
方法,通常情況下keyWindow
能響應(yīng)事件并且觸摸的點也在keyWindow
身上旗唁,于是keyWindow
又會把該觸摸事件倒序傳遞給它的子視圖畦浓;(注意:這里執(zhí)行完,就找到了觸摸事件所在的window
检疫,UIEvent.UITouch.window
屬性就有值了) -
子視圖接收到觸摸事件后讶请,就會調(diào)用自己的
hitTest
方法,如果發(fā)現(xiàn)自己不能響應(yīng)事件或者觸摸的點不在自己身上屎媳,keyWindow
就會把觸摸事件傳遞給其它的子視圖夺溢,如果某個子視圖能響應(yīng)事件并且觸摸的點也在它身上抹蚀,那么它就會繼續(xù)把觸摸事件倒序傳遞給它的子視圖......如此循環(huán),直到找到第一響應(yīng)者——即觸摸事件所在的view
企垦。(注意:這里執(zhí)行完环壤,就找到了觸摸事件所在的view
,UIEvent.UITouch.view
屬性就有值了)
舉個例子钞诡,view
層級如下:
WhiteView(rootViewController的view)
————RedView
————————YellowView
————OrangeView
————————GreenView
————————CyanView
- 假設(shè)我們觸摸了
GreenView
郑现; -
keyWindow
調(diào)用hitTest
方法,發(fā)現(xiàn)自己能響應(yīng)事件并且觸摸的點也在自己身上荧降,于是就把觸摸事件傳遞給它的子視圖WhiteView
接箫; -
WhiteView
調(diào)用hitTest
方法,發(fā)現(xiàn)自己能響應(yīng)事件并且觸摸的點也在自己身上朵诫,于是就把觸摸事件傳遞給它的子視圖OrangeView
辛友; -
OrangeView
調(diào)用hitTest
方法,發(fā)現(xiàn)自己能響應(yīng)事件并且觸摸的點也在自己身上剪返,于是就把觸摸事件傳遞給它的子視圖CyanView
废累; -
CyanView
調(diào)用hitTest
方法,發(fā)現(xiàn)自己能響應(yīng)事件但是觸摸的點不在自己身上脱盲,于是OrangeView
又把觸摸事件傳遞給它的子視圖GreenView
邑滨; -
GreenView
調(diào)用hitTest
方法,發(fā)現(xiàn)自己能響應(yīng)事件并且觸摸的點在自己身上钱反,但是自己已經(jīng)沒有子視圖了掖看,于是不再做事件傳遞,所以GreenView
就成為第一響應(yīng)者面哥。
一些經(jīng)驗:
hitTest
第一關(guān):如果一個父視圖不能響應(yīng)事件哎壳,那么它的子視圖肯定就不能響應(yīng)事件,因為父視圖壓根就沒機會把觸摸事件傳遞給子視圖尚卫,子視圖都不知道這個觸摸事件的存在归榕,當(dāng)然就不能響應(yīng)事件;hitTest
第二關(guān):如果一個父視圖能響應(yīng)事件焕毫,它的子視圖不能響應(yīng)事件蹲坷,那么點擊子視圖時父視圖就會響應(yīng)事件,但是如果子視圖有超出父視圖的部分邑飒,那么點擊子視圖超出父視圖的部分時循签,父視圖就不會響應(yīng)事件,因為父視圖在判斷到觸摸的點不在自己身上時就會直接return nil
疙咸,而不會作為第一響應(yīng)者县匠;- 尋找第一響應(yīng)者的過程中存在事件傳遞,父視圖是通過
hitTest
方法把事件傳遞給子視圖的,因此在實際開發(fā)中我們可以重寫視圖的hitTest
方法來自定義到底由誰來做第一響應(yīng)者——即到底由誰來響應(yīng)觸摸事件乞旦。至于該重寫誰的hitTest
方法贼穆,我們只需要分析一遍hitTest
的過程,看看到底是誰的hitTest
方法導(dǎo)致不滿足實際需求就可以了兰粉。
事件傳遞
經(jīng)過第一步尋找第一響應(yīng)者故痊,UIApplication
就知道觸摸事件該由誰來響應(yīng)了,因為UIEvent.UITouch.window
屬性和UIEvent.UITouch.view
屬性都有值了玖姑,接下來要做的就是第二步:將觸摸事件傳遞給第一響應(yīng)者愕秫。UIApplication
會通過sendEvent
方法把觸摸事件精準(zhǔn)地傳遞給觸摸事件所在的window
,window
又會通過sendEvent
方法把觸摸事件精準(zhǔn)地傳遞給觸摸事件所在的view
——即第一響應(yīng)者焰络。
還是上面的例子戴甩,我們在GreenView
的touchesBegan
方法里打個斷點,觸摸一下GreenView
闪彼,查看方法調(diào)用棧就能看到事件傳遞的過程:
事件響應(yīng)
我們知道UIResponder
內(nèi)部提供了四個方法來響應(yīng)觸摸事件甜孤,實際上系統(tǒng)為所有的響應(yīng)者都默認(rèn)實現(xiàn)了這四個方法,只不過大家默認(rèn)的實現(xiàn)都是什么都不做畏腕,只是調(diào)用父類的touches...
方法把觸摸事件沿著響應(yīng)者鏈傳遞給nextResponder
缴川。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 什么都不做
// 只是調(diào)用父類的touches...方法把觸摸事件沿著響應(yīng)者鏈傳遞給nextResponder
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 什么都不做
// 只是調(diào)用父類的touches...方法把觸摸事件沿著響應(yīng)者鏈傳遞給nextResponder
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 什么都不做
// 只是調(diào)用父類的touches...方法把觸摸事件沿著響應(yīng)者鏈傳遞給nextResponder
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 什么都不做
// 只是調(diào)用父類的touches...方法把觸摸事件沿著響應(yīng)者鏈傳遞給nextResponder
[super touchesCancelled:touches withEvent:event];
}
經(jīng)過第二步事件傳遞,第一響應(yīng)者就接收到了需要響應(yīng)的觸摸事件郊尝,接下來要做的就是第三步:事件響應(yīng)二跋。如果第一響應(yīng)者重寫了觸摸事件的四個方法战惊,那么它就會響應(yīng)該觸摸事件流昏;如果第一響應(yīng)者沒有重寫觸摸事件的四個方法,那么它就不會響應(yīng)該觸摸事件吞获,該觸摸事件就會默認(rèn)地沿著響應(yīng)者鏈傳遞給第一響應(yīng)者的nextResponder
况凉;這樣一直傳一直傳,如果觸摸事件傳遞到
了window
乃至UIApplication
各拷,UIApplication
都沒有重寫觸摸事件的四個方法刁绒,那么該觸摸事件就會被丟棄。
一些經(jīng)驗:
- 實際開發(fā)中我們可以重寫這四個方法來完成一些自定義的操作烤黍,并且可以主動決定要不要調(diào)用父類的
touches...
方法來把觸摸事件繼續(xù)沿著響應(yīng)者鏈傳遞知市。
六、Flutter里的尋找第一響應(yīng)者速蕊、事件分發(fā)和事件響應(yīng)
那在Flutter里“手指觸摸屏幕后嫂丙,發(fā)生了什么”,答案和iOS基本一樣规哲,還是分三步:
- 第一步:尋找第一響應(yīng)者
- 第二步:事件分發(fā)(iOS里是事件傳遞)
- 第三步:事件響應(yīng)
尋找第一響應(yīng)者
Flutter里尋找第一響應(yīng)者的過程和iOS里幾乎一模一樣跟啤,都是一個遞歸調(diào)用hitTest
的過程——PointerDown
事件發(fā)生后,就從根視圖RenderView
的hitTest
方法開始,倒序遞歸調(diào)用子視圖的hitTest
方法隅肥,如果判斷到觸摸的點在某個視圖內(nèi)部竿奏,就把它放進(jìn)響應(yīng)者數(shù)組里,位于視圖層級上方的視圖會被優(yōu)先放進(jìn)響應(yīng)者數(shù)組腥放,最終響應(yīng)者數(shù)組的第一個元素就會成為第一響應(yīng)者泛啸。hitTest
的默認(rèn)實現(xiàn):
bool hitTest(HitTestResult result, { @required Offset position }) {
...
if (_size.contains(position)) { // 如果觸摸的點在Widget范圍內(nèi)
// 就去檢測在不在子視圖的范圍內(nèi) || 執(zhí)行hitTestSelf
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
// 如果在子視圖的范圍內(nèi),就把子視圖和自己都添加進(jìn)響應(yīng)者數(shù)組
result.add(BoxHitTestEntry(this, position));
// 同時return true秃症,告訴父視圖已經(jīng)命中了平痰,不用去hitTest自己的兄弟視圖了
return true;
}
}
// 如果觸摸的點不在Widget范圍內(nèi),直接return false伍纫,告訴父視圖去hitTest自己的兄弟視圖
return false;
}
還是iOS里舉過的例子宗雇,view
層級如下:
WhiteView
————RedView
————————YellowView
————OrangeView
————————GreenView
————————CyanView
- 假設(shè)我們觸摸了
GreenView
; -
RenderView
調(diào)用hitTest
方法莹规,發(fā)現(xiàn)觸摸的點也在自己身上赔蒲,于是就把觸摸事件傳遞給它的子視圖WhiteView
; -
WhiteView
調(diào)用hitTest
方法良漱,發(fā)現(xiàn)觸摸的點也在自己身上舞虱,于是就把觸摸事件傳遞給它的子視圖OrangeView
; -
OrangeView
調(diào)用hitTest
方法母市,發(fā)現(xiàn)觸摸的點也在自己身上矾兜,于是就把觸摸事件傳遞給它的子視圖CyanView
; -
CyanView
調(diào)用hitTest
方法患久,發(fā)現(xiàn)觸摸的點不在自己身上椅寺,于是OrangeView
又把觸摸事件傳遞給它的子視圖GreenView
; -
GreenView
調(diào)用hitTest
方法蒋失,發(fā)現(xiàn)觸摸的點在自己身上返帕,但是自己已經(jīng)沒有子視圖了,于是不再做事件傳遞篙挽,所以GreenView
就成為第一響應(yīng)者——即響應(yīng)者數(shù)組里的第一個元素荆萤; - 經(jīng)過這么一輪查找,響應(yīng)者數(shù)組里依次存放的就是
[GreenView铣卡、OrangeView链韭、WhiteView、RenderView]
煮落。
事件分發(fā)
這一步和iOS有區(qū)別敞峭,iOS里是事件傳遞——即window
會精準(zhǔn)地把觸摸事件傳遞給第一響應(yīng)者,而Flutter里是事件分發(fā)——即GestureBinding
會遍歷響應(yīng)者數(shù)組里所有的響應(yīng)者州邢,按順序把觸摸事件分發(fā)給所有的響應(yīng)者儡陨。
------GestureBinding------
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
// hitTestResult.path就是響應(yīng)者數(shù)組
// entry就是對響應(yīng)者包裝后的一個對象褪子,entry.target就是響應(yīng)者
for (final HitTestEntry entry in hitTestResult.path) {
// entry.target.handleEvent就是調(diào)用響應(yīng)者的handleEvent方法,而handleEvent方法里會真正調(diào)用Listener的四個方法
entry.target.handleEvent(event.transformed(entry.transform), entry);
}
}
事件響應(yīng)
這一步和iOS也有區(qū)別骗村,iOS里是如果第一響應(yīng)者重寫了觸摸事件的四個方法嫌褪,那么它就會響應(yīng)觸摸事件,其它的響應(yīng)者默認(rèn)是不響應(yīng)事件的(當(dāng)然我們也可以自己搞得其它響應(yīng)者也響應(yīng)事件)胚股,只有第一響應(yīng)者沒有重寫觸摸事件的四個方法時笼痛,它才不會響應(yīng)該觸摸事件,該觸摸事件就會默認(rèn)地沿著響應(yīng)者鏈傳遞給第一響應(yīng)者的nextResponder
來響應(yīng)琅拌。而Flutter里因為是一次性給所有的響應(yīng)者都分發(fā)了事件缨伊,所以只要是實現(xiàn)了四個方法的Listener
都會響應(yīng)事件,沒實現(xiàn)的就不響應(yīng)进宝,不存在往下一個響應(yīng)者傳遞這么一說刻坊,只不過是第一響應(yīng)者會第一個響應(yīng)、第二個響應(yīng)者會第二個響應(yīng)等等党晋。
七谭胚、iOS里的手勢
這里我們不專門說手勢,主要說一下原始指針事件和手勢同時存在時會怎樣未玻。
UIGestureRecognizer的優(yōu)先級
實際上,iOS里并非只有UIResponder
才能傳遞和響應(yīng)事件扳剿,UIGestureRecognizer
也行旁趟,而且UIGestureRecognizer
本質(zhì)上就是對UIResponder
四個方法的封裝。常見的手勢有點按手勢UITapGestureRecognizer
庇绽、輕掃手勢UISwipeGestureRecognizer
锡搜,平移手勢UIPanGestureRecognizer
、旋轉(zhuǎn)手勢UIRotationGestureRecognizer
敛劝、縮放手勢UIPinchGestureRecognizer
余爆、長按手勢UILongPressGestureRecognizer
。
現(xiàn)在看個例子:
ViewController.view
上添加了一個redView
夸盟,redView
上添加了一個平移手勢,并且redView
實現(xiàn)了UIResponder
的四個方法像捶。
------ViewController.h------
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
------ViewController.m------
#import "ViewController.h"
#import "RedView.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet RedView *redView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// redView添加平移手勢
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
[self.redView addGestureRecognizer:panGestureRecognizer];
}
- (void)pan:(UIPanGestureRecognizer *)panGestureRecognizer {
NSLog(@"redView panned");
}
@end
------RedView.h------
#import <UIKit/UIKit.h>
@interface RedView : UIView
@end
------RedView.m------
#import "RedView.h"
@implementation RedView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"redView touchesBegan");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"redView touchesMoved");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"redView touchesEnded");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"redView touchesCancelled");
}
@end
在redView
上執(zhí)行一次滑動上陕,控制臺的打印如下:
redView touchesBegan // 此時,手勢識別器正在識別觸摸事件拓春,還沒有識別成功...
redView touchesMoved // 此時释簿,手勢識別器正在識別觸摸事件,還沒有識別成功...
redView touchesMoved // 此時硼莽,手勢識別器正在識別觸摸事件庶溶,還沒有識別成功...
redView touchesMoved // 此時,手勢識別器正在識別觸摸事件,還沒有識別成功...
redView panned // 此時偏螺,手勢識別器成功識別觸摸事件
redView touchesCancelled // 此時行疏,系統(tǒng)取消了redView對觸摸事件的響應(yīng)
redView panned
redView panned
...
從打印可以看出這次滑動觸發(fā)了redView
的touchesBegan
、touchesMoved
方法套像,然后觸發(fā)了一次平移手勢的方法酿联,緊接著觸發(fā)了一次redView
的touchesCancelled
方法,接下來就一直觸發(fā)平移手勢的方法夺巩,直至滑動結(jié)束我們也沒見到觸發(fā)redView
的touchesEnded
方法贞让。為什么redView
的touches
被cancel
掉了,而不能正常end
柳譬?官方文檔對此有如下解釋:
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, delaysTouchesEnded properties.
意思就是說:
window
在將觸摸事件傳遞給第一響應(yīng)者hitTested view
之前喳张,會優(yōu)先把觸摸事件傳遞給手勢識別器。如果手勢識別器成功識別了該觸摸事件美澳,那么手勢識別器就擁有了該觸摸事件的響應(yīng)權(quán)蹲姐,系統(tǒng)就會取消第一響應(yīng)者hitTested view
對觸摸事件的響應(yīng);如果手勢識別器沒能識別該觸摸事件人柿,那么第一響應(yīng)者hitTested view
才擁有該觸摸事件的響應(yīng)權(quán)柴墩。一言以蔽之,手勢識別器擁有比
UIResponder
更高的事件響應(yīng)優(yōu)先級凫岖。(注意如果自己身上沒有添加手勢江咳,那父視圖、爺視圖......身上的手勢也會比自己的原始指針事件響應(yīng)優(yōu)先級高哥放,因為UIEvent.UITouch.gestureRecognizers
里存儲的是響應(yīng)者鏈上所有響應(yīng)者的手勢歼指,不僅僅是自己身上的手勢)
UIGestureRecognizer的兩個屬性和一個代理方法
@property (nonatomic) BOOL cancelsTouchesInView;
@property (nonatomic) BOOL delaysTouchesBegan;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
- cancelsTouchesInView
默認(rèn)值為YES
,代表如果手勢識別器成功識別了觸摸事件甥雕,那么手勢識別器就擁有該觸摸事件的響應(yīng)權(quán)踩身,系統(tǒng)就取消第一響應(yīng)者hitTested view
對觸摸事件的響應(yīng)。
如果設(shè)置為NO
社露,代表就算手勢識別器成功識別了觸摸事件挟阻,系統(tǒng)也不取消第一響應(yīng)者hitTested view
對觸摸事件的響應(yīng),即手勢識別器和第一響應(yīng)者hitTested view
同時響應(yīng)觸摸事件峭弟。
上面例子中如果設(shè)置panGestureRecognizer.cancelsTouchesInView = NO;
附鸽,那么控制臺將會打印:
redView touchesBegan
redView touchesMoved
redView touchesMoved
redView touchesMoved
redView panned
redView touchesMoved
redView panned
redView touchesMoved
redView panned
redView touchesMoved
...
redView touchesEnded
- delaysTouchesBegan
默認(rèn)值為NO
瞒瘸,代表window
不僅會把觸摸事件傳遞給手勢識別器坷备,而且在手勢識別器識別事件期間還會把觸摸事件傳遞給第一響應(yīng)者hitTested view
。
如果設(shè)置為YES
情臭,代表window
只會把觸摸事件傳遞給手勢識別器省撑,不會傳遞給第一響應(yīng)者hitTested view
赌蔑,即只有手勢識別器響應(yīng)觸摸事件。
上面例子中如果設(shè)置panGestureRecognizer.delaysTouchesBegan = YES;
竟秫,那么控制臺將會打油薰摺:
redView panned
redView panned
redView panned
...
-
gestureRecognizer: shouldReceiveTouch:
(enabled
屬性也行)
默認(rèn)返回YES
,代表手勢識別器響應(yīng)觸摸事件鸿摇。
如果返回NO
石景,代表手勢識別器不響應(yīng)觸摸事件,即只有第一響應(yīng)者hitTested view
響應(yīng)觸摸事件拙吉。
上面例子中如果設(shè)置
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return NO;
}
那么控制臺將會打映蹦酢:
redView touchesBegan
redView touchesMoved
redView touchesMoved
redView touchesMoved
...
redView touchesEnded
八、Flutter里的手勢
這里我們也不專門說手勢筷黔、手勢競技往史,也主要說一下原始指針事件和手勢同時存在時會怎樣。
GestureDetector的優(yōu)先級
實際上椎例,F(xiàn)lutter里也不止Listener
才能傳遞傳遞和響應(yīng)事件,GestureDetector
也行请祖,不過話說回來GestureDetector
本質(zhì)上就是個Listener
订歪。常見的手勢也有點按手勢、輕掃手勢肆捕,平移手勢刷晋、旋轉(zhuǎn)手勢、縮放手勢慎陵、長按手勢眼虱。
還是iOS里舉過的例子:
界面上添加了一個redView
,redView
上添加了一個平移手勢席纽,并且redView
實現(xiàn)了Listener
的四個方法捏悬。
------main.dart------
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({
Key? key,
}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
padding: EdgeInsets.only(top: 20, left: 20),
alignment: Alignment.topLeft,
child: Listener(
onPointerDown: (_) {
print("redView onPointerDown");
},
onPointerMove: (_) {
print("redView onPointerMove");
},
onPointerUp: (_) {
print("redView onPointerUp");
},
onPointerCancel: (_) {
print("redView onPointerCancel");
},
child: GestureDetector(
onPanUpdate: (_) {
print("redView onPanUpdate");
},
child: Container(
color: Colors.red,
width: 300,
height: 300,
),
),
),
);
}
}
在redView
上執(zhí)行一次滑動,控制臺的打印如下:
flutter: redView onPointerDown
flutter: redView onPointerMove
flutter: redView onPanUpdate
flutter: redView onPointerMove
flutter: redView onPanUpdate
flutter: redView onPointerMove
flutter: redView onPanUpdate
flutter: redView onPointerUp
...
從打印可以看出原始指針事件和平移手勢會同時觸發(fā)润梯,并不像iOS里那樣手勢會具備更高的優(yōu)先級过牙。但是手勢的響應(yīng)總是在原始指針事件的后面,這是為什么仆救?
當(dāng)我們PointDown
到redView
上時抒和,會先執(zhí)行第一步尋找第一響應(yīng)者律适,最先觸發(fā)的是RenderBinding
的hitTest
亏镰,里面就是先做UI的hitTest
——即從renderView
開始遞歸調(diào)用hitTest
屁桑,把命中的子視圖都添加到響應(yīng)者數(shù)組里,這一步GestureDetector對應(yīng)的Listener
會被先放進(jìn)響應(yīng)者數(shù)組里顿痪,然后Listener
也會被先放進(jìn)響應(yīng)者數(shù)組里镊辕,此時響應(yīng)者數(shù)組里存放的就是[GestureDetector對應(yīng)的Listener、Listener]
蚁袭;然后才會做手勢的hitTest
——手勢的hitTest
比較簡單征懈,就是把GestureBinding
這個類本身添加到響應(yīng)者數(shù)組里,手勢相關(guān)的回調(diào)其實都放在GestureBinding
類里由這個類處理揩悄,此時響應(yīng)者數(shù)組里存放的就是[GestureDetector對應(yīng)的Listener卖哎、Listener、GestureBinding]
删性;到此響應(yīng)者數(shù)組就確定了亏娜,第一響應(yīng)者也就確定了——它就是GestureDetector對應(yīng)的Listener
,第二響應(yīng)者就是Listener
蹬挺,第三響應(yīng)者才是GestureBinding
所以手勢的響應(yīng)總是在原始指針事件的后面维贺。
------RenderBinding------
@override
void hitTest(HitTestResult result, Offset position) {
// UI的hitTest:從根節(jié)點開始進(jìn)行命中測試
renderView.hitTest(result, position: position);
// 手勢的hitTest:會調(diào)用GestureBinding中的hitTest方法
super.hitTest(result, position);
}
兩個攔截事件的Widget
如果我們只想讓原始指針事件和手勢中的一個響應(yīng)事件,那就換換它們的父子關(guān)系巴帮,給子視圖外面套一個IgnorePointer
或AbsorbPointer
就行了溯泣,它倆分別有一個bool
值屬性叫ignoring
、absorbing
用來決定是否攔截事件榕茧,我們可以根據(jù)實際情況來改變這倆屬性的值垃沦,其實這倆Widget攔截事件的本質(zhì)就是攔截響應(yīng)者不被添加進(jìn)響應(yīng)者數(shù)組里。
參考
1用押、史上最詳細(xì)的iOS之事件的傳遞和響應(yīng)機制-原理篇
2肢簿、史上最詳細(xì)的iOS之事件的傳遞和響應(yīng)機制-實踐篇
3、iOS 事件(UITouch只恨、UIControl译仗、UIGestureRecognizer)傳遞機制
4、iOS觸摸事件全家桶
5官觅、Flutter實戰(zhàn)電子書第八章:事件處理與通知
6纵菌、Flutter完整開發(fā)實戰(zhàn)詳解(十三、全面深入觸摸和滑動原理)
7休涤、Flutter中的事件流和手勢簡析
8咱圆、flutter的RenderBox使用&原理淺析