深入理解Flutter的GestureDetector元件

AndroidHint發表於2020-03-15

引言

上一篇文章 深入理解Flutter的Listener元件 介紹了觸控事件的監聽原理,讓我們對Flutter中觸控事件的傳遞過程有了進一步的認識。

今天我們學習一下手勢識別元件GestureDetector的原理。GestureDetector的內部實現使用的是Listener元件,如果對Listener還不太熟悉,可以先了解一下Listener的原理。

原始碼解析

一、GestureDetector是Listener的封裝

GestureDector是一個無狀態元件,它的build方法如下所示。

class GestureDetector extends StatelessWidget {
    ...省略
    
    Widget build(BuildContext context) {
        ...省略
        return RawGestureDetector(
          gestures: gestures,
          behavior: behavior,
          excludeFromSemantics: excludeFromSemantics,
          child: child,
        );
    }
}
複製程式碼

build方法直接返回了RawGestureDetector元件,說明GestureDetector是由子元件RawGestureDetector構成的。而RawGestureDetector是一個有狀態元件,它的Statebuild方法如下所示。

class RawGestureDetector extends StatefulWidget {
  @override
  RawGestureDetectorState createState() => RawGestureDetectorState();
}

class RawGestureDetectorState extends State<RawGestureDetector> {
    ...省略
    
    @override
    Widget build(BuildContext context) {
        Widget result = Listener(
          onPointerDown: _handlePointerDown,
          behavior: widget.behavior ?? _defaultBehavior,
          child: widget.child,
        );
        if (!widget.excludeFromSemantics)
          result = _GestureSemantics(owner: this, child: result);
        return result;
    }
}
複製程式碼

build方法裡面返回了Listener元件,這也證明了上面的結論:

GestureDetector的內部實現使用的是Listener元件。

二、GestureDetector的實現原理

相比於ListenerGestureDetector有自己的屬性,如onDoubleTaponLongPressonHorizontalDragStartonVerticalDragStart等。

其實說到底,這些屬性也是由ListeneronPointerDownonPointerMoveonPointerUp這三個屬性封裝而成的。

重新看一下RawGestureDetectorStatebuild方法。

@override
Widget build(BuildContext context) {
    Widget result = Listener(
      onPointerDown: _handlePointerDown,
      behavior: widget.behavior ?? _defaultBehavior,
      child: widget.child,
    );
    if (!widget.excludeFromSemantics)
      result = _GestureSemantics(owner: this, child: result);
    return result;
}
複製程式碼

Listener元件的child屬性是由GestureDector傳遞進來的,也就是說GestureDector自上而下的Widget構成如下圖所示。

深入理解Flutter的GestureDetector元件

從之前對Listener元件的分析可知,Listener中的Child的觸控事件由onPointerDownonPointerMoveonPointerUp等屬性值決定。

這裡Listener屬性值為_handlePointerDown,它是一個方法。

void _handlePointerDown(PointerDownEvent event) {
    assert(_recognizers != null);
    for (GestureRecognizer recognizer in _recognizers.values)
      recognizer.addPointer(event);
}
複製程式碼

該方法遍歷了_recognizers裡面的值(值型別為GestureRecognizer),_recognizers又是在_syncAll方法中賦值的。

void _syncAll(Map<Type, GestureRecognizerFactory> gestures) {
    final Map<Type, GestureRecognizer> oldRecognizers = _recognizers;
    _recognizers = <Type, GestureRecognizer>{};
    for (Type type in gestures.keys) {
      _recognizers[type] = oldRecognizers[type] ?? gestures[type].constructor(); //重要方法
      gestures[type].initializer(_recognizers[type]); //重要方法
    }
    for (Type type in oldRecognizers.keys) {
      if (!_recognizers.containsKey(type))
        oldRecognizers[type].dispose();
    }
}
複製程式碼

_syncAll方法會將原有的_recognizers儲存下來,然後遍歷引數中的gestures,若原有的_recognizers有該手勢型別物件,則使用,否則呼叫gestures[type]constructor方法。然後繼續呼叫gestures[type]initializer方法。記住constructorinitializer這兩個方法,後面的分析需要用到。

_syncAll方法在兩處地方被呼叫,分別是initStatedidUpdateWidget方法。

@override
void initState() {
    super.initState();
    _syncAll(widget.gestures);
}

@override
void didUpdateWidget(RawGestureDetector oldWidget) {
    super.didUpdateWidget(oldWidget);
    _syncAll(widget.gestures);
}
複製程式碼

元件初始化會呼叫initState方法,並傳遞了widgetgestures屬性,這裡的widget是指RawGestureDetector元件。

讓我們再回過頭來看GestureDectorbuild方法,如下所示。

@override
Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

    if (
      onTapDown != null ||
      onTapUp != null ||
      onTap != null ||
      onTapCancel != null ||
      onSecondaryTapDown != null ||
      onSecondaryTapUp != null ||
      onSecondaryTapCancel != null
    ) {
      gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
        () => TapGestureRecognizer(debugOwner: this),
        (TapGestureRecognizer instance) {
          instance
            ..onTapDown = onTapDown
            ..onTapUp = onTapUp
            ..onTap = onTap
            ..onTapCancel = onTapCancel
            ..onSecondaryTapDown = onSecondaryTapDown
            ..onSecondaryTapUp = onSecondaryTapUp
            ..onSecondaryTapCancel = onSecondaryTapCancel;
        },
      );
    }

    if (onDoubleTap != null) {
      gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
        () => DoubleTapGestureRecognizer(debugOwner: this),
        (DoubleTapGestureRecognizer instance) {
          instance
            ..onDoubleTap = onDoubleTap;
        },
      );
    }
    if (onLongPress != null ||
        onLongPressUp != null ||
        onLongPressStart != null ||
        onLongPressMoveUpdate != null ||
        onLongPressEnd != null) {
      gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
        () => LongPressGestureRecognizer(debugOwner: this),
        (LongPressGestureRecognizer instance) {
          instance
            ..onLongPress = onLongPress
            ..onLongPressStart = onLongPressStart
            ..onLongPressMoveUpdate = onLongPressMoveUpdate
            ..onLongPressEnd =onLongPressEnd
            ..onLongPressUp = onLongPressUp;
        },
      );
    }
    ...省略
    return RawGestureDetector(
      gestures: gestures,
      behavior: behavior,
      excludeFromSemantics: excludeFromSemantics,
      child: child,
    );
}
複製程式碼

首先初始化了gestures,並且對於每一種手勢族都定義了一種型別。

1、TapGestureRecognizer手勢族裡面就包含了onTapDownonTapUponTaponTapCancelonSecondaryTapDownonSecondaryTapUponSecondaryTapCancel事件。

2、DoubleTapGestureRecognizer手勢族裡面就包含了onDoubleTap事件。

3、LongPressGestureRecognizer手勢族裡面就包含了onLongPressonLongPressStartonLongPressMoveUpdateonLongPressEndonLongPressUp事件。

深入理解Flutter的GestureDetector元件

gestures中的每一個值都是GestureRecognizerFactory型別。通過GestureRecognizerFactoryWithHandlers的構造方法,分別給GestureRecognizerFactoryconstructor、initializer方法進行初始化。

還記得RawGestureDetector元件的_syncAll中提到的constructor、initializer方法麼?所以結合起來看,我們得出瞭如下結論:

GestureDetector的多種手勢屬性,都有其所屬的手勢族(GestureRecognizerFactory物件)。這些屬性會通過手勢族的initializer方法儲存起來。

三、舉個例子

GestureDetector(
    child: ConstrainedBox(
      constraints: BoxConstraints.tight(Size(300, 150)),
      child: Container(
        color: Colors.blue,
        child: Center(
          child: Text('click me'),
        ),
      ),
    ),
    onTapDown: (TapDownDetails details) {
      print("onTap down");
    },
    onTapUp: (TapUpDetails details) {
      print("onTap up");
    },
),
複製程式碼

執行上面的程式碼後,展示如下。

深入理解Flutter的GestureDetector元件
GestureDector本質上也是Listener,所以當我們點選了click me文案後,需要執行命中測試,命中測試列表如下所示:RenderParagraph->RenderPositionedBox->RenderDecoratedBox->RenderConstrainedBox->RenderPointerListener

根據命中測試列表,從上而下執行每一個物件的handleEvent方法。Listener對應的RenderObject就是RenderPointerListener,而RenderPointerListenerhandleEvent方法如下。

@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
    assert(debugHandleEvent(event, entry));
    if (onPointerDown != null && event is PointerDownEvent)
      return onPointerDown(event);
    if (onPointerMove != null && event is PointerMoveEvent)
      return onPointerMove(event);
    if (onPointerUp != null && event is PointerUpEvent)
      return onPointerUp(event);
    if (onPointerCancel != null && event is PointerCancelEvent)
      return onPointerCancel(event);
    if (onPointerSignal != null && event is PointerSignalEvent)
      return onPointerSignal(event);
 }
複製程式碼

這裡的onPointerDownonPointerMoveonPointerUponPointerCancelonPointerSignal屬性和Listener中是一一對應的。

當點選click me文案時,由於onPointerDown!=null && event is PointerDownEvent為true,從而執行了Listener中的onPointerDown方法,也就是RawGestureDetector元件的_handlePointerDown方法。

void _handlePointerDown(PointerDownEvent event) {
    assert(_recognizers != null);
    for (GestureRecognizer recognizer in _recognizers.values)
      recognizer.addPointer(event);
}
複製程式碼

_recognizers.values值遍歷的結果我們上面分析過了,這裡遍歷的結果是每次都會去執行GestureRecognizer物件的addPointer方法。

void addPointer(PointerDownEvent event) {
    _pointerToKind[event.pointer] = event.kind;
    if (isPointerAllowed(event)) {
      addAllowedPointer(event);
    } else {
      handleNonAllowedPointer(event);
    }
}
複製程式碼

首先通過了isPointerAllowed方法判斷PointerDownEvent手勢事件是否被GestureRecognizer物件所接受,一般每一個GestureRecognizer物件都會重寫isPointerAllowed方法。

對於上面的例子,這裡的GestureRecognizer物件就是TapGestureRecognizer,它的addAllowedPointer方法如下所示。

@override
void addAllowedPointer(PointerDownEvent event) {
    super.addAllowedPointer(event);
    _initialButtons = event.buttons;
}
複製程式碼

這裡直接呼叫了父類的addAllowedPointer方法。

 @override
 void addAllowedPointer(PointerDownEvent event) {
    startTrackingPointer(event.pointer, event.transform);
    if (state == GestureRecognizerState.ready) {
      state = GestureRecognizerState.possible;
      primaryPointer = event.pointer;
      initialPosition = OffsetPair(local: event.localPosition, global: event.position);
      if (deadline != null)
        _timer = Timer(deadline, () => didExceedDeadlineWithEvent(event));
    }
}
複製程式碼

然後是startTrackingPointer方法。

@protected
void startTrackingPointer(int pointer, [Matrix4 transform]) {
    GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
    _trackedPointers.add(pointer);
    assert(!_entries.containsValue(pointer));
    _entries[pointer] = _addPointerToArena(pointer);
}
複製程式碼

1、第一步通過GestureBinding.instance.pointerRouter呼叫了addRoute方法,引數為PointerDownEvent事件的唯一值(pointer)、handleEvent物件(由GestureRecognizer的子類實現)、PointerDownEvent事件座標系(transform)。

注意:這裡的GestureBinding.instance返回的是GestureBinding的物件,它是一個單例類,作用是管理手勢事件生命週期與手勢衝突。

void addRoute(int pointer, PointerRoute route, [Matrix4 transform]) {
    final LinkedHashSet<_RouteEntry> routes = _routeMap.putIfAbsent(pointer, () => LinkedHashSet<_RouteEntry>());
    assert(!routes.any(_RouteEntry.isRoutePredicate(route)));
    routes.add(_RouteEntry(route: route, transform: transform));
}
複製程式碼

addRoute方法將在_routeMap中尋找pointer對應的LinkedHashSet,不存在則新建一個,然後建立一個_RouteEntry物件,並將routetransform傳遞過去。

2、第二步呼叫了_addPointerToArena方法。

GestureArenaEntry _addPointerToArena(int pointer) {
    if (_team != null)
      return _team.add(pointer, this);
    return GestureBinding.instance.gestureArena.add(pointer, this);
}
複製程式碼

_addPointerToArena方法中,也通過GestureBinding.instance.gestureArena呼叫了add方法,引數為PointerDownEvent事件的唯一值(pointer)、GestureRecognizer物件(具體子類)。

還記得我們點選click me文案時,上面提到的命中測試列表麼?其實上面只是列出了一部分,在RenderPointerListener的最後還有WidgetsFlutterBinding。所以應該是這樣的: RenderParagraph->RenderPositionedBox->RenderDecoratedBox->RenderConstrainedBox->RenderPointerListener->...->WidgetsFlutterBinding

所以在命中測試列表最後一步,執行的是WidgetsFlutterBindinghandleEvent方法,這一步很重要,我們來看一下。

@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
    pointerRouter.route(event);
    if (event is PointerDownEvent) {
      gestureArena.close(event.pointer);
    } else if (event is PointerUpEvent) {
      gestureArena.sweep(event.pointer);
    } else if (event is PointerSignalEvent) {
      pointerSignalResolver.resolve(event);
    }
}
複製程式碼

方法中引數event是一個PointerEvent物件,由PointerDownEvent、一系列PointerMoveEventPointerUpEvent事件組成,對於每一個PointerEvent事件,都會執行pointerRouterroute方法。這裡的pointerRouter物件就是GestureBinding.instance.pointerRouter物件。

void route(PointerEvent event) {
    final LinkedHashSet<_RouteEntry> routes = _routeMap[event.pointer];
    final List<_RouteEntry> globalRoutes = List<_RouteEntry>.from(_globalRoutes);
    if (routes != null) {
      for (_RouteEntry entry in List<_RouteEntry>.from(routes)) {
        if (routes.any(_RouteEntry.isRoutePredicate(entry.route)))
          _dispatch(event, entry);
      }
    }
    for (_RouteEntry entry in globalRoutes) {
      if (_globalRoutes.any(_RouteEntry.isRoutePredicate(entry.route)))
        _dispatch(event, entry);
    }
}
複製程式碼

route方法會從_routeMap中取出該觸控事件,並執行_dispatch將該事件分發下去。這裡_routeMap對應的資料,在上面的startTrackingPointer方法中已分析過。

void _dispatch(PointerEvent event, _RouteEntry entry) {
    try {
      event = event.transformed(entry.transform);
      entry.route(event);
    } catch (exception, stack) {
      ...省略
    }
}
複製程式碼

event.transfromed方法會對當前觸控事件物件進行座標系轉換。一般來說,非特殊情況下,這裡轉換前和轉換後是同一個觸控事件物件。然後呼叫了_RouteEntryroute方法,將該觸控事件物件傳遞過去。

這裡_RouteEntryroute方法就是上面的startTrackingPointer方法中初始化的,並且它指向的是每一個GestureRecognizer子類的handleEvent方法。

拿上面的例子來說,就是TapGestureRecognizerhandleEvent方法,由於TapGestureRecognizer沒有該方法,我們從它父類PrimaryPointerGestureRecognizer可以找到。

@override
  void handleEvent(PointerEvent event) {
    assert(state != GestureRecognizerState.ready);
    if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
      final bool isPreAcceptSlopPastTolerance =
          !_gestureAccepted &&
          preAcceptSlopTolerance != null &&
          _getGlobalDistance(event) > preAcceptSlopTolerance;
      final bool isPostAcceptSlopPastTolerance =
          _gestureAccepted &&
          postAcceptSlopTolerance != null &&
          _getGlobalDistance(event) > postAcceptSlopTolerance;

      if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
        resolve(GestureDisposition.rejected);
        stopTrackingPointer(primaryPointer);
      } else {
        handlePrimaryPointer(event);
      }
    }
    stopTrackingIfPointerNoLongerDown(event);
}
複製程式碼

這裡最重要的方法是執行了resolve方法。

void resolve(GestureDisposition disposition) {
    final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values);
    _entries.clear();
    for (GestureArenaEntry entry in localEntries)
      entry.resolve(disposition);
}
複製程式碼

這裡的_entries也是在上面的startTrackingPointer方法分析過的,所以上述方法會遍歷_entries的每一個GestureArenaEntry物件(對應著每一個GestureRecognizer物件),並執行它的resolved方法,然後再呼叫GestureArenaManager_resolve方法。

void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; // This arena has already resolved.
    assert(_debugLogDiagnostic(pointer, '${ disposition == GestureDisposition.accepted ? "Accepting" : "Rejecting" }: $member'));
    assert(state.members.contains(member));
    if (disposition == GestureDisposition.rejected) {
      state.members.remove(member);
      member.rejectGesture(pointer);
      if (!state.isOpen)
        _tryToResolveArena(pointer, state);
    } else {
      assert(disposition == GestureDisposition.accepted);
      if (state.isOpen) {
        state.eagerWinner ??= member;
      } else {
        assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
        _resolveInFavorOf(pointer, state, member);
      }
    }
}
複製程式碼

該方法主要的作用是處理手勢衝突,通過手勢衝突處理後,能成功執行的手勢事件會呼叫_resolveInFavorOf方法。

void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
    assert(state == _arenas[pointer]);
    assert(state != null);
    assert(state.eagerWinner == null || state.eagerWinner == member);
    assert(!state.isOpen);
    _arenas.remove(pointer);
    for (GestureArenaMember rejectedMember in state.members) {
      if (rejectedMember != member)
        rejectedMember.rejectGesture(pointer);
    }
    member.acceptGesture(pointer);
}
複製程式碼

然後再執行GestureArenaMemberacceptGesture方法。該方法是抽象方法,具體的實現是在其子類中。我們看一下TapGestureRecognizer的實現。

@override
void acceptGesture(int pointer) {
    super.acceptGesture(pointer);
    if (pointer == primaryPointer) {
      _checkDown(pointer);
      _wonArenaForPrimaryPointer = true;
      _checkUp();
    }
}
複製程式碼

這裡_checkDown方法主要處理按下事件,_checkUp主要處理抬起事件。這也說明了TapGestureRecognizer手勢族只處理手勢的按下與抬起。其他事件由其他手勢族進行處理。

_checkDown_checkUp方法後面還會呼叫諸多方法,最終會呼叫onTapDownonTapUp方法,這裡的方法鏈路就不再分析了,有興趣的同學可以去看看原始碼。

總結

本文以TapGestureRecognizer作為例子,分析了GestureDector元件的觸控事件的原理。GestureDector元件的底層是通過Listener實現的,並且與Listener一樣也需要對觸控事件進行命中測試。GestureDector元件的各個屬性方法在得到響應之前,會通過WidgetsFlutterBinding做事件分發,並通過手勢衝突競技場做手勢衝突管理,最終通過的手勢事件才會分發到各個GestureRecognizer物件的handleEvent方法進行處理,結果才會是各個GestureRecognizer物件的屬性方法得到響應。

相關文章