preface

The theme of this discussion is the principle of Flutter touch feedback. It mainly leads us to analyze the concepts related to Flutter gestures step by step from the perspective of source code, such as gesture verdict, gesture recognizer and gesture arena, which will lay a solid foundation for us to develop Flutter related applications in the future. Before the lecture, it is assumed that we all have some basic knowledge of Flutter and have actually applied Flutter in our work, so we will not share the basic concept this time. If you have any questions, please ask me offline. I am not sure I can help you, but I will try my best to solve the problem. OK, so let’s begin today’s topic of “Source code insight into Flutter touch feedback principles”.

Flutter event inlet

The diagram above was taken from other articles on the web. For the moment, don’t worry about how events are distributed from native to the Flutter layer, just know the steps to be boxed in red. The entry point for event distribution in a Flutter is in the GestureBinding class, So the first thing to focus on is the initialization of the GestureBindingGestureBinding#initInstances

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

In the initialization process of GestureBinding _handlePointerDataPacket will be set to the window. The onPointerDataPacket callback, the window here is a very important to interact with native object, When the event from the primary distribution over time will eventually call window. OnPointerDataPacket, here due to set up a callback so we continue to focus on _handlePointerDataPacket function to can.

/ / 1
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
  // The PointerDataPacket is a point of information that converts the original event into a PointerEvent and adds it to the queue of events to be processed
  _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
  if(! locked) _flushPointerEventQueue(); }/ / 2
void _flushPointerEventQueue() {
 
  while (_pendingPointerEvents.isNotEmpty)
    handlePointerEvent(_pendingPointerEvents.removeFirst());
}

/ / 3
void handlePointerEvent(PointerEvent event) {
  assert(! locked);if (resamplingEnabled) {
    _resampler.addOrDispatch(event);
    _resampler.sample(samplingOffset, _samplingClock);
    return;
  }

  _resampler.stop();
  _handlePointerEventImmediately(event);
}



Copy the code

1. You can see that this function takes a PointerDataPacket. A PointerDataPacket is a touch object that encapsulates the information about the point on the screen that your finger touches. Wrap it as a PointerEvent object collection and add it to _pendingPointerEvents(a collection of events to be processed).

2. If the _pendingPointerEvents queue has data, the handlePointerEvent function is executed.

3, there is nothing is mainly see _handlePointerEventImmediately function, but the have to lead another concept “hit test”

Hit testing

GestureBinding#_handlePointerEventImmediately

/ / GestureBinding# _handlePointerEventImmediately handlePointerEvent will for every thing
/ / processing, and eventually to _handlePointerEventImmediately function, the function is mainly perform hit "test"
// and the event distribution entry function dispatchEvent
void _handlePointerEventImmediately(PointerEvent event) {
  // Encapsulates the result of executing a hit test
  HitTestResult? hitTestResult;
  // Just focus on the down event first
  if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
    hitTestResult = HitTestResult();
    // Hit test
    hitTest(hitTestResult, event.position);
    if (event isPointerDownEvent) { _hitTests[event.pointer] = hitTestResult; }}// omit the code......
  if(hitTestResult ! =null ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) {
    assert(event.position ! =null); dispatchEvent(event, hitTestResult); }}Copy the code

HitTest is mainly responsible for hitting the test-related business logic, but when you directly click on the hitTest function you will find the following code

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

GestureBinding is a mixin class, so it is probably pasted to other classes as an extension. This feature is similar to Inheritance in Java, but is much more flexible than inheritance. We continue to find you will find in source GestureBinding being paste to RendererBinding behind the speculation is likely to rewrite the hitTest function in RendererBinding.

RendererBinding#hitTest

//RendererBinding#hitTest
@override
void hitTest(HitTestResult result, Offset position) {
  / / 1
  renderView.hitTest(result, position: position);
  super.hitTest(result, position);                                                      
}

//GestureBinding#hitTest 
@override 
void hitTest(HitTestResult result, Offset position) {
  result.add(HitTestEntry(this));
}

//2 RenderView#hitTest
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

RenderView is the root node of the draw tree, which you can think of as the ancestor of all widgets. Super. hitTest(result, position); That is, GestureBinding#hitTest to add itself to HitTestResult

RenderView#hitTest if there is a child node, then the hitTest function of the child node is called

RenderBox#hitTest

//RenderBox#hitTest
bool hitTest(BoxHitTestResult result, { required Offset position }) {
  // The position touch of the event must be within the current component
  if(_size! .contains(position)) {/ / 1
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      / / 2
      result.add(BoxHitTestEntry(this, position));
      return true; }}return false;
}

//HitTestResult#add
final List<HitTestEntry> _path;

void add(HitTestEntry entry) {
  assert(entry._transform == null);
  entry._transform = _lastTransform;
  _path.add(entry);
}

Copy the code

1. Call hitTestChildren to iterate over child, prioritize children, add itself to HitTestResult as long as one of the children nodes returns true. Since the order of traversal is from the parent node to the child node, the child node is added to the HitTestResult first if it meets the criteria

BoxHitTestResult is a subclass of HitTestResult, which calls the add function (BoxHitTestResult does not override HitTestResult#add, So the HitTestResult add function is called), encapsulating the contact information into BoxHitTestEntry and stuffing it into the _path collection

RenderBox#hitTestChildren

For example, Center is a typical single-node control, Row is a multi-node control, and the judgment is based on whether they have one or more child nodes. HitTestChildren is an empty implementation in RenderBox. So let’s look specifically at how its implementation class handles the difference between single-node and multi-node, single-node for example, Center

/ / 1
class Center extends Align {
  const Center({ Key? key, double? widthFactor, double? heightFactor, Widget? child })
    : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}

//2 Align#createRenderObject
@override
RenderPositionedBox createRenderObject(BuildContext context) {
  return RenderPositionedBox(
    alignment: alignment,
    widthFactor: widthFactor,
    heightFactor: heightFactor,
    textDirection: Directionality.maybeOf(context),
  );
}
Copy the code

Center is just a facade, it inherited from the Align, so we still see the Align this component focus, view the Align you will find that it inherited from SingleChildRenderObjectWidget, here I have a little familiar, When reviewing the three trees of Flutter, I came across this feature frequently. When I basically saw it, I had to rewrite the function createRenderObject.

//Align#createRenderObject
@override
RenderPositionedBox createRenderObject(BuildContext context) {
  return RenderPositionedBox(
    alignment: alignment,
    widthFactor: widthFactor,                                                 
    heightFactor: heightFactor,
    textDirection: Directionality.maybeOf(context),
  );
}  

//RenderShiftedBox#hitTestChildren
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
  if(child ! =null) {
    finalBoxParentData childParentData = child! .parentData!as BoxParentData;
    return result.addWithPaintOffset(
      offset: childParentData.offset,
      position: position,
      hitTest: (BoxHitTestResult result, Offset? transformed) {
        assert(transformed == position - childParentData.offset);
        return child!.hitTest(result, position: transformed!);
      },
    );
  }
  return false;
}

Copy the code

Align’s RenderBox is RenderPositionedBox, and if you go up layer by layer you can see that RenderPositionedBox overrides hitTestChildren and its parent is RenderShiftedBox, In result. AddWithPaintOffset inside you’ll find that eventually calls RenderBox# hitTest execute child node hit test to continue the steps above, A single node is really easy to read as long as you have children and your touch information is contained within the Widget and you iterate layer by layer. For multiple nodes let’s use Row, the first step is to look at the createRenderObject function, and if it doesn’t have one, look in the parent class, Finally we find createRenderObject in its parent Flex class, and we’ll focus on its return class RenderFlex’s hitTestChildren method.

@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
  return defaultHitTestChildren(result, position: position);
}


bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) {
  // define a child variable
  ChildType? child = lastChild;
  while(child ! =null) {
    final ParentDataType childParentData = child.parentData! as ParentDataType;
    //2 Check whether a match is made
    final bool isHit = result.addWithPaintOffset(
      offset: childParentData.offset,
      position: position,
      hitTest: (BoxHitTestResult result, Offset? transformed) {
        assert(transformed == position - childParentData.offset);
        return child!.hitTest(result, position: transformed!);
      },
    );
    if (isHit)
      return true;
    child = childParentData.previousSibling;
  }
  return false;
}

Copy the code

1. The main logic in hitTestChildren is in defaultHitTestChildren. Flex is multi-node, so first define a variable to hold the last child in the set of components

2, because it is a collection of “components”, so it will iterate over each child nodes until you find the control can respond to hit test, continue to go down and the second step in the here before about the single node traversal, so here we will find that in the case of multiple nodes as long as there is a response to hit test would return true, The other nodes will not iterate and will continue to iterate over the control that responded to the initial contact, as shown below. So far all we have talked about is actually pressing down. This step will help us maintain a set of HitTestResult# _PATH for subsequent hit tests. The contact information for the nearest control is first encapsulated as a HitTestEntry and added to the collection, and so on, until the GestureBinding is added, which is the purpose of the hit test.

Dispatching events

I’ve talked about hit tests over and over, but we’ve talked about them so far in the context of interdownevent, what about other events? Before we put the _handlePointerEventImmediately function to pull out the code in turn

void _handlePointerEventImmediately(PointerEvent event) {
  HitTestResult? hitTestResult;
  if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
    
    hitTestResult = HitTestResult();
    // Hit test - this was discussed in the previous article
    hitTest(hitTestResult, event.position);
    if (event is PointerDownEvent) {
      _hitTests[event.pointer] = hitTestResult;
    }
   / / 1
  } else if (event is PointerUpEvent || event is PointerCancelEvent) {
    hitTestResult = _hitTests.remove(event.pointer);
  } else if (event.down) {
   / / 2
    hitTestResult = _hitTests[event.pointer];
  }
  / / 3
  if(hitTestResult ! =null ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) {
    assert(event.position ! =null); dispatchEvent(event, hitTestResult); }}Copy the code

1. If the event is PointerUpEvent or PointerCancelEvent removes it from _hitTests and returns the current hitTestResult, The event will continue to the dispatchEvent at 3 but the event flow will end, and it makes sense that up and Cancel must represent the end of a sequence of events

2, HERE I check the explanation of other articles is “for example, when moving events, fingers are always down”, I am a little confused and do not understand what it means. If it is a move event, it will fetch the previous HitTestResult object via event.pointer. That is eventually move events prior to distribution or use _hitTests will PointerDownEvent preserved HitTestResult object, when I am here to understand, if wrong please bosses to give directions.

3. If hitTestResult is not null, execute dispatchEvent

dispatchEvent

@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
  assert(! locked);if (hitTestResult == null) {
    assert(event is PointerAddedEvent || event is PointerRemovedEvent);
    try {
      pointerRouter.route(event);
    } catch (exception, stack) {
      / /...
    }
    return;
  }
  / / 1
  for (final HitTestEntry entry in hitTestResult.path) {
    try {
      entry.target.handleEvent(event.transformed(entry.transform), entry);
    } catch (exception, stack) {
     / /...}}}/ / 2
class BoxHitTestEntry extends HitTestEntry {
  
  BoxHitTestEntry(RenderBox target, this.localPosition)
    : assert(localPosition ! =null),
      super(target);

  @override
  RenderBox get target => super.target as RenderBox;

  final Offset localPosition;

}

Copy the code

1, traversal the hitTestResult, HitTestEntry easy to understand, every time the hit test meets the condition will encapsulate some information into HitTestEntry to maintain hitTestResult, Entry. Target is HitTestTarget. Because what’s actually jammed into HitTestResult#_path is the subclass of HitTestEntry, BoxHitTestEntry, moving on to code 2

The first entry to the BoxHitTestEntry constructor is RenderBox, which inherits from RenderObject, which implements HitTestTarget. So the previous entry. Target. handleEvent can be interpreted as calling RenderBox#handleEvent, which handles this contact. Not all controls handle handleEvent. RenderPointerListener handles the handleEvent function. RenderPointerListener is created in the Listener component’s createRenderObject. The Listener component is created in the build function in RawGestureDetector#RawGestureDetectorState, HandleEvent is called back to RawGestureDetector for relevant gesture processing depending on the event type

Gestures in Flutter

Learn about gesture recognizers and gesture recognizer factories

In the previous article we learned about the hit test and event distribution. I think you must have a basic concept of Flutter event distribution in your mind. At the end of the previous article we introduced the concept of gesture. Different gestures are handled by different gesture recognizers in Flutter. To better explain gesture recognizers, we will start with one of our most familiar controls, GestureDetector. I believe we are all familiar with GestureDetector. With this Widget we can handle clicks, double clicks, long presses, and swipes. It’s no exaggeration to say that GestureDetector can solve 80% of our business operations related to gestures. The main reason is that GestureDetector encapsulates 8 gesture recognizers, which basically covers our daily development needs. Firstly, GestureDetector is a StatelessWidget, so we only need to pay attention to its build function.

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

  if(onTapDown ! =null|| onTapUp ! =null|| onTap ! =null|| onTapCancel ! =null|| onSecondaryTap ! =null|| onSecondaryTapDown ! =null|| onSecondaryTapUp ! =null|| onSecondaryTapCancel ! =null|| onTertiaryTapDown ! =null|| onTertiaryTapUp ! =null|| onTertiaryTapCancel ! =null
  ) {
  / / 2
    ///Here to build a generic for TapGestureRecognizer GestureRecognizerFactoryWithHandlers object
    gestures[TapGestureRecognizer] =
        GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
          /// Returns the corresponding type of gesture recognizer
              () => TapGestureRecognizer(debugOwner: this),
          /// Bind the callbacks defined in GestureDetector through the callbacks of this object,
          /// The callback methods in GestureDetector are essentially triggered in the gesture recognizer(TapGestureRecognizer instance) { instance .. onTapDown = onTapDown .. onTapUp = onTapUp .. onTap = onTap .. onTapCancel = onTapCancel .. onSecondaryTap = onSecondaryTap .. onSecondaryTapDown = onSecondaryTapDown .. onSecondaryTapUp = onSecondaryTapUp .. onSecondaryTapCancel = onSecondaryTapCancel .. onTertiaryTapDown = onTertiaryTapDown .. onTertiaryTapUp = onTertiaryTapUp .. onTertiaryTapCancel = onTertiaryTapCancel; }); }// omit other gesture recognizers...

  //GestureDetector contains 8 gesture recognizers, which can be used for most purposes
  // The GestureDetector implementation is ultimately delegated to RawGestureDetector.
  The key to using the RawGestureDetector component is to handle the GestureRecognizerFactory Map
      
       .
      ,>
  return RawGestureDetector(
    gestures: gestures,
    behavior: behavior,
    excludeFromSemantics: excludeFromSemantics,
    child: child,
  );
}
Copy the code

There’s a lot of code in the build function so we’ll just look at what we need to look at, and since the code for creating gesture recognizers is mostly the same, we won’t be able to look at all of them, so we’ll just focus on TapGestureRecognizer.

First we create a set of Map<Type, GestureRecognizerFactory> Gestures. The Key is Type, which can be interpreted as the runtime representation of the dart Type. Value: GestureRecognizerFactory indicates the GestureRecognizerFactory.

2. Save the gesture recognizer factory object in Gestures. Note that the Key type here is the specific gesture recognizer. Value for generics are TapGestureRecognizer GestureRecognizerFactoryWithHandlers objects, see you on this big tuo code a little cold, So far we know that the main thing that the build function of GestureDetector does is save the factory that makes the specific gesture recognizer into the Gestures collection, so let’s take a look at what a gesture recognizer factory is.

//gesture_detector.dart # GestureRecognizerFactoryWithHandlers
class GestureRecognizerFactoryWithHandlers<T extends GestureRecognizer>
    extends GestureRecognizerFactory<T> {

  ///Exposed in the constructor_constructor _Initializer callbacks are for the user to handle themselves
  ///_constructor is primarily used to create and return a gesture recognizer
  ///_Initializer calls back the gesture recognizer for use by the user, which is more of a bind listener
  const GestureRecognizerFactoryWithHandlers(this._constructor,
      this._initializer)
      : assert(_constructor ! =null),
        assert(_initializer ! =null);

  final GestureRecognizerFactoryConstructor<T> _constructor;

  final GestureRecognizerFactoryInitializer<T> _initializer;

  @override
  T constructor() => _constructor();

  @override
  void initializer(T instance) => _initializer(instance);
}

//gesture_detector.dart # GestureRecognizerFactory
/ / GestureRecognizerFactory itself is an abstract class, that means he cannot be instantiated
/ / GestureRecognizerFactory GestureRecognizer acceptable types of generics
/ / in the Flutter GestureRecognizerFactoryWithHandlers is GestureRecognizerFactory concrete implementation subclass
@optionalTypeArgs
abstract class GestureRecognizerFactory<T extends GestureRecognizer> {

  const GestureRecognizerFactory();

  // To build a gesture recognizer
  T constructor();

  // Call back the gesture recognizer for the user to use
  void initializer(T instance);

}

Copy the code

Can see GestureRecognizerFactoryWithHandlers derived in GestureRecognizerFactory. It is a specific gesture recognizer factory class, override the constructor, initializer and function, Instead of writing the concrete logic, we pass the logic processing to the _constructor() and _initializer(instance) functions, But you will find it is interesting to note that two methods of the final build is in GestureRecognizerFactoryWithHandlers constructor, so back to GestureDector before the build function look down the screenshot below

The generic type we passed in is TapGestureRecognizer which shows that what this factory class ends up creating is a click-gesture recognizer inCode 1Is performed GestureRecognizerFactoryWithHandlers constructor function is used to generate the gesture recognizer object,Code 2The _initializer(instance) function is used to perform the callback binding. You can see that the gesture recognizer already implements gesture callbacks. For example, the onTap callback is handled by the TapGestureRecognizer. The gesture recognizer factory is used to create gestures that are recognizers and callback bindings.

Know RawGestureDetector

The GestureDetector factory class is returned at the end of the Build function of GestureDetector. RawGestureDetector is a StatefulWidget. This indicates that it maintains component State through the State class and handles it according to the State lifecycle callback function. The task of building the component will be taken over by the corresponding State# Build. For a subclass of StatefulWidget, the component itself only records property information. We just need to know which properties can be set. Source code analysis focuses on how the State class maintains State and builds the component. The analysis of RawGestureDetector focuses on RawGestureDetectorState and _syncAll(Widget. Function.

// Just add user-provided Gestures to the member in the corresponding state class using the _syncAll method
// _recognizers perform maintenance; And using the Listener component when pressed,
// Associate gesture recognizers in _recognizers with gesture event data using the _handlePointerDown method
/// State for a [RawGestureDetector].
class RawGestureDetectorState extends State<RawGestureDetector> {
  // Initialize a _recognizers map collection
  Map<Type, GestureRecognizer>? _recognizers = const <Type, GestureRecognizer>{};
  SemanticsGestureDelegate? _semantics;

  @override
  void initState() {
    super.initState();
    // the _syncAll function processes the Gestures collection
    _syncAll(widget.gestures);
  }

  @override
  void didUpdateWidget(RawGestureDetector oldWidget) {
    super.didUpdateWidget(oldWidget);
    
    ///This is also done when the didUpdateWidget of this state is triggered by a call to setState from the outside worldLogic in _syncAll
    _syncAll(widget.gestures);
  }

  // In the Dispose callback, all gesture detectors in the _recognizers are destroyed in turn.
  // This is why the RawGestureDetector component needs to be a StatefulWidget: GestureRecognizer objects need to be destroyed.
  @override
  void dispose() {
    for (final GestureRecognizer recognizer in_recognizers! .values) recognizer.dispose(); _recognizers =null;
    super.dispose();
  }

  When RawGestureDetectorState is initialized, the associated gesture detector is created and the listener is bound.
  // this logic takes place in the RawGestureDetectorState#_syncAll method.
  void _syncAll(Map<Type, GestureRecognizerFactory> gestures) {
    assert(_recognizers ! =null);
    ///1. Maintain _ in the state classRecognizers are preserved as local variables oldRecognizers, while this_recognizers
    ///So the oldRecognizers are empty when they first use them
    final Map<Type, GestureRecognizer> oldRecognizers = _recognizers! ;///Reinitialization _Recognizers member, for empty mapping {}.
    _recognizers = <Type, GestureRecognizer>{};
    // Iterate through incoming gestures, adding members to _recognizers.
    for (final Type type in gestures.keys) {
    
      ///To formally start processing Gestures call the gesture recognizer factory object constructor function to return the gesture recognizer populated toIn the _recognizers
      ///If oldRecognizers already have GestureRecognizer of the same type, they will reuse it and create new ones from time to time. This is the gesture detector that synchronizes everything._recognizers! [type] = oldRecognizers[type] ?? gestures[type]! .constructor();assert(_recognizers! [type].runtimeType == type,'GestureRecognizerFactory of type $type created a GestureRecognizer of type ${_recognizers! [type] .runtimeType}. The GestureRecognizerFactory must be specialized with the type of the class that it returns from its constructor method.');
      ///Call the Initializer function binding listener for the gesture recognizer factory classgestures[type]! .initializer(_recognizers! [type]!) ; }// If there are types in the oldRecognizers _recognizers that are not already in the class, destroy them.
    for (final Type type in oldRecognizers.keys) {
      if(! _recognizers! .containsKey(type)) oldRecognizers[type]! .dispose(); }}void _handlePointerDown(PointerDownEvent event) {
    assert(_recognizers ! =null);
    // Go through the gesture detector objects in _recognizers and add PointerDownEvent to them.
    // Gesture event data PointerDownEvent is called by the Listener component onPointerDown callback,
    // In _handlePointerDown of RawGestureDetectorState, add PointerDownEvent to _recognizers mapping.
    for (final GestureRecognizer recognizer in_recognizers! .values) recognizer.addPointer(event); }@override
  Widget build(BuildContext context) {
    // The core of RawGestureDetector is ultimately the Listener component
    Widget result = Listener(
      // GestureRecognizer is related to the RawGestureDetector and is not directly related to the Listener component.
      // The onPointerDown callback is triggered when you click the button
      onPointerDown: _handlePointerDown,
      behavior: widget.behavior ?? _defaultBehavior,
      child: widget.child,
    );
   
    return result;
  }


Copy the code

I have marked the comments of the relevant code above, I believe it will be easier to read these comments. At the same time, the return value of the build function of RawGestureDetector returns a Listener component. The Listener component was mentioned in the dispatchEvent section earlier, Those handleEvent functions are eventually handed to the Listener component’s RenderPointerListener#handleEvent during event distribution. The GestureDetector is responsible for producing the gesture recognizer. RawGestureDetector is responsible for handling gesture recognizers and implementing listening bindings, and these gesture recognizers are ultimately handed over to _handlePointerDown. The _handlePointerDown function eventually iterates through the gesture recognizer and calls GestureRecognizer#addPointer. The next step is to explain the flow of a click event, but before that we need to understand the concepts of Flutter gesture processing.

Concepts related to Flutter gesture processing

GestureArenaMember

// Contestant GestureArenaMember
abstract class GestureArenaMember {
  // A callback to competitive success
  void acceptGesture(int pointer);

  // A callback from a race failure
  void rejectGesture(int pointer);
}
Copy the code

GestureArenaEntry

/// Arena message sender
/// This class serves as a bridge between competitors and arena administrators,
/// This class object is maintained directly or indirectly in the gesture detector, and through this class object, it sends a message to the arena manager about wanting to win or want to lose, thus triggering the arena manager's decision method.
class GestureArenaEntry {
  // Private constructs can only be constructed by classes in the arena.dart file
  GestureArenaEntry._(this._arena, this._pointer, this._member);

  // Gesture arena manager
  final GestureArenaManager _arena;
  / / contact id
  final int _pointer;
  // Contestants - gesture recognizer
  final GestureArenaMember _member;

  // Send a message whether the gesture is rejected or accepted
  void resolve(GestureDisposition disposition) {
    // Call the arena manager's _resolve function_arena._resolve(_pointer, _member, disposition); }}Copy the code

_GestureArena

class _GestureArena {
  // List of participants
  final List<GestureArenaMember> members = <GestureArenaMember>[];
  // Whether the arena is open
  bool isOpen = true;
  // Indicates whether the arena is suspended
  bool isHeld = false;
  // Indicates whether the arena is waiting to be cleaned
  bool hasPendingSweep = false;

  // Contestants eager to win
  GestureArenaMember? eagerWinner;

  void add(GestureArenaMember member) {
    assert(isOpen); members.add(member); }}Copy the code

GestureArenaManager

///Arena Administrator - Performs all adjudication logic
///The arena manager is instantiated in [GestureBinding]
///Mixins used in GestureBinding are similar to inheritance, but "paste" is more appropriate
class GestureArenaManager {
  ///Key is the contact ID and value is the arena object. The arena manager does not manage just one arena but several. Each contact corresponds to one arena
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

  ///maintenance_arenas and adds the contestant to _In the arenas
  ///The add function calls can be based on the analysis of click gesture recognizer view [OneSequenceGestureRecognizer]
  ///-> startTrackingPointer ->  _addPointerToArena -> GestureBinding.instance! .gestureArena.add(pointer, this);
  GestureArenaEntry add(int pointer, GestureArenaMember member) {
    /// PutIfAbsent is a method of the Map class. The second argument is a function object that returns value.
    /// The mapping element is added only if the key does not exist. The method eventually returns the value of the key.
    final _GestureArena state = _arenas.putIfAbsent(pointer, () {
      assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
      return _GestureArena();
    });
    // Add a contestant
    state.add(member);
    assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
    // Create and return the arena message sender
    return GestureArenaEntry._(this, pointer, member);
  }

  // Arena closed
  void close(int pointer) {
    // Get the arena object based on the contact ID
    final _GestureArena? state = _arenas[pointer];
    if (state == null)
      return; // This arena either never existed or has been resolved.
    // Tell whether the switch state is set to false to close the arena
    state.isOpen = false;
    assert(_debugLogDiagnostic(pointer, 'Closing', state));
    // Attempt to enforce the ruling
    _tryToResolveArena(pointer, state);
  }

  // Arena cleanup function
  void sweep(int pointer) {
    // Get the arena object based on the contact ID
    final _GestureArena? state = _arenas[pointer];
    // If null, return
    if (state == null)
      return; // This arena either never existed or has been resolved.
    // Assert that the arena must be open
    assert(! state.isOpen);// If the arena is suspended, set its hasPendingSweep to true and return
    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));
    // In the non-suspended state, remove the _arenas corresponding arena based on the contact ID
    _arenas.remove(pointer);
    if (state.members.isNotEmpty) {
      // The arena has a competitor execute the acceptGesture function for the first competitor, which determines the success of the first competitor
      // First member wins.
      assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
      state.members.first.acceptGesture(pointer);
      // The remaining participants call back to inform them of their failure
      for (int i = 1; i < state.members.length; i++) state.members[i].rejectGesture(pointer); }}// Arena suspend function
  void hold(int pointer) {
    // Get the corresponding arena object by the contact ID, set isHeld to true if the arena exists otherwise return
    final _GestureArena? state = _arenas[pointer];
    if (state == null)
      return; // This arena either never existed or has been resolved.
    state.isHeld = true;
    assert(_debugLogDiagnostic(pointer, 'Holding', state));
  }

  // Arena release operation
  void release(int pointer) {
    // Still get arena objects based on the contact ID
    final _GestureArena? state = _arenas[pointer];
    // There is no direct return
    if (state == null)
      return; // This arena either never existed or has been resolved.
    state.isHeld = false;// Set the suspension state of the arena to false
    assert(_debugLogDiagnostic(pointer, 'Releasing', state));
    HasPendingSweep is set to true if the arena was suspended while the arena was being cleaned
    // When the release method is triggered, the sweep function is executed again and the first entrant is declared the winner
    if (state.hasPendingSweep)
      sweep(pointer);
  }

  // Enforcement of adjudication - refusal or admission of a participant in accordance with the circumstances of GestureDisposition
  // When the arena is not closed and disposition is accepted, if it is accepted and the arena is closed, the contestant is declared the winner directly via _resolveInFavorOf
  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));

    //GestureDisposition status == rejection
    if (disposition == GestureDisposition.rejected) {
      // Remove this contestant from the contestant list
      state.members.remove(member);
      // Call the contestant gesture rejected function
      member.rejectGesture(pointer);

      if(! state.isOpen)// If the arena is closed, execute _tryToResolveArena to try to decide
        _tryToResolveArena(pointer, state);
    } else {
      //GestureDisposition status == accept
      assert(disposition == GestureDisposition.accepted);
      if (state.isOpen) {
        // Set member player to "eager to win"state.eagerWinner ?? = member; }else {
        // If the arena is closed, execute _resolveInFavorOf to set member to success
        assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member')); _resolveInFavorOf(pointer, state, member); }}}/// Try verdict - Traverse arena players, rejecting all non-participants, triggering their rejectGesture method,
  /// The entry member is then declared the winner and executes its acceptGesture method. That is, when the ruling method is called, the game is over.
  void _tryToResolveArena(int pointer, _GestureArena state) {
    assert(_arenas[pointer] == state);
    assert(! state.isOpen);// The _resolveByDefault function is triggered when there is only one contestant to make the race successful
    if (state.members.length == 1) {
      scheduleMicrotask(() => _resolveByDefault(pointer, state));
    } else if (state.members.isEmpty) {
      // Remove arena if there are no competitors
      _arenas.remove(pointer);
      assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
    } else if(state.eagerWinner ! =null) {
      // Execute the _resolveInFavorOf function if the winner is specified
      assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
      _resolveInFavorOf(pointer, state, state.eagerWinner!);
    }
  }

  ///Only arena with only one player will be handled
  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);
  }

  // The designated member wins
  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);// Remove the arena object with the contact ID
    _arenas.remove(pointer);
    // Run through arena players and reject all non-participants, triggering their rejectGesture method,
    // Then declare member the winner and execute its acceptGesture method. That is, when the ruling method is called, the game is over.
    for (final GestureArenaMember rejectedMember in state.members) {
      if (rejectedMember != member)
        rejectedMember.rejectGesture(pointer);
    }
    member.acceptGesture(pointer);
  }


}
Copy the code

TapGestureRecognizer example

When there is a down event comes first to perform GestureBinding# _handlePointerEventImmediately hit test related logic, We get the final HitTestResult, and then we go to the dispatchEvent function for event distribution, and as we said earlier, The dispatchEvent function executes entry.target.handleEvent(Event.transformed (entry.transform), entry); This means that RenderObject’s handleEvent function is called, but RenderPointerListener is usually the only one handling the handleEvent function, so continue with the next flow.

RenderPointerListener#handleEvent determines what type of event the current click event is. Now the onPointerDown callback is executed because it is a press event. This callback is passed in by the Listener component. The final execution is onPointerDown in the Listener component which is_handlePointerDownThe addPointer function of the gesture recognizer is called in _handlePointerDown. For now we’ll just focus on TapGestureRecognizer. [GestureRecognizer#addPointer] -> 【 PrimaryPointerGestureRecognizer# addAllowedPointer] – > [OneSequenceGestureRecognizer# startTrackingPointer 】, to the final executionstartTrackingPointerTrace contact ID.

@protected
void startTrackingPointer(int pointer, [Matrix4? transform]) {
  / / 1GestureBinding.instance! .pointerRouter.addRoute(pointer, handleEvent, transform); _trackedPointers.add(pointer);/ / 2
  _entries[pointer] = _addPointerToArena(pointer);
}
Copy the code

1. A pointerRouter is maintained in GestureBinding, where handleEvent is passed as a parameter to the pointerRouter. When the user makes contact with the platform, the event is distributed to the registered gesture recognizer. Trigger handleEvent to distribute events

2. Calling the _addPointerToArena function essentially adds yourself to the gesture arena

GestureArenaEntry _addPointerToArena(int pointer) {
  if(_team ! =null)
    return_team! .add(pointer,this);
    //pointer: touch id this: gesture recognizer
  returnGestureBinding.instance! .gestureArena.add(pointer,this);
}
Copy the code

The first hit test is done when you hit the down event. Based on the hit result, a HitTestResult is generated and distributed. The Listener component is the first one distributed to the event call _handlePointerDown. Bind handleEvent to the touch route and add itself to the gesture arena by calling _addPointerToArena. Now let’s see how we add it to the gesture arena in our code. The arena manager is instantiated in GestureBinding GestureArenaManager gestureArena = GestureArenaManager(); Call _addPointerToArena to execute the following code:

// If you looked at the concept of Flutter gestures earlier, this will be a little easier
GestureArenaEntry add(int pointer, GestureArenaMember member) {
  /// PutIfAbsent is a method of the Map class. The second argument is a function object that returns value.
  /// The mapping element is added only if the key does not exist. The method eventually returns the value of the key.
  final _GestureArena state = _arenas.putIfAbsent(pointer, () {
    assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
    return _GestureArena();
  });
  // Add a contestant
  state.add(member);
  assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
  // Create and return the arena message sender
  return GestureArenaEntry._(this, pointer, member);
}
Copy the code

Create a new arena _GestureArena, add player GestureArenaMember to the arena and return an arena information sender, GestureArenaEntry. See OneSequenceGestureRecognizer code, you will find that it maintains a Map < int, GestureArenaEntry > _entries used to hold information transmitter arena, So the gesture detector can trigger the GestureArenaEntry gesture adjudicator, and remember that RenderPointerListener that we talked about in event distribution executes its handleEvent function, Therefore, the above logic will walk through the other RawGestureDetector and eventually execute the GestureBinding handleEvent function.

@override // from HitTestTarget
//GestureBinding#handleEvent
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

Gesturearena.close (event.pointer); gesturearena.close (event. I won’t post the exact code, it’s all there, but in this step the _tryToResolveArena function is executed

/// Try verdict - Traverse arena players, rejecting all non-participants, triggering their rejectGesture method,
/// The entry member is then declared the winner and executes its acceptGesture method. That is, when the ruling method is called, the game is over.
void _tryToResolveArena(int pointer, _GestureArena state) {
  assert(_arenas[pointer] == state);
  assert(! state.isOpen);// The _resolveByDefault function is triggered when there is only one contestant to make the race successful
  if (state.members.length == 1) {
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  } else if (state.members.isEmpty) {
    // Remove arena if there are no competitors
    _arenas.remove(pointer);
    assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
  } else if(state.eagerWinner ! =null) {
    // Execute the _resolveInFavorOf function if the winner is specified
    assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
    _resolveInFavorOf(pointer, state, state.eagerWinner!);
  }
}
Copy the code

At this point, we assume that there is only one competitor, i.e. only one RawGestureDetector in your Widget tree. At this point, we can be sure that there is only one competitor that will execute the _resolveByDefault function. In this function, the acceptGesture function for the participant is called, which is the callback for the gesture recognizer’s competitive success, but if there are more than one participant on the Widget tree, no winner will be determined at this point.

So far in contact has already been registered to the signal detector, and PrimaryPointerGestureRecognizer# handleEvent function will be maintained in the routing PointerRouter contact, When you raise your finger and trigger the up event, the event will be distributed. At this point, the GestureBinding#handleEvent will be executed, and pointerRouter.route(event) will be executed. According to the contact id you can get to you what is to be executed handleEvent, here to perform is registered before PrimaryPointerGestureRecognizer# handleEvent

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

1. When entering the arena, the participant’s state has been set as possible, and the contact ID of the current Event must be consistent with it before the verification logic of the IF code block can be carried out.

The _gestureAccepted property defaults to false and is set to true only after the contestant has won. PreAcceptSlopTolerance is a double value that marks the threshold of contact distance and is 18 logical pixels. Haven’t isPreAcceptSlopPastTolerance is mainly in competitive victory, to verify the contact of the join coordinates, and whether the current event contact distance is greater than the logic of 18 pixels, if greater than returns true, Established isPreAcceptSlopPastTolerance condition that participants have victory and contact migration 18 pixels. (Refer to the exploration of Flutter gestures – Governing the world)

3, When the touch is in the move event and one of the above two flags is true, the resolve method will be executed, and the rejected method will be passed, indicating that the current contestant sends a failed request and will quit the competition, and then stopTrackingPointer will be used to stop tracking the touch. This is also the root reason why the click event fails after the click is pressed and then slides some distance.

Spoke in front of so many, in fact is about click gestures how athletic competition, but how to judge how a click gesture of victory, can so understanding of the other competitors have failed so the final victory, when press the arena will be closed, but not closed does not mean that the follow-up process, but a new beginning, When PointerUpEvent is triggered, the arena sweep process is executed. Gesturearena.pointer is executed. Function, if the arena is not suspended then the first winner is declared.

Of course, there are still many things to be added, but I really can’t write. This article is mainly used for sharing in my company, and I will slowly add what I haven’t understood.

The resources

1. Flutter gesture exploration — behind the principle and implementation

2. Flutter gestures explore — take charge of the world

3. Learn about event distribution and learn about Flutter

4. Go deeper – Explore the Flutter event distribution principle from one click

Flutter widget-Element-renderObject

6. The Use of Flutter RenderBox & Principle Analysis