Flutter event distribution process

The WidgetsFlutterBinding in Flutter is integrated GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding, etc Binding, both of which have their own functions. GestureBinding is mainly responsible for event distribution and gesture detection.

In the process of the generation and utilization of a flutter, there are three roles of the native, the engine and the flutter. The native is the producer (in the native system, the native belongs to the consumer, but in the flutter system, it can be regarded as the producer. Because Flutter considers its native system to be the native system, but for native systems such as Android, its native is the Linux kernel), the engine deliverer, and flutter is the consumer.

Native layer

Android has its own event distribution mechanism. The whole process of Flutter is dispatch- Intercept -onTouch. Flutter is mediated by flutterView in Android. By the same token, events acquired by FLUTTER are actually events received by flutterView in the Android system and then transmitted to Flutter. On Android, touch events are generally received through the onTouchEvent method, while other events, such as sticks, mice, and wheels, can be received through onGenericMotionEvent. Both take MotionEvent and hand it to the AndroidTouchProcessor. The two events are handled in the same way: They store the MotionEvent in ByteBuffer and hand it to engine. This is further transmitted to flutter.

For example, onTouchEvent handling:

public boolean onTouchEvent(@NonNull MotionEvent event) {
  int pointerCount = event.getPointerCount();
  // Prepare a data packet of the appropriate size and order.
  ByteBuffer packet =
      ByteBuffer.allocateDirect(pointerCount * POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD);
  packet.order(ByteOrder.LITTLE_ENDIAN);
  int maskedAction = event.getActionMasked();
  int pointerChange = getPointerChangeForAction(event.getActionMasked());
  boolean updateForSinglePointer =
      maskedAction == MotionEvent.ACTION_DOWN || maskedAction == MotionEvent.ACTION_POINTER_DOWN;
  booleanupdateForMultiplePointers = ! updateForSinglePointer && (maskedAction == MotionEvent.ACTION_UP || maskedAction == MotionEvent.ACTION_POINTER_UP);if (updateForSinglePointer) {
    // ACTION_DOWN and ACTION_POINTER_DOWN always apply to a single pointer only.
    addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, packet);
  } else if (updateForMultiplePointers) {
    // ACTION_UP and ACTION_POINTER_UP may contain position updates for other pointers.
    // We are converting these updates to move events here in order to preserve this data.
    // We also mark these events with a flag in order to help the framework reassemble
    // the original Android event later, should it need to forward it to a PlatformView.
    for (int p = 0; p < pointerCount; p++) {
      if (p != event.getActionIndex() && event.getToolType(p) == MotionEvent.TOOL_TYPE_FINGER) {
        addPointerForIndex(event, p, PointerChange.MOVE, POINTER_DATA_FLAG_BATCHED, packet);
      }
    }
    // It's important that we're sending the UP event last. This allows PlatformView
    // to correctly batch everything back into the original Android event if needed.
    addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, packet);
  } else {
    // ACTION_MOVE may not actually mean all pointers have moved
    // but it's the responsibility of a later part of the system to
    // ignore 0-deltas if desired.
    for (int p = 0; p < pointerCount; p++) {
      addPointerForIndex(event, p, pointerChange, 0, packet); }}// Verify that the packet is the expected size.
  if(packet.position() % (POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD) ! =0) {
    throw new AssertionError("Packet position is not on field boundary");
  }
  // Send the packet to flutter.
  renderer.dispatchPointerDataPacket(packet, packet.position());
  return true;
}
Copy the code

As above, a MotionEvent may include multiple touch points (multi-finger touch). Here, the data of each touch point need to be separated and loaded into packet one by one. By calling addPointerForIndex method, finally, Call the renderer. DispatchPointerDataPacket began to send pocket to flutter layers, after several relay, eventually call flutterJNI# nativeDispatchPointerDataPacket, Enter the Engine layer.

Engine layer

The engine layer is responsible for passing events to the FLUTTER layer. On the one hand, the event is fetched from Android via JNI. On the other hand, the event is passed to the Flutter layer through window calls to dart functions, which also involves a data conversion.

NativeDispatchPointerDataPacket corresponding DispatchPointerDataPacket function of engine, and then transferred to PlatformView here, PointerDataPacketConverter transformed on the pocket, pocket is coming in the android data, it is a ByteBuffer, multiple events are put together, PointerDataPacketConverter here on the split, convert PointerDataPacket, through data storing PointerData PointerDataPacket interior, which is every single event. After this layer of conversion, the number of PointerData may not be the same as the one passed in, because the PointerData will be processed again after being fetched from ByteBuffer, The ConvertPointerData function further processes PointerData data, where PointerData may be discarded or new PointerData may be derived. For example, when an event is transmitted from the native layer, but the event appears out of thin air and cannot meet the conditions of a complete series of events, it will be discarded. If two consecutive events can be regarded as the same event (unchanged type, position, etc.), they will also be discarded. In addition, if two consecutive events of the same series of events are transmitted from the native layer, but these two events do not constitute continuous events according to Flutter, a composite event will be created and interspersed between the two events to ensure their continuity. For example, this event is an Up event. PointerDataPacketConverter will compare the last sound of the series of events, if the location of the last time the event is different, so will be in the Up before you insert a Move events, will be moved to the current position, then add this Up event, so then you can make it more straight, It will also be easier to use with flutter. To sum up, the purpose of this flutter conversion is to split the event from ByteBuffer and to perform some content conversion to ensure that it is reasonable and can be handled more easily by the flutter layer.

The PointerDataPacket is passed to the Shell, Engine, PointerDataDispatcher, RuntimeController, Window, etc. Including PointerDataDispatcher to have the event planning of distribution, such as its subclasses SmoothPointerDataDispatcher delay can distribute events.

Finally, through the Window DartInvokeField function, called _dispatchPointerDataPacket function of the dart, transfer events to flutter.

@pragma('vm:entry-point')
// ignore: unused_element
void _dispatchPointerDataPacket(ByteData packet) {
  if (window.onPointerDataPacket ! =null)
    _invoke1<PointerDataPacket>(window.onPointerDataPacket, window._onPointerDataPacketZone, _unpackPointerDataPacket(packet));
}
Copy the code

This is where the window’s onPointerDataPacket function is called, which is the _handlePointerDataPacket passed to the window when GestureBinding is initialized, and event distribution enters the flutter layer. Data conversion is also involved. When the engine layer transfers event data to the flutter layer, it does not directly transfer the object. Instead, it transfers the event data to the buffer first. You also need to call _unpackPointerDataPacket to transfer the buffer data back to the PointerDataPacket object.

This means that the event performed five transitions from Android to FLUTTER:

  1. In Android, the event is taken from MotionEvent and stored in a ByteBuffer
  2. Engine converts ByteBuffer to PointerDataPacket
  3. Engine converts the PointerDataPacket into a buffer to be passed to dart
  4. In DART, turn buffer into a PointerDataPacket
  5. Dart transforms PointerData into PointerEvents for upper-layer use

Flutter layers

A WidgetsflutterBinding is initialized from the start runApp function of the flutter app. One of the mixins of the WidgetsflutterBinding is a GestureBinding. Gesture-related capabilities are implemented, including retrieving event information from the native layer and then passing events down from the root of the widget tree.

In GestureBinding, there are two ways to pass events, one is HitTest, the other is route, the former is the normal process, after the GestureBinding gets the event, the render tree starts from the root and passes it down. In route mode, a node adds a route to the pointerRoute in GestureBinding, so that the GestureBinding receives the event and sends it directly to the corresponding node through the route. Compared with the former method, it is more direct and flexible.

_handlePointerDataPacket

The GestureBinding function that receives event information is _handlePointerDataPacket. It takes PointerDataPacket and contains a series of event PointerData. Then through PointerEventConverter. Expand, which is used to convert it to flutter PointerEvent stored in _pendingPointerEvents, The _flushPointerEventQueue is then called to process the event.

In PointerEventConverter. Expand transformation event sometimes used to sync *, the usage is commonly used to delay processing loop, when the Iterable traversed cycle will perform, But here in the calling PointerEventConverter. Expand immediately after call _pendingPointerEvents. AddAll, that is to say immediately to traverse the Iterable, The point of using sync* here is less clear.

_flushPointerEventQueue

The _flushPointerEventQueue is a loop that continually pulls events from the _pendingPointerEvents queue and hands them to the _handlePointerEvent queue.

void _flushPointerEventQueue() { assert(! locked);while (_pendingPointerEvents.isNotEmpty)
    _handlePointerEvent(_pendingPointerEvents.removeFirst());
}
Copy the code

_handlePointerEvent

HitTestResult is created in _handlePointerEvent. When you first see the name, you might think this class is for testing, but it actually plays an important role throughout event distribution. First, HitTestResult can represent a series of events that are created and added to _hitTests when the PointerDownEvent comes, And be removed when PointerUpEvent/arrival PointerCancelEvent _hitTests, in the middle of a series of events, you can through the _hitTests/event. The pointer to get to the corresponding HitTestResult. The HitTestResult distribution object is determined by the execution of the hitTest function. The RendererBinding hitTest function is used as the entry point to call the RenderView hitTest function. RenderView can be thought of as an entry point to the Render tree. It calls Child-hittest to get HitTestResult passed in the render tree, and then recurses via hitTest/hitTestChildren. Find the RenderObject consuming the event, save the path from the root to the event, and then distribute the series of events. RenderObject subclasses can determine if they need to consume the current event by overwriting hitTest/hitTestChildren.

If a node (or its children) needs to consume events, it calls hittestResult. add to add itself to the path of HitTestResult, stored in the _path of HitTestResult, and distributed according to that path later. For example, in the GestureBinding hitTest,

void hitTest(HitTestResult result, Offset position) {
  result.add(HitTestEntry(this));
}
Copy the code

Will add itself to the path of HitTestResult. In theory, GestureBinding should not handle any events. It is added to HitTestResult for the following handleEvent callback, This function is the callback function for each node in the HitTestResult distribution process:

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 isPointerSignalEvent) { pointerSignalResolver.resolve(event); }}Copy the code

For example, if events are routed through the PointerRouter, press the table first.

After hitTest is executed, the next step is to call dispatchEvent to dispatch the event.

dispatchEvent

There are two types of event distribution: one is the distribution path determined by HitTestResult, and the other is when HitTestResult is null. Or the upper layer can directly conduct gesture detection through GestureDecter, etc.), which requires direct routing to the corresponding node.

In HitTestResult mode, dispatchEvent will call the handleEvent of each node in the HitTestResult save path to handle the event, that is, the event distribution path determined in the hitTest stage. Starting with GestureBinding, call their handleEvent function.

In route mode, GestureBinding calls the pointerRouter.route function to distribute events to nodes stored in _routeMap. Recipients can be added or deleted by using addRoute and removeRoute. There are two types of recipients: route stored in _routeMap and GlobalRoutes stored in _globalRoutes. The former is bound to pointer, which responds to all events:

/// Calls the routes registered for this pointer event.
///
/// Routes are called in the order in which they were added to the
/// PointerRouter object.
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); }}static _RouteEntryPredicate isRoutePredicate(PointerRoute route) {
  return (_RouteEntry entry) => entry.route == route;
}
Copy the code

There is a little strange, but it is the if (routes. Any (_RouteEntry. IsRoutePredicate (entry. The route))) this way, if stubbornly interpretation, This sentence is to determine whether the route of an entry is the same as that of an entry in routes, but the entry itself is obtained by traversing routes, so the entry must be in routes. This condition should be constant. But if I write it this way, I really can’t figure out what it means.

Typedef PointerRoute = void Function(PointerEvent event); Route is a function that receives a PointerEvent.

There is also a problem here. According to the logic above, dispatchEvent has two ways of distributing events, route and HitTestResut. PointerRouter. Route is also executed here, so from some point of view route distribution is always executed (either in dispatchEvent or handleEvent), Instead, the HitTestResult procedure is only executed when HitTestResult is not empty, so, If you delete pointerRouter.route from handleEvent and then remove the if judgment from route mode of dispatchEvent, can the same effect be achieved?

HitTest

The HitTest distribution process can be divided into two parts. The first part is the HitTest process, which determines the path of the event receiver. This process is executed only when the PointerDownEvent and PointerSignalEvent events occur. It will only be executed once, and all subsequent events will find the HitTestResult from the first event via pointer. If there is no HitTestResult, distribution will not be performed (ignoring the route process for now). The second part is the dispatchEvent, which calls the handleEvent function of all nodes in the HitTestResult path. This process is performed for each event (with the corresponding HitTestResult). From the perspective of HitTestResult alone, the first process registers the event receiver and the second process distributes the event to the receiver, so its basic process is the same as route, but in different dimensions, the former relies on Widgets tree structure, There is an inclusion relationship between its receivers, which is a normal pass-through – consumption process of events. Is more casual than the route process, it can be directly through the GestureBinding. Instance. PointerRouter. AddRoute registered a series of events the recipient, without the need for a transmission process, no limits between nodes, more suitable for the monitoring operations such as gestures.

In the HitTest process, starting with the HitTest for GestureBinding, add the GestureBinding to the HitTestResult path. This means that all HitTest processes call the GestureBinding handleEvent function first. RendererBinding then calls the hitTest of RenderView, which is a subclass of RenderObject and the entry point to the Render tree. RenderObject implements HitTestTarget, but the implementation of hitTest is in RenderBox, which can be regarded as the base class of the Render node. It implements the hitTest and hitTestChildren functions:

bool hitTest(BoxHitTestResult result, { @required Offset position }) {
  if (_size.contains(position)) {
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true; }}return false;
}
Copy the code

If the position of the event is on itself, then call hitTestChildren and hitTestSelf to determine whether the children or itself consume the event and decide whether to add itself to the HitTestResult path. The order in the HitTestResult path is from child to root, and finally to GestureBinding. For example, the default implementation of hitTestChildren:

bool defaultHitTestChildren(BoxHitTestResult result, { Offset position }) {
  // the x, y parameters have the top left of the node's box as the origin
  ChildType child = lastChild;
  while(child ! =null) {
    final ParentDataType childParentData = child.parentData;
    final bool isHit = result.addWithPaintOffset(
      offset: childParentData.offset,
      position: position,
      hitTest: (BoxHitTestResult result, Offset transformed) {
        assert(transformed == position - childParentData.offset);
        returnchild.hitTest(result, position: transformed); });if (isHit)
      return true;
    child = childParentData.previousSibling;
  }
  return false;
}
Copy the code

Starting with lastChild, it returns true when it finds a node that consumes an event, and only one child responds.

The final landing location of HitTest is the handleEvent function. HandleEvent is used in two main places in flutter. One is GestureBinding. RenderPointerListener is a RenderObject that is attached to _PointerListener. _PointerListener is used in the Listener, and its build function is as follows:

Widget build(BuildContext context) {
  Widget result = _child;
  if(onPointerEnter ! =null|| onPointerExit ! =null|| onPointerHover ! =null) {
    result = MouseRegion(
      onEnter: onPointerEnter,
      onExit: onPointerExit,
      onHover: onPointerHover,
      child: result,
    );
  }
  result = _PointerListener(
    onPointerDown: onPointerDown,
    onPointerUp: onPointerUp,
    onPointerMove: onPointerMove,
    onPointerCancel: onPointerCancel,
    onPointerSignal: onPointerSignal,
    behavior: behavior,
    child: result,
  );
  return result;
}
Copy the code

A Listener is a Widget that integrates multiple types of listeners, including mouse and gesture. However, MouseRegion is recommended for mouse-related listeners, and a Listener focuses on gesture-related events. We can use the Listener directly in the code to listen for related events, such as PointerUp, PointerDown, etc., thus ending the HitTest process.

route

Route process as a whole is divided into two processes, the first step is to event listeners, by calling the GestureBinding. Instance. PointerRouter. Complete registration addRoute, where the incoming parameters for the pointer (in general, for the touch events, For mouse events, pointer is always 0), handleEvent (handles the event function), and Transform (used for point-position conversions, such as converting positions from native layer to positions in a flutter), In addRoute they are encapsulated as _RouteEntry and stored in _routeMap waiting to be delivered. In addition to addGlobalRoute, removeRoute can be used to register global listener, remove listener.

The distribution is done in the same dispatchEvent as the HitTest, and of course in the GestureBinding handleEvent, which calls the route function and then calls each listener’s PointerRoute function, Let the listener do whatever he wants.

In the case of the HitTest process, the process of registering listeners is the HitTest function. This function is automatically executed when a new series of events arrive.

Flutter monitoring gestures are often used in flutter development, such as clicking, double-clicking, etc. Obviously, gestures are monitored here. The widget you need to use is GestureDecter.

As you can see from GestureDecter’s build function, it first sorts various gesture callbacks into gestures and loads them into Gestures. Then it returns an instance of RawGestureDetector. RawGestureDetectorState build function:

Widget build(BuildContext context) {
  Widget result = Listener(
    onPointerDown: _handlePointerDown,
    behavior: widget.behavior ?? _defaultBehavior,
    child: widget.child,
  );
  if(! widget.excludeFromSemantics) result = _GestureSemantics( child: result, assignSemantics: _updateSemanticsForRenderObject, );return result;
}
Copy the code

It can be seen that the Listener is still used inside the RawGestureDetector, but the Listener is only listening to onPointerDown events, which cannot meet the detection of multiple gestures inside the RawGestureDetector. Therefore, the real purpose of RawGestureDetector is not to receive events through the Listener, but to introduce routes for gesture detection.

The RawGestureDetector only listens for onPointerDown events through the Listener and hands them to _handlePointerDown. In _handlePointerDown:

void _handlePointerDown(PointerDownEvent event) {
  assert(_recognizers ! =null);
  for (GestureRecognizer recognizer in _recognizers.values)
    recognizer.addPointer(event);
}
Copy the code

It passes events to all Gesturerecognizers, _recognizers are initialized in the initState phase, and internally save all gesturerecognizers’ subclasses. Such as TapGestureRecognizer, LongPressGestureRecognizer, etc.

In addPointer, the GestureRecognizer will first determine if the GestureRecognizer needs to receive the event (for example, if all of the TabGestureRecognizer’s callback functions are null, the event will not be received, Maybe GestureRecognizer has set fiterKind to receive only certain kinds of events (touch, mouse, etc.) to decide whether to call addAllowedPointer or handleNonAllowedPointer.

AddAllowedPointer PrimaryPointerGestureRecognizer has implemented:

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)); }}Copy the code

StartTrackingPointer will add to the GestureBinding routing, the following are some additional processing, such as the deadline will use for detecting in LongPressGestureRecognizer long press operation.

void startTrackingPointer(int pointer, [Matrix4 transform]) {
  GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
  _trackedPointers.add(pointer);
  assert(! _entries.containsValue(pointer)); _entries[pointer] = _addPointerToArena(pointer); }Copy the code

The Listener is not used to detect gestures. Instead, the Listener is used to detect gestures directly through the route process. It is possible to use this method to detect gestures more quickly. So sometimes when you look at the code it’s not clear in which flow the function is being called, but most of the time it’s not necessary.

PrimaryPointerGestureRecognizer handleEvent implementation is as follows:

void handleEvent(PointerEvent event) {
  assert(state ! = GestureRecognizerState.ready);if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
    final boolisPreAcceptSlopPastTolerance = ! _gestureAccepted && preAcceptSlopTolerance ! =null &&
        _getGlobalDistance(event) > preAcceptSlopTolerance;
    final boolisPostAcceptSlopPastTolerance = _gestureAccepted && postAcceptSlopTolerance ! =null &&
        _getGlobalDistance(event) > postAcceptSlopTolerance;
    if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
      resolve(GestureDisposition.rejected);
      stopTrackingPointer(primaryPointer);
    } else {
      handlePrimaryPointer(event);
    }
  }
  stopTrackingIfPointerNoLongerDown(event);
}
Copy the code

It determines that if this is an available click event, handlePrimaryPointer is called, but if something like move or UP is triggered, It first send a GestureDisposition. Rejected said listening to cancel (GestureArenaManager corresponding to cancel gesture detection aspects of listening, the relevant operation, the later say), Then call stopTrackingPointer to unregister.

In handlePrimaryPointer, use TapGestureRecognizer as an example:

void handlePrimaryPointer(PointerEvent event) {
  if (event is PointerUpEvent) {
    _finalPosition = OffsetPair(global: event.position, local: event.localPosition);
    _checkUp();
  } else if (event is PointerCancelEvent) {
    resolve(GestureDisposition.rejected);
    if (_sentTapDown) {
      _checkCancel(' ');
    }
    _reset();
  } else if (event.buttons != _initialButtons) {
    resolve(GestureDisposition.rejected);
    stopTrackingPointer(primaryPointer);
  }
}
Copy the code

Based on the current event, determine what the next callback might be. For example, if we received a PointerUpEvent event, we would call _checkUp to complete the onTap and onTapUp callbacks. If it was a PointerCancelEvent, First send GestureDisposition rejected said listening to end, and the callback onTapCancel function, buttons or buttons of current events and events for the first time is not at the same time, also said that the current monitoring need to be over.

Look in the _checkUp:

void _checkUp() {
  if(! _wonArenaForPrimaryPointer || _finalPosition ==null) {
    return;
  }
  final TapUpDetails details = TapUpDetails(
    globalPosition: _finalPosition.global,
    localPosition: _finalPosition.local,
  );
  switch (_initialButtons) {
    case kPrimaryButton:
      if(onTapUp ! =null)
        invokeCallback<void> ('onTapUp', () => onTapUp(details));
      if(onTap ! =null)
        invokeCallback<void> ('onTap', onTap);
      break;
    case kSecondaryButton:
      if(onSecondaryTapUp ! =null)
        invokeCallback<void> ('onSecondaryTapUp',
          () => onSecondaryTapUp(details));
      break;
    default:
  }
  _reset();
}
Copy the code

At this point the onTap callback is finished, and a TapGestureRecognizer listener can be declared finished. But about the working principle of the TapGestureRecognizer still not clear, such as the resolve (GestureDisposition. Rejected) specific is how to play a role? Why is there only checkUp? When is checkDown performed? What are the acceptGesture and rejectGesture functions for? Here we need to look again at the GestureArenaManager in event distribution.

gestureArena

GestureArenaManager is similar to Route in that it has a registration-callback process. Going back to the startTrackingPointer function, it calls _addPointerToArena in addition to adding handleEvent to the route.

GestureArenaEntry _addPointerToArena(int pointer) {
  if(_team ! =null)
    return _team.add(pointer, this);
  return GestureBinding.instance.gestureArena.add(pointer, this);
}
Copy the code

Gesturearena. add takes a pointer to the ID of the current series of events and GestureArenaMember. Its two functions are acceptGesture and rejectGesture.

GestureArenaEntry add(int pointer, GestureArenaMember member) {
  final _GestureArena state = _arenas.putIfAbsent(pointer, () {
    assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
    return _GestureArena();
  });
  state.add(member);
  assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
  return GestureArenaEntry._(this, pointer, member);
}
Copy the code

The add function is implemented as shown above. _arenas uses pointer as a key to save _GestureArena, and _GestureArena stores the GestureArenaMember list.

Then the distribution process of gestureArena is similar to route. In the handleEvent of GestureBinding,

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 isPointerSignalEvent) { pointerSignalResolver.resolve(event); }}Copy the code

As shown above, the PointerDownEvent, PointerUpEvent, and PointerSignalEvent will be responded differently. When the PointerDown event comes, GestureArenaManager calls close to close adding a member to the corresponding _GestureArena, and tries to find a member to call its acceptGesture function, indicating that the current set of events has identified the consumer. For the rest of the registered members, the rejectGesture function is invoked:

void close(int pointer) {
  final _GestureArena state = _arenas[pointer];
  if (state == null)
    return; // This arena either never existed or has been resolved.
  state.isOpen = false;
  assert(_debugLogDiagnostic(pointer, 'Closing', state));
  _tryToResolveArena(pointer, state);
}

void _tryToResolveArena(int pointer, _GestureArena state) {
  assert(_arenas[pointer] == state);
  assert(! state.isOpen);if (state.members.length == 1) {
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  } else if (state.members.isEmpty) {
    _arenas.remove(pointer);
    assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
  } else if(state.eagerWinner ! =null) {
    assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}')); _resolveInFavorOf(pointer, state, state.eagerWinner); }}Copy the code

As shown above, there are three ways to register members in _GestureArena. If there is only one member, it is definitely eagerWinner. If there is no member, it will skip processing. In case of multiple members, only eagerWinner event listening is received, and others are rejected. However, if there are multiple members and there is no eagerWinner, it needs to wait for subsequent processing.

If a PointerUp event is received in a handleEvent, the current series of events ends (a series of events usually starts with a PointerDown and ends with a PointerUp/PointerCancel). The sweep function is called to clean up the saved data,

void sweep(int pointer) {
  final _GestureArena state = _arenas[pointer];
  if (state == null)
    return; // This arena either never existed or has been resolved.
  assert(! state.isOpen);if (state.isHeld) {
    state.hasPendingSweep = true;
    assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
    return; // This arena is being held for a long-lived member.
  }
  assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
  _arenas.remove(pointer);
  if (state.members.isNotEmpty) {
    // First member wins.
    assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
    state.members.first.acceptGesture(pointer);
    // Give all the other members the bad news.
    for (int i = 1; i < state.members.length; i++) state.members[i].rejectGesture(pointer); }}Copy the code

When there is no eagerWinner but _GestureArena needs to be cleaned, the first member is identified as eagerWinner and the others are rejected. For gesture detection that cannot be done by a single event such as double clicking, the _GestureArena needs to be held when the first click is OK, preventing the GestureArenaManager from clearing it directly. The HitTest process uses the HitTest function to determine who can receive the event, which is only determined by the current render tree. The route process registers the listener through addRoute. Only the receiver removes the listener by removeRoute. GestureArena is different from gestureArena. From the above point of view, for all members in a _GestureArena, there can only be one eagerWinner, that is, only one gesture can be listened to at the same time, and the rest will be rejected. This is the difference between gestureArena and the other two, and it is also closely related to gesture detection. If HitTest and Route provide the original materials for gesture detection, gestureArena is an important tool for gesture detection, and there are essential differences between them.

It’s up to the event receiver to decide how to become an eagerWinner. A GestureRecognizer works by registering itself in the Route process, always listening for events, GestureRecognizer can use resolve to send a request to GestureArenaManager during the process of receiving subsequent events. If so, remove the other GestureArenaManager from the contention list and call their rejectGesture function to indicate that they have failed to compete, adjusting and resetting (typically unregistering in route, Restore default data, etc.), participate in the next contest, and then initiate gesture detection in your acceptGesture until the event is over. If not, GestureArenaManager removes GestureRecognizer from the contention list and tries to find an eagerWinner. If not, GestureArenaManager continues to wait until an eagerWinner appears or the event ends. GestureArenaManager starts with the first GestureRecognizer participating in the eagerWinner competition and finally finds the eagerWinner or the event ends.

Conclusion: A complete gesture detection

The following is a summary of the above content from a complete click gesture detection process in Flutter (native layer and Engine layer are relatively simple, only data conversion and transfer, not described).

First, the click event listens through the GestureDetector. In its build function, each callback is classified into the GestureRecognizer. The onTap callback is categorized into the TapGestureRecognizer. And pass each GestureRecognizer constructor to RawGestureDetector, where each GestureRecognizer will be instantiated. And the Listener listens for onPointerDown events. Listenr works on the basis of the HitTest process and determines whether to distribute events to the Listener according to the touch position.

When the onPointerDown event is triggered, the RawGestureDetector will make all GestureRecognizer internal attempt to handle the event. Through addPointer, the RawGestureDetector internally performs three functions:

void addPointer(PointerDownEvent event) {
  _pointerToKind[event.pointer] = event.kind;
  if (isPointerAllowed(event)) {
    addAllowedPointer(event);
  } else{ handleNonAllowedPointer(event); }}Copy the code

First call isPointerAllowed to determine whether GestureRecognizer needs to handle this event. Different subclasses have different implementations. For example, the TapGestureRecognizer will only handle kPrimaryButton events if the onTap series of methods are not null. Prepare to receive subsequent events of the series to further determine whether the corresponding gesture is triggered, whether detection needs to be stopped, and so on. Such as PrimaryPointerGestureRecognizer implementation:

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)); }}Copy the code

StartTrackingPointer internal is put PrimaryPointerGestureRecognizer handleEvent function to register the route, It also registers itself in the gestureArena to compete for spending rights on subsequent events. That’s what the HitTest callback phase does.

At the end of the HitTest phase, in the GestureBinding handleEvent, pointerRouter.route is called to start the route phase event distribution, And above PrimaryPointerGestureRecognizer HitTest stage have registered to the route, so this phase will callback it handleEvent function:

void handleEvent(PointerEvent event) {
  assert(state ! = GestureRecognizerState.ready);if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
    final boolisPreAcceptSlopPastTolerance = ! _gestureAccepted && preAcceptSlopTolerance ! =null &&
        _getGlobalDistance(event) > preAcceptSlopTolerance;
    final boolisPostAcceptSlopPastTolerance = _gestureAccepted && postAcceptSlopTolerance ! =null &&
        _getGlobalDistance(event) > postAcceptSlopTolerance;
    if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance))
      resolve(GestureDisposition.rejected);
      stopTrackingPointer(primaryPointer);
    } else {
      handlePrimaryPointer(event);
    }
  }
  stopTrackingIfPointerNoLongerDown(event);
}
Copy the code

At this point, it is the Route phase. The previous part is the judgment of the PointerMoveEvent event. If it is still within the tolerable range, it will continue processing. If the click event is not satisfied, Will exit the gesture of the incident detection (resolve (GestureDisposition. Rejected) remove registered in the gestureArena stopTrackingPointer remove registered in the route). Or call handlePrimaryPointer again to subclass, it eventually will determine whether the event for PointerUpEvent/PointerCancelEvent, if so, also need to remove the route to register, pay attention to, The registration in gestureArena is not removed here, because in the handleEvent of GestureBinding, PointerUpEvent events received are cleaned up directly, so there is no need to remove them one by one.

HandlePrimaryPointer is implemented in TapGestureRecognizer as follows:

void handlePrimaryPointer(PointerEvent event) {
  if (event is PointerUpEvent) {
    _finalPosition = OffsetPair(global: event.position, local: event.localPosition);
    _checkUp();
  } else if (event is PointerCancelEvent) {
    resolve(GestureDisposition.rejected);
    if (_sentTapDown) {
      _checkCancel(' ');
    }
    _reset();
  } else if (event.buttons != _initialButtons) {
    resolve(GestureDisposition.rejected);
    stopTrackingPointer(primaryPointer);
  }
}
Copy the code

Call _checkUp and call onTapUp, onTap, etc. In that order, the first call to this would be a PointerDownEvent, so none of the above should fire. At this point, the first event in the Route phase has ended.

And then, gestureArena, normally, the first PointerDownEvent event will cause it to call close, and an eagerWinner will be selected internally, For a single GestureRecognizer, GestureArenaManager calls its acceptGesture, but if multiple Gesturerecognizers compete for an event, Then the TapGestureRecognizer might not be able to compete and be rejectGesture,

void rejectGesture(int pointer) {
  super.rejectGesture(pointer);
  if (pointer == primaryPointer) {
    // Another gesture won the arena.
    assert(state ! = GestureRecognizerState.possible);if (_sentTapDown)
      _checkCancel('forced '); _reset(); }}Copy the code

The TapGestureRecognizer will reset the data and wait for the next series of events. If the competition is successful and acceptGesture is called,

void acceptGesture(int pointer) {
  super.acceptGesture(pointer);
  if (pointer == primaryPointer) {
    _checkDown(pointer);
    _wonArenaForPrimaryPointer = true; _checkUp(); }}Copy the code

_checkDown and _checkUp are executed for gesture judgment, and handlePrimaryPointer continuously listens for events and responds to PointerUpEvent and PointerCancelEvent. This is how the first PointerDownEvent event was handled. When the event is handled, two things can be determined:

  1. Which GestureRecognizer is listening for events via route
  2. GestureArenaManager which GestureRecognizer are contesting events

One thing that’s not always clear is who the eagerWinner in the GestureArenaManager is, when there’s only one competitor the eagerWinner is the only competitor, but if there’s multiple competitors, Will need to have a competitor calls to resolve (GestureDisposition. Accepted) or until PointerUpEvent, GestureArenaManager need to clean up the data, to determine who is the ultimate recipient.

We assume that there is only one competitor, TapGestureRecognizer, and all subsequent entries are handled in handlePrimaryPointer. If a PointerMoveEvent occurs, If a PointerCancelEvent occurs, cancel the listener. If it is a PointerUpEvent, click the gesture to complete and trigger the onTap callback. At this point, we execute the onTap function passed in the GestureDetector and the gesture detection of the one-click event ends. The rest of the such as long as test (LongPressGestureRecognizer), double-click the test (DoubleTapGestureRecognizer), and other basic processing procedure are roughly similar.

To summarize, there are two ways to distribute events in flutter. One is to pass events in the render tree (HitTest) and the other is to register callback functions directly with the GestureBinding (route). They play different roles in the Flutter system. The HitTest mode is mainly used to listen for basic events, such as the Flutter event, the Flutter event, and so on. The route method is generally used with GestureRecognizer to detect gestures, such as onTap and onDoubleTap. In addition, GestureArenaManager is also a heavily involved user in the process of gesture detection. Assist GestureRecognizer to ensure that the same event triggers only one gesture at a time.