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.runUnaryGuarded
Method is later called towindow.onPointDataPacket
Method, 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
->_handlePointerEventImmediately
Below 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
),handleEvent
Will 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
updateUserScrollDirection
Will initiate a slide direction notificationUserScrollNotification
; setPixels
Will initiate aScrollUpdateNotification
Notify and invokenotifyListeners()
To informViewport
Update offsets that we have added ourselves toScrollController
The 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…