Delve deeper – Discover the event distribution of Flutter from one click

Introduction:

A requirement encountered a scenario where there were three pages in PageView, one of which was a TabBarView structure. The result is that the outer PageView does not slide when sliding onto TabBar (slide conflict). Finally, I found a solution to this problem on StackOverflow, summarizing the Flutter gesture and slide mechanism in the process. This is also a knowledge that must be mastered by the progression of Flutter. Believe me, this must be the most detailed and easy to understand summary on the whole web!

This series will be divided into three parts:

1. Explore the event distribution principle of Flutter from one click

2. A diagram illustrates the sliding principle of Flutter

3. Practice the principle of Flutter sliding

4. Two ideas for resolving Flutter sliding conflicts

After reading this article you will learn: How does Flutter handle a click event


The introduction

Generally, App development involves a variety of logic to interact with the user, such as the user clicking a button or double-clicking. The GestureDetector in Flutter provides a series of callbacks so that we can easily respond to these user actions. How does Flutter handle this seemingly simple behavior? This article explores the response process of a click event, taking everyone to explore one by one from the source code process.

1. Touch event passing

First of all, we know that any user interaction must be on a native device. So our event distribution must pass from the Native side to the Flutter. The following diagram depicts this process (from other articles).

The one-click response can be decomposed into two events, one Down event for finger press and one Up event for finger lift. In android’s case, these two events are first passed from the Java layer to C++, and finally to Dart. In the Dart section, we noticed that the method window.onPointDataPacket would be called after the zone.runUnaryGuarded method. (Window is a core concept of Flutter. See the GestureBinding initialization process to see that this method executes _handlePointerDataPacket

mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
   / / sets the _handlePointerDataPacket to window. OnPointerDataPacket callback
   window.onPointerDataPacket = _handlePointerDataPacket; }}Copy the code

GestureBinding# _handlePointerDataPacket(ui.PointerDataPacket packet)

// Queue of unprocessed events
final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
// Packet is the information of a point
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
  // We convert pointer data to logical pixels so that e.g. the touch slop can be
  // defined in a device-independent manner.
  // Map the data in data to logical pixels
  DevicePixelRatio The device pixel corresponding to a logical pixel, such as Nex6:3.5
  _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
  if(! locked) _flushPointerEventQueue(); }Copy the code

This method first maps the data to a logical pixel based on the device’s attributes and then adds it to the queue. The next step is to call _flushPointerEventQueue().


GestureBinding# _flushPointerEventQueue()

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

GestureBinding# _handlePointerEvent(PointerEvent event)

/// The key here is event.pointer. Pointer is not repeated. +1 is added to each down event
final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};

void _handlePointerEvent(PointerEvent event) {
  HitTestResult hitTestResult;
  if (event is PointerDownEvent || event is PointerSignalEvent) {
    // hitTest is performed for the down event
    hitTestResult = HitTestResult();
    hitTest(hitTestResult, event.position);
    if (event is PointerDownEvent) {
      // The dowmn event assigns a value to the hitTest set_hitTests[event.pointer] = hitTestResult; }}else if (event is PointerUpEvent || event is PointerCancelEvent) {
    // The up event indicates that the operation has finished, so it is removed
    hitTestResult = _hitTests.remove(event.pointer);
  } else if (event.down) {
    // The move event should also be distributed to the area where the down event was initially clicked. For example, when an ITEM in the list is clicked and the slide starts, the event is always handled by the list and the A item, but if the slide happens the event is handled by the list
    hitTestResult = _hitTests[event.pointer];
  }
  if(hitTestResult ! =null ||
      event is PointerHoverEvent ||
      event is PointerAddedEvent ||
      event isPointerRemovedEvent) { dispatchEvent(event, hitTestResult); }}Copy the code

The call ends up at _handlePointerEvent(PointerEvent event), because the click event must start with the Down event, and a HitTestResult() object is said first in the flow of the PointerDownEvent, Then call hitTest(hitTestResult, event.position).

Summary: A one-click response can be regarded as a Down+ Up event. Dart first converts actual pixels into physical pixels, and then joins the queue in sequence. HitTest is performed for Down events


HitTest collects response controls and distributes them

RendererBinding#hitTest(HitTestResult result, Offset position)

///Renderview: The root node responsible for drawing
RenderView get renderView => _pipelineOwner.rootNode;
///Draw tree owner, responsible for drawing, layout, and composition
PipelineOwner get pipelineOwner => _pipelineOwner;
@override
void hitTest(HitTestResult result, Offset position) {
  assert(renderView ! =null);
  renderView.hitTest(result, position: position);
  super.hitTest(result, position);
  =>
  GestureBinding#hitTest(HitTestResult result, Offset position) {
    result.add(HitTestEntry(this)); }}Copy the code

This hitTest method is overridden by RendererBinding, which calls renderView hitTest(result, position: position). RenderView is the root node of the draw tree and the ancestor of all widgets.


RenderView#hitTest(BoxHitTestResult result, { @required Offset position })

  bool hitTest(HitTestResult result, { Offset position }) {
    if(child ! =null) (RenderBox) child.hittest (boxhittestresult.wrap (result), position: position); result.add(HitTestEntry(this));
    return true;
  }
Copy the code

RenderBox#hitTest(BoxHitTestResult result, { @required Offset position })

///Function: Gives all drawing controls for the specified position
///Returns true, when the control or its child controls are in the given position, to add the drawn object to the given hitResult to indicate that the current control has absorbed the hit event and no other controls are responding
///Returns false to indicate that the event is handled by the control following the current object,
///For example, in a row, multiple areas can respond to a click, so long as the first area responds to a click, there is no need to determine whether it responds later
///The caller needs to convert the global coordinates to the coordinates associated with RenderBox, which determines whether the coordinates are included in the current scope
///This method relies on the latest Layout instead of paint, because the area is determined only by layout
bool hitTest(BoxHitTestResult result, { @required Offset position }) {
  if (_size.contains(position)) {
    // Call its own hitTest for each child, so the deepest child wiget is placed at the beginning
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true; }}return false;
}
Copy the code

And the RenderView’s hitTest(BoxHitTestResult result, {@required Offset position}) finally call RenderBox hitTest(BoxHitTestResult result, {@required Offset position}), HitTestChildren and hitTestSelf are two abstract methods (because widgets can have one or more children). If you look at the implementation, the logic is similar to here. The hitTest of the child Widget is then recursively called. Looking at the method structure, we know that the deeper a Widget is, the sooner it is added to HitTestResult. After this process is executed, HitTestResult gets the set of all controls that can respond to the click event coordinates. Note that the GestureBinding finally adds itself to the end of Result.

//GestureBinding finally adds itself to result
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
  result.add(HitTestEntry(this));
}
Copy the code


GestureBinding#void dispatchEvent(PointerEvent event, HitTestResult hitTestResult)

_handlePointerEvent() 
if(hitTestResult ! =null ||
      event is PointerHoverEvent ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) {
    dispatchEvent(event, hitTestResult);
  }
/// Dispatch an event to a hit test result's path.
/// Distribute hit events to every widget that can respond
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
  ///Loop through the handleEvent for each space
  for (HitTestEntry entry in hitTestResult.path) {
    try{ entry.target.handleEvent(event.transformed(entry.transform), entry); }}}Copy the code

Go back to _handlePointerEvent and finally dispatch dispatchEvent(event, hitTestResult) if hitTestResult is not null. Loop through the handleEvent(Event.Transformed (Entry.transform), entry) method for each object in the collection, However, not all controls handle handleEvents; most of the time only RenderPointerListener handles handleEvents. Query the reference relationship, which is nested in the RawGestureDetector. In fact, almost all of the gesture processing in Flutter is wrapped around this class (see InkWell’s structure, which returns a RenderPointerListener at its innermost layer). Handleevents will vary depending on the event type, Callback to RawGestureDetector’s relevant gesture processing.

Conclusion: At the time of Down event, hitTest obtains a collection of objects that can respond to the event through rendview according to the position clicked, and adds itself to the end of the collection by GestureBinding. But not all control RenderObject subclasses handle handleEvents. Most of the time, RenderPointerListener handles handleEvents. This control is nested within the RawGestureDetector, and handleEvent will call back to RawGestureDetector’s relevant gesture processing depending on the event type. It can be seen that each RawGestureDetector can handleEvent. If there are multiple RawGestureDetector controls in the click area, who should respond to this click?


Gesture competition

Before we look at how conflicts work, we need to understand the basic concepts of gesture processing:

  • GestureRecognizer: GestureRecognizer: GestureRecognizer: GestureRecognizer: GestureRecognizer: GestureRecognizer: GestureRecognizer: GestureRecognizer OneSequenceGestureRecognizer, MultiTapGestureRecognizer, VerticalDragGestureRecognizer, TapGestureRecognizer and so on.

  • GestureArenaManagerr: Gesture competition management, which manages the whole process of “war”, in principle, the conditions for winning the competition are: the first member to win the competition or the last member not to be rejected.

  • GestureArenaEntry: An entity that provides information about the gesture event competition and encapsulates the members participating in the event competition.

  • GestureArenaMember: The participating member abstraction object has the acceptGesture and rejectGesture methods, which represent the member of the gesture gesture competition. The default GestureRecognizer implements this. All competitive members can be understood as GestureRecognizer competitions.

  • _GestureArena: an arena within the GestureArenaManager that has the members of the GestureArenaManager. If a gesture attempts to win when the arena isOpen (isOpen=true), it becomes an object with the attribute “desire to win”. When the arena is closed (isOpen=false), the arena will look for a “hungry to win” object to become a new participant, and if there is only one at that time, that one participant will become the victorious presence of the arena.

Gesturerecognizers compete as GestureArenaMember in _GestureArena when multiple controls can respond to events.


We mentioned above that dispatchEvent(Event, hitTestResult) calls handleEvent for each participant in turn. This method triggers different processing in RawGestureDetector based on different event types. A click starts with the Dowm event and ends with the Up event, followed by the corresponding callback in HitResult.

RenderPointerListener#handleEvent(PointerEvent event, HitTestEntry entry)

Based on the call timing in the figure, each RawGestureDetector in the HitTestResult adds itself to the GestureArenaManager. Don’t forget that the GestureBinding is added at the end of the HitTestResult.


GestureBinding#handleEvent(PointerEvent event, HitTestEntry entry)

@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
 /// Navigate events to trigger GestureRecognizer's handleEvent
 /// Generally PointerDownEvent is not handled in route execution.
 ///GestureArena is GestureArenaManager
  pointerRouter.route(event);
  if (event is PointerDownEvent) {
  	/// Closing the arena
    gestureArena.close(event.pointer);
  } else if (event is PointerUpEvent) {
  	/// Clear the arena to pick a winner
    gestureArena.sweep(event.pointer);
  } else if (event isPointerSignalEvent) { pointerSignalResolver.resolve(event); }}Copy the code

GestureBinding For PointerDownEvent events gesturearena. close(Event.pointer), PointerUpEvent events gesturearena.pointer (event.pointer)


GestureArenaManager#void close(int pointer)

/// Prevents new members from entering the arena.
/// Prevent new members from entering the arena
/// Called after the framework has finished dispatching the pointer down event.
/// Called after event distribution is complete
void close(int pointer) {
	/// Get the member encapsulation that was added with addPointer above
  final _GestureArena state = _arenas[pointer];
  ///Closing the arena
  state.isOpen = false;
	///Try a fight
  _tryToResolveArena(pointer, state);
}
Copy the code

GestureArenaManager# _tryToResolveArena(int pointer, _GestureArena state)

void _tryToResolveArena(int pointer, _GestureArena state) {
  if (state.members.length == 1) {
 		///If there's only one rodeo, let him handle it
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  } else if (state.members.isEmpty) {
    _arenas.remove(pointer);
  } else if(state.eagerWinner ! =null) { _resolveInFavorOf(pointer, state, state.eagerWinner); }}Copy the code

Tracking the flow above shows that the Down event will eventually drive the closing of the arena (because the controls that participate in the gesture contest have already been identified). If only one control responds to the gesture and the control wins, its acceptGesture process is triggered. If there are multiple Tapgesturerecognizers in the control area, there will be no winner in the PointerDownEvent process. Taking a single click as an example, the entire process does not produce a Move. When the UP event is reached, gesturearena.sweep (event.pointer) is executed to force a sweep.


GestureArenaManager#sweep(int pointer)

/// Forces resolution of the arena, giving the win to the first member.
/// Force the arena to produce a winner
/// Sweep is typically after all the other processing for a [PointerUpEvent]
/// have taken place. It ensures that multiple passive gestures do not cause a
/// stalemate that prevents the user from interacting with the app.
/// Sweep is usually done after [PointerUpEvent] occurs. It ensures that competition does not stall, preventing users from interacting with the application.
/// See also:
void sweep(int pointer) {
  ///Get the competing object
  final _GestureArena state = _arenas[pointer];
  if (state.isHeld) {
    state.hasPendingSweep = true;
    return; // This arena is being held for a long-lived member.
  }
  _arenas.remove(pointer);
  if (state.members.isNotEmpty) {
    // First member wins.
    ///The first competitor wins
    state.members.first.acceptGesture(pointer);
    // Give all the other members the bad news.
    for (int i = 1; i < state.members.length; i++)
      ///All subsequent competitors refused to accept the gesturestate.members[i].rejectGesture(pointer); }}Copy the code

The process in sweep is simple; the first of the competitors wins directly and the others refuse to respond. The first competitor is the deepest gesture responder in the Widget tree.


4. Respond to click

BaseTapGestureRecognizer#acceptGesture(int pointer)

@override
void acceptGesture(int pointer) {
  ///Mark yourself as having won the gesture competition
  super.acceptGesture(pointer);
  if (pointer == primaryPointer) {
    _checkDown();
    _wonArenaForPrimaryPointer = true; _checkUp(); }}void _checkDown() {
   ///If it has already been processed, it will not be processed again!!
   if (_sentTapDown) {
     return;
   }
   ///Hands the child control to handle the Down event
   handleTapDown(down: _down);
   _sentTapDown = true;
}
Copy the code

In an acceptGesture, the down event is consumed first


BaseTapGestureRecognizer#_checkUp()

void _checkUp() {
  ///If _up is empty or not the winner of the gesture contest, return directly
  if(! _wonArenaForPrimaryPointer || _up ==null) {
    return;
  }
  handleTapUp(down: _down, up: _up);
  _reset();
}
Copy the code

HandleTapUp () will not be executed if the _UP event is empty. This _up will call handlePrimaryPointer on PointUpEvent, so even when the arena is empty, A Down event with only one competitor will not be recognized as a full click action.

TapGestureRecognizer#handleTapUp({PointerDownEvent down, PointerUpEvent up})

void handleTapUp({PointerDownEvent down, PointerUpEvent up}) {
  final TapUpDetails details = TapUpDetails(
    globalPosition: up.position,
    localPosition: up.localPosition,
  );
  switch (down.buttons) {
    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:}}Copy the code

Finally, execute onTapUp first and onTap later in handleTapUp to complete the identification of a click event.

conclusion

According to the one-click event, the event distribution process of Flutter is as follows:

1. Events are passed from Native layer to Dart layer via C++, mapped to logical pixels and processed in GestureBinding

2. Whatever gesture must start with the Down event. In the Down phase, HitTest starts at the root node that draws the tree, recursively adds controls that can respond to the event to HitTestResult, and GesureBinding adds itself to the end of the list. Event distribution is performed for each object in Result.

3. Not all controls handle handleEvent. RawGestureDetector handles handleEvent. In the Down event, all competitors are added to the _GestureArena to compete, and finally back to the GestureBinding to close the arena. If there is only one RawGestureDetector in the area, the control will win the Down event and perform an acceptGesture. However, onTap will not be triggered. After the Up event, onTapUp will be triggered and onTap will be executed.

4. If there are multiple RawgeStureDetectors in the area, arena Close will not race for a winner during a Down event. During the Up event, Arena Sweep selects the first position to perform an acceptGesture for the winner.

This process takes a click, such as a swipe, as an example, and is sure to produce a winner in the Move phase, rather than waiting for the Up event. This article only provides a complete process to analyze the event distribution mechanism of Flutter, which is the basic process of all gesture interactions. Gestures can be summed up as click, double click, long press and swipe. Disassemble, perhaps making special judgments in down, up, or move events. Familiar with the overall process, in the time to solve the problem will not be unable to start.

The last

This article is the first one about sliding conflict, which is also the most basic knowledge point in event distribution. The next one will explore the nested structure of Scrollable with PageView as an example. The most detailed nested structure diagram will be clear to the Scrollable structure after reading it. One last upvote, QAQ.