Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

Event delivery and competitive response

Explore event distribution in Flutter with the following code

GestureDetector(
  onTap: () {
    print("tap_red");
  },
  onTapDown: (detail){
    print("tap_red_down");
  },
  onTapUp: (detail){
    print("tap_red_up");
  },
  child: Container(
      color: Colors.red,
      width: 200,
      height: 200,

  ),
)
Copy the code

First, we know that the user’s touch behavior must be on a native device (as shown above), and our event distribution must be passed from the Java layer to C++ and ultimately to Dart. In the Dart section, we notice the passagezone.runUnaryGuardedMethod is later called towindow.onPointDataPacketMethod, look at the GestureBinding initialization process to see that the method executes_handlePointerDataPacket.

From the breakpoint we can also see that executing a click event executes the GestureBinding_handlePointerDataPacket->_flushPointerEventQueue->handlePointerEvent->_handlePointerEventImmediatelyBelow we specific look at the specific content of _handlePointerEventImmediately

void _handlePointerEventImmediately(PointerEvent event) { HitTestResult? hitTestResult; if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) { assert(! _hitTests.containsKey(event.pointer)); hitTestResult = HitTestResult(); hitTest(hitTestResult, event.position); if (event is PointerDownEvent) { _hitTests[event.pointer] = hitTestResult; } assert(() { if (debugPrintHitTestResults) debugPrint('$event: $hitTestResult'); return true; } ()); } else if (event is PointerUpEvent || event is PointerCancelEvent) { hitTestResult = _hitTests.remove(event.pointer); } else if (event.down) { // Because events that occur with the pointer down (like // [PointerMoveEvent]s) should be dispatched to the same place that their // initial PointerDownEvent was, we want to re-use the path we found when // the pointer went down, rather than do hit detection each time we get // such an event. hitTestResult = _hitTests[event.pointer]; } assert(() { if (debugPrintMouseHoverEvents && event is PointerHoverEvent) debugPrint('$event'); return true; } ()); if (hitTestResult ! = null || event is PointerAddedEvent || event is PointerRemovedEvent) { assert(event.position ! = null); dispatchEvent(event, hitTestResult); }}Copy the code

We know that a click response can be viewed as a Down+Up event, so the PointerDownEvent goes first, and we initialize hitTestResult, and then we initialize the target in hitTestResult by executing the hitTest method

  • HitTestResult

HitTestResult has a List< HitTestEntry > _path, and HitTestEntry has a HitTestTarget target, which has handleEvent capabilities.

abstract class HitTestTarget {
  // This class is intended to be used as an interface, and should not be
  // extended directly; this constructor prevents instantiation and extension.
  HitTestTarget._();

  /// Override this method to receive events.
  void handleEvent(PointerEvent event, HitTestEntry entry);
}
Copy the code

HitTest method

Looking at the source code, we see that the hitTest method has this relationship in the source code, so let’s look at the specific code in order of execution

RendererBinding.hitTest
@override void hitTest(HitTestResult result, Offset position) { assert(renderView ! = null); assert(result ! = null); assert(position ! = null); renderView.hitTest(result, position: position); super.hitTest(result, position); }Copy the code

Renderview. hitTest and super.hitTest(GestureBinding. HitTest)

RenderView.hitTest

(RenderView is the root node of the draw tree and the ancestor of all widgets.)

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

First implement,child! .hittest (RenderBox. HitTest), and then add itself to result

RenderBox.hitTest
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

To determine whether the touch point belongs to the widget’s size range, execute hitTestChildren and hitTestSelf as two abstract methods (because the widget may have one or more children). Renderbox.hittest is the same as renderBox.hittest, which determines whether it is within the postion range of the click, and then recursively calls the hitTest of the child Widget. Looking at the method structure, we know that the deeper a Widget is, the sooner it is added to HitTestResult. After the whole process is executed, HitTestResult gets the set of all controls that can respond to the click event coordinates. RendererBinding. HitTest executes super.hitTest(result, position); , namely GestureBinding. HitTest

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

Finally, add yourself to the end of HitTestResult; The next key step is to distribute events

dispatchEvent

According to the above logic, the first and go first RendererBinding. DispathchEvent, event. Kind is PointerDeviceKind. The touch, will direct call GestureBinding. DispatchEvent, directly to us see

GestureBinding.dispatchEvent
@override // from HitTestDispatcher @pragma('vm:notify-debugger-on-exception') void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { ... for (final HitTestEntry entry in hitTestResult.path) { entry.target.handleEvent(event.transformed(entry.transform), entry); }}Copy the code

The most important of these is to loop through handleEvents (Event.Transformed (Entry.transform) for each object in the hitTestResult collection, Entry) method, which most of the time is handled only by RenderPointerListener, which is nested within the RawGestureDetector.

RenderPointerListener.handleEvent
@override void handleEvent(PointerEvent event, HitTestEntry entry) { assert(debugHandleEvent(event, entry)); if (event is PointerDownEvent) return onPointerDown? .call(event); if (event is PointerMoveEvent) return onPointerMove? .call(event); if (event is PointerUpEvent) return onPointerUp? .call(event); if (event is PointerHoverEvent) return onPointerHover? .call(event); if (event is PointerCancelEvent) return onPointerCancel? .call(event); if (event is PointerSignalEvent) return onPointerSignal? .call(event); }Copy the code

This method is to trigger different processing in the RawGestureDetector by event type. Almost all gesture processing in Flutter is wrapped around this class (InkWell’s structure returns one at the innermost layer)RenderPointerListener),handleEventWill call back to the relevant gesture processing of RawGestureDetector according to different event types.Compare the execution of our example:The BaseTapGestureRecognizer class performs the recognizer.addPointer internal method, and ultimately adds Gesturecognizer to the arena to compete. Each RawGestureDetector in the HitTestResult adds itself to the GestureArenaManager. Don’t forget that the GestureBinding is added at the end of the HitTestResult, and the GestureBinding also performs handleEvent.

GestureBinding.handleEvent
@override // from HitTestTarget 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); }}Copy the code

summary

Competitive response

A click event must be started with the down event, GestureBinding. The handleEvent method will go first

gestureArena.close(event.pointer);

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

In this case, the arena has only one competitor and executes the _resolveByDefault method directly

void _tryToResolveArena(int pointer, _GestureArena state) { 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!) ; } } void _resolveByDefault(int pointer, _GestureArena state) { if (! _arenas.containsKey(pointer)) return; // Already resolved earlier. assert(_arenas[pointer] == state); assert(! state.isOpen); final List<GestureArenaMember> members = state.members; assert(members.length == 1); _arenas.remove(pointer); assert(_debugLogDiagnostic(pointer, 'Default winner: ${state.members.first}')); state.members.first.acceptGesture(pointer); }Copy the code

As you can see, the final implementation is

BaseTapGestureRecognizer.acceptGesture

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

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

In this case, there is only one competitor, response. Click for BaseTapGestureRecognizer acceptGesture (int pointer), will be in the down event consumption; _checkUp () at this time of _up is null, the _up in PrimaryPointerGestureRecognizer. HandleEvent called when handlePrimaryPointer assignment, so even when the arena, A Down event with only one competitor will not be recognized as a full click action.

void _checkDown() { if (_sentTapDown) { return; } handleTapDown(down: _down!) ; _sentTapDown = true; } void _checkUp() { if (! _wonArenaForPrimaryPointer || _up == null) { return; } assert(_up! .pointer == _down! .pointer); handleTapUp(down: _down! , up: _up!) ; _reset(); }Copy the code

The above is the response process with only one competitor in the region, and the winner has been decided in the Down event. When up, the arena is already cleaned and no operations will be performed.

What if there are multiple responders?

GestureDetector(
  onTap: () {
    print("tap_red");
  },
  onTapDown: (detail){
    print("tap_red_down");
  },
  onTapUp: (detail){
    print("tap_red_up");
  },
  child: Container(
      color: Colors.red,
      width: 200,
      height: 200,
      child: Center(
        child: GestureDetector(
          onTap: () {
            print("tap_green");
          },
          onTapDown: (detail){
            print("tap_green_down");
          },
          onTapUp: (detail){
            print("tap_green_up");
          },
          child: Container(
            color: Colors.green,
            width: 100,
            height: 100,
          ),
        ),
      ),
  ),
)
Copy the code

When I click on the inner green area, there is no more than one responder in the arena. In the down event, the arena cannot directly decide the winner; When you need to sweep the arena in an UP event, select member, the first (innermost layer of the nested structure), as the responder, and other memeber. RejectGesture

GestureArenaManager.sweep

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

Sliding event

According to the above flow, let’s analyze the transfer of sliding events

ListView.builder(itemBuilder: (context, index) {
  return GestureDetector(
      onTap: () {
        print("tap_red");
      },
      onTapDown: (detail) {
        print("tap_red_down");
      },
      onTapUp: (detail) {
        print("tap_red_up");
      },
      child: Container(
        height: 100,
        child: Text("$index"),
      ));
})
Copy the code

First, the slide is a Down + Move event

The down execution process is as follows

At this point, in the down process, there is not only one competitor in the arena, and the result cannot be directly determined;

First move process

DragGestureRecognizer.handleEvent

@override void handleEvent(PointerEvent event) { assert(_state ! = _DragState.ready); if (! event.synthesized && (event is PointerDownEvent || event is PointerMoveEvent)) { final VelocityTracker tracker = _velocityTrackers[event.pointer]! ; assert(tracker ! = null); tracker.addPosition(event.timeStamp, event.localPosition); } if (event is PointerMoveEvent) { if (event.buttons ! = _initialButtons) { _giveUpPointer(event.pointer); return; } if (_state == _DragState.accepted) { _checkUpdate( sourceTimeStamp: event.timeStamp, delta: _getDeltaForDetails(event.localDelta), primaryDelta: _getPrimaryValueFromOffset(event.localDelta), globalPosition: event.position, localPosition: event.localPosition, ); } else { _pendingDragOffset += OffsetPair(local: event.localDelta, global: event.delta); _lastPendingEventTimestamp = event.timeStamp; _lastTransform = event.transform; final Offset movedLocally = _getDeltaForDetails(event.localDelta); final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!) ; _globalDistanceMoved += PointerEvent.transformDeltaViaPositions( transform: localToGlobalTransform, untransformedDelta: movedLocally, untransformedEndPosition: event.localPosition, ).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign; if (_hasSufficientGlobalDistanceToAccept(event.kind)) resolve(GestureDisposition.accepted); } } if (event is PointerUpEvent || event is PointerCancelEvent) { _giveUpPointer(event.pointer); }}Copy the code

In the first Move phase, _state == _dragstate. accepted must be false, Go below process, the bottom there is a judgment _hasSufficientGlobalDistanceToAccept, compared the finger on the screen in the judgment of the sliding distance, if greater than 18 logic pixels are considered to be a slide, Call to resolve (GestureDisposition. Accepted), the method, will call to GestureArenaManager. _resolve, decide the winner, perform acceptGesture

VerticalDragGestureRecognizer._hasSufficientGlobalDistanceToAccept

@override bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind) { return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind); / / > 18}Copy the code

GestureArenaManager._resolve

/// Reject or accept a gesture recognizer. /// /// This is called by calling [GestureArenaEntry.resolve] on the object returned from [add]. 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); } } } 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 (final GestureArenaMember rejectedMember in state.members) { if (rejectedMember ! = member) rejectedMember.rejectGesture(pointer); } member.acceptGesture(pointer); }Copy the code

DragGestureRecognizer.acceptGesture

@override void acceptGesture(int pointer) { assert(! _acceptedActivePointers.contains(pointer)); _acceptedActivePointers.add(pointer); if (_state ! = _DragState.accepted) { _state = _DragState.accepted; final OffsetPair delta = _pendingDragOffset; final Duration timestamp = _lastPendingEventTimestamp! ; final Matrix4? transform = _lastTransform; final Offset localUpdateDelta; switch (dragStartBehavior) { case DragStartBehavior.start: _initialPosition = _initialPosition + delta; localUpdateDelta = Offset.zero; break; case DragStartBehavior.down: localUpdateDelta = _getDeltaForDetails(delta.local); break; } _pendingDragOffset = OffsetPair.zero; _lastPendingEventTimestamp = null; _lastTransform = null; _checkStart(timestamp, pointer); if (localUpdateDelta ! = Offset.zero && onUpdate ! = null) { final Matrix4? localToGlobal = transform ! = null ? Matrix4.tryInvert(transform) : null; final Offset correctedLocalPosition = _initialPosition.local + localUpdateDelta; final Offset globalUpdateDelta = PointerEvent.transformDeltaViaPositions( untransformedEndPosition: correctedLocalPosition, untransformedDelta: localUpdateDelta, transform: localToGlobal, ); final OffsetPair updateDelta = OffsetPair(local: localUpdateDelta, global: globalUpdateDelta); final OffsetPair correctedPosition = _initialPosition + updateDelta; // Only adds delta for down behaviour _checkUpdate( sourceTimeStamp: timestamp, delta: localUpdateDelta, primaryDelta: _getPrimaryValueFromOffset(localUpdateDelta), globalPosition: correctedPosition.global, localPosition: correctedPosition.local, ); } // This acceptGesture might have been called only for one pointer, instead // of all pointers. Resolve all pointers to `accepted`. This won't cause // infinite recursion because an accepted pointer won't be accepted again. resolve(GestureDisposition.accepted); }}Copy the code

Where _checkStart(timestamp)

DragGestureRecognizer._checkStart

void _checkStart(Duration timestamp, int pointer) { assert(_initialButtons == kPrimaryButton); if (onStart ! = null) { final DragStartDetails details = DragStartDetails( sourceTimeStamp: timestamp, globalPosition: _initialPosition.global, localPosition: _initialPosition.local, kind: getKindForPointer(pointer), ); invokeCallback<void>('onStart', () => onStart! (details)); }}Copy the code

Here a DragStartDetails object is wrapped, and the onStart(Details) call is a variable passed in. Let’s go back to the setCanDrag(bool canDrag) method of Scrollable

Scrollable.setCanDrag

@override @protected void setCanDrag(bool canDrag) { if (canDrag == _lastCanDrag && (! canDrag || widget.axis == _lastAxisDirection)) return; if (! canDrag) { _gestureRecognizers = const <Type, GestureRecognizerFactory>{}; // Cancel the active hold/drag (if any) because the gesture recognizers // will soon be disposed by our RawGestureDetector, and we won't be // receiving pointer up events to cancel the hold/drag. _handleDragCancel(); } else { switch (widget.axis) { case Axis.vertical: _gestureRecognizers = <Type, GestureRecognizerFactory>{ VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>( () => VerticalDragGestureRecognizer(supportedDevices: _configuration.dragDevices), (VerticalDragGestureRecognizer instance) { instance .. onDown = _handleDragDown .. onStart = _handleDragStart .. onUpdate = _handleDragUpdate .. onEnd = _handleDragEnd .. onCancel = _handleDragCancel .. minFlingDistance = _physics? .minFlingDistance .. minFlingVelocity = _physics? .minFlingVelocity .. maxFlingVelocity = _physics? .maxFlingVelocity .. velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context) .. dragStartBehavior = widget.dragStartBehavior; })}; break; case Axis.horizontal: _gestureRecognizers = <Type, GestureRecognizerFactory>{ HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>( () => HorizontalDragGestureRecognizer(supportedDevices: _configuration.dragDevices), (HorizontalDragGestureRecognizer instance) { instance .. onDown = _handleDragDown .. onStart = _handleDragStart .. onUpdate = _handleDragUpdate .. onEnd = _handleDragEnd .. onCancel = _handleDragCancel .. minFlingDistance = _physics? .minFlingDistance .. minFlingVelocity = _physics? .minFlingVelocity .. maxFlingVelocity = _physics? .maxFlingVelocity .. velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context) .. dragStartBehavior = widget.dragStartBehavior; })}; break; } } _lastCanDrag = canDrag; _lastAxisDirection = widget.axis; if (_gestureDetectorKey.currentState ! = null) _gestureDetectorKey.currentState! .replaceGestureRecognizers(_gestureRecognizers); }Copy the code

ScrollableState._handleDragStart

void _handleDragStart(DragStartDetails details) { // It's possible for _hold to become null between _handleDragDown and // _handleDragStart, for example if some user code calls jumpTo or otherwise // triggers a new activity to begin. assert(_drag == null); _drag = position.drag(details, _disposeDrag); assert(_drag ! = null); assert(_hold == null); }Copy the code

The position is ScrollPositionWithSingleContext here

ScrollPositionWithSingleContext.drag

@override
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
  final ScrollDragController drag = ScrollDragController(
    delegate: this,
    details: details,
    onDragCanceled: dragCancelCallback,
    carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity),
    motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold,
  );
  beginActivity(DragScrollActivity(this, drag));
  assert(_currentDrag == null);
  _currentDrag = drag;
  return drag;
}
Copy the code

The beginActivity is basically the notification that scrolls UserScrollNotification

Subsequent move

ScrollableState._handleDragUpdate

void _handleDragUpdate(DragUpdateDetails details) { // _drag might be null if the drag activity ended and called _disposeDrag. assert(_hold == null || _drag == null); _drag? .update(details); }Copy the code

So the _drag here is ScrollDragController

ScrollDragController.update

@override void update(DragUpdateDetails details) { assert(details.primaryDelta ! = null); _lastDetails = details; double offset = details.primaryDelta! ; if (offset ! = 0.0) {_lastNonStationaryTimestamp = details. SourceTimeStamp; } // By default, iOS platforms carries momentum and has a start threshold // (configured in [BouncingScrollPhysics]). The 2 operations below are // no-ops on Android. _maybeLoseMomentum(offset, details.sourceTimeStamp); offset = _adjustForScrollStartThreshold(offset, details.sourceTimeStamp); If (offset == 0.0) {return; } if (_reversed) // e.g. an AxisDirection.up scrollable offset = -offset; delegate.applyUserOffset(offset); }Copy the code

Here will invoke the delegate. ApplyUserOffset (offset), the delegate is a ScrollActivityDelegate interface, the main implementation class is ScrollPositionWithSingleContext.

ScrollPositionWithSingleContext.applyUserOffset

@ override void applyUserOffset (double delta) {updateUserScrollDirection (delta > 0.0? ScrollDirection.forward : ScrollDirection.reverse); setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta)); }Copy the code

updateUserScrollDirectionWill initiate a slide direction notificationUserScrollNotification; setPixelsWill initiate aScrollUpdateNotificationNotify and invokenotifyListeners()To informViewportUpdate offsets that we have added ourselves toScrollControllerThe methods in

Now, we know the whole process of sliding

conclusion

In this paper, only examples of intuitive analysis of the whole process, as their own learning record, if you are helpful, welcome little caution heart ~

Reference article: juejin.cn/post/689599… Juejin. Cn/post / 689856…