【跨平臺開發(fā)Flutter】iOS和Flutter里的事件處理

目錄

先說一下事件處理里的被處理者:事件
一、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哥捕、PointerCancelEventPointerUpEvent嘉熊,它們內(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)者毫炉。我們常見的UIApplicationUIViewController削罩、UIView都繼承自UIResponder瞄勾,所以它們都能傳遞和響應(yīng)事件费奸。注意這里的傳遞是指尋找第一響應(yīng)者階段父視圖通過hitTest方法把事件傳遞給子視圖以及事件傳遞階段UIApplicationwindow通過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添加到它的父視圖上后腾供,該viewnextResponder就已經(jīng)指向了它的父視圖仆邓;父視圖的nextResponder又會指向rootViewControllerviewrootViewControllerviewnextResponder又會指向rootViewController伴鳖;rootViewControllernextResponder又會指向window节值;windownextResponder又會指向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
————————MultiChildRenderObjectWidget

Widget可以分為兩類:組件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又是個抽象類,真正渲染在屏幕上的東西其實又是它的子類:單個子對象的時候钝凶,就用RenderProxyBoxRenderShiftedBox仪芒,它倆的主要區(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í)行完环壤,就找到了觸摸事件所在的viewUIEvent.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)地傳遞給觸摸事件所在的windowwindow又會通過sendEvent方法把觸摸事件精準(zhǔn)地傳遞給觸摸事件所在的view——即第一響應(yīng)者焰络。

還是上面的例子戴甩,我們在GreenViewtouchesBegan方法里打個斷點,觸摸一下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ā)生后,就從根視圖RenderViewhitTest方法開始,倒序遞歸調(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)先級

UIGestureRecognizer不在響應(yīng)者鏈里灾而,更不是UIResponder的子類

實際上,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ā)了redViewtouchesBegantouchesMoved方法套像,然后觸發(fā)了一次平移手勢的方法酿联,緊接著觸發(fā)了一次redViewtouchesCancelled方法,接下來就一直觸發(fā)平移手勢的方法夺巩,直至滑動結(jié)束我們也沒見到觸發(fā)redViewtouchesEnded方法贞让。為什么redViewtouchescancel掉了,而不能正常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)先級

GestureDetector在響應(yīng)者數(shù)組里佛舱,因為它就是個`Listener`

實際上椎例,F(xiàn)lutter里也不止Listener才能傳遞傳遞和響應(yīng)事件,GestureDetector也行请祖,不過話說回來GestureDetector本質(zhì)上就是個Listener订歪。常見的手勢也有點按手勢、輕掃手勢肆捕,平移手勢刷晋、旋轉(zhuǎn)手勢、縮放手勢慎陵、長按手勢眼虱。

還是iOS里舉過的例子:

界面上添加了一個redViewredView上添加了一個平移手勢席纽,并且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)我們PointDownredView上時抒和,會先執(zhí)行第一步尋找第一響應(yīng)者律适,最先觸發(fā)的是RenderBindinghitTest亏镰,里面就是先做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)系巴帮,給子視圖外面套一個IgnorePointerAbsorbPointer就行了溯泣,它倆分別有一個bool值屬性叫ignoringabsorbing用來決定是否攔截事件榕茧,我們可以根據(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使用&原理淺析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市功氨,隨后出現(xiàn)的幾起案子序苏,更是在濱河造成了極大的恐慌,老刑警劉巖捷凄,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忱详,死亡現(xiàn)場離奇詭異,居然都是意外死亡跺涤,警方通過查閱死者的電腦和手機匈睁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進(jìn)店門监透,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人航唆,你說我怎么就攤上這事胀蛮。” “怎么了糯钙?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵粪狼,是天一觀的道長。 經(jīng)常有香客問我任岸,道長再榄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任演闭,我火速辦了婚禮不跟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘米碰。我一直安慰自己窝革,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布吕座。 她就那樣靜靜地躺著虐译,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吴趴。 梳的紋絲不亂的頭發(fā)上漆诽,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天,我揣著相機與錄音锣枝,去河邊找鬼厢拭。 笑死,一個胖子當(dāng)著我的面吹牛撇叁,可吹牛的內(nèi)容都是我干的供鸠。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼陨闹,長吁一口氣:“原來是場噩夢啊……” “哼楞捂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起趋厉,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤寨闹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后君账,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體繁堡,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了帖蔓。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片矮瘟。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡瞳脓,死狀恐怖塑娇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情劫侧,我是刑警寧澤埋酬,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站烧栋,受9級特大地震影響写妥,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜审姓,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一珍特、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧魔吐,春花似錦扎筒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至辞色,卻和暖如春骨宠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背相满。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工层亿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人立美。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓匿又,卻偏偏與公主長得像,于是被迫代替她去往敵國和親悯辙。 傳聞我的和親對象是個殘疾皇子琳省,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,509評論 2 348

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