Flutter, as a UI framework, has its own event processing mode. This paper mainly describes how touch events are recognized and distributed by widgets after they are transferred from native to Flutter. As for how native systems listen for touch events and transmit them to Flutter, you can learn for yourself that different host systems handle them differently.

Event Handling Process

The processing of touch events in Flutter can be roughly divided into the following stages:

  • Listen for the arrival of events
  • Hit test whether widgets respond to events
  • Distribute events to widgets that pass the hit test

Subsequent touch events are simply called events

Listen for an event

Events are transmitted to a Flutter by the native system through message channels. Therefore, a Flutter must have corresponding listening methods or callbacks. The source code of the Flutter startup process can be found in the following code:

@override 
void initInstances() { 
  super.initInstances(); 
  _instance = this; 
  window.onPointerDataPacket = _handlePointerDataPacket; 
} 
Copy the code

Which window. OnPointerDataPacket is to monitor the event callback, the window is Flutter connected to the host operating system interface, which contains the current equipment and some information of the system and Flutter Engine some callback, below shows some of its properties. Other attributes can be viewed in the official documentation. Note that the window is not the Window class in the DART: HTML standard library.

Class Window {// The DPI of the current device, that is, how many physical pixels are displayed in a logical pixel. The larger the number, the finer the fidelity of the display. Double get devicePixelRatio => _devicePixelRatio; double get devicePixelRatio => _devicePixelRatio; // The Size of the Flutter UI area get physicalSize => _physicalSize; // The default language of the current system Locale Locale get Locale; // Current system font scale. double get textScaleFactor => _textScaleFactor; VoidCallback get onMetricsChanged => _onMetricsChanged; VoidCallback get onLocaleChanged => _onLocaleChanged; VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged; FrameCallback get onBeginFrame => _onBeginFrame; FrameCallback get onBeginFrame => _onBeginFrame; VoidCallback get onDrawFrame => _onDrawFrame; / / pointer to click or event callback PointerDataPacketCallback get onPointerDataPacket = > _onPointerDataPacket; OnBeginFrame and onDrawFrame will then be called when appropriate, Void scheduleFrame() native 'Window_scheduleFrame'; void scheduleFrame() native 'Window_scheduleFrame'; Void render(Scene Scene) native 'Window_render' on the Flutter engine; / / message sending platform void sendPlatformMessage (String name, ByteData data, PlatformMessageResponseCallback callback); PlatformMessageCallback get onPlatformMessage => _onPlatformMessage; . Other attributes and callbacks}Copy the code

Now we have the event entry function _handlePointerDataPacket on the Flutter side. This function allows us to see how the Flutter behaves when it receives the event.

_handlePointerDataPacket

Convert the event once and add it to a queue

Pendingpointerevents: Queue<PointerEvent> queues //locked: Void _handlePointerDataPacket(uI.pointerDatapacket packet) {// We convert pointer data to logical Pixels so that e.g. the touch slop can be // defined in a device-independent manner. _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio)); if (! locked) _flushPointerEventQueue(); }Copy the code

_flushPointerEventQueue

Traverse the queue, locked can be understood as a simple semaphores (lock), call the corresponding handlePointerEvent, handlePointerEvent within call _handlePointerEventImmediately method directly.

void _flushPointerEventQueue() { assert(! locked); while (_pendingPointerEvents.isNotEmpty) handlePointerEvent(_pendingPointerEvents.removeFirst()); } / / / handlePointerEvent: The default didn't dry anything / / / is called _handlePointerEventImmediately method simplified the code void handlePointerEvent (PointerEvent event) { _handlePointerEventImmediately(event); }Copy the code

_handlePointerEventImmediately

Core method: Start different processes based on different event types. Here we only care about PointerDownEvent events.

You can see that when a flutter listens to a PointerDownEvent, it starts a hit test for the specified location.

Flutter contains several event types: see lib-> SRC -> Gesture ->event.dart for details

/ / PointerDownEvent: finger on the screen by pressing is the event void _handlePointerEventImmediately (PointerEvent event) {HitTestResult? hitTestResult; if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {//down assert(! _hitTests.containsKey(event.pointer)); // store widgets that pass the hit test hitTestResult = hitTestResult (); // start hitTest hitTest(hitTestResult, event.position); If (event is PointerDownEvent) {_hitTests[event.pointer] = hitTestResult; if (event is PointerDownEvent) {_hitTests[event. } } else if (event is PointerUpEvent || event is PointerCancelEvent) {//cancel hitTestResult = _hitTests.remove(event.pointer); } else if (event.down) {//move hitTestResult = _hitTests[event.pointer]; } if (hitTestResult ! = null || event is PointerAddedEvent || event is PointerRemovedEvent) { assert(event.position ! = null); // dispatchEvent(event, hitTestResult); }}Copy the code

Main contents of this stage:

  • The listener event callback is registered: _handlePointerDataPacket
  • When an event is received, the converted event is placed in a queue: _flushPointerEventQueue
  • Traverse the queue began to hit testing process: _handlePointerEventImmediately – > hitTest (hitTestResult, event. The position)

Hit testing

The purpose is to determine what renderObjects are at the location of a given event, and in the process store the objects that pass the hit test in the HitTestResult object described above. Take a look at how a hit test is performed inside Flutter through the source code call flow, which we can control within flutter.

To prepare

This is the core method that initializes runApp in the main method of the Flutter entry function. Here WidgetsFlutterBinding implements multiple mixins, many of which implement the hitTest method. This case far from with the keyword of priority, so _handlePointerEventImmediately invokes the hitTest method is rather than in the RendererBinding GestureBinding. For details, you can learn about the call relationship in dart with multiple mixins and each mixin contains the same method. To put it simply, the mixin with the last with is called first.

class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding { 
  static WidgetsBinding ensureInitialized() { 
    if (WidgetsBinding.instance == null) 
      WidgetsFlutterBinding(); 
    return WidgetsBinding.instance!; 
  } 
} 
Copy the code

RendererBinding. HitTest: method to start a hitTest

The main function is to call the hitTest method that renders the root node

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

RendererBinding. RenderView:

Render the root node of the tree

/// The render tree that's attached to the output surface. RenderView get renderView => _pipelineOwner.rootNode! as RenderView; /// Sets the given [RenderView] object (which must not be null), and its tree, to /// be the new render tree to display. The previous tree, if any, is detached. set renderView(RenderView value) { assert(value ! = null); _pipelineOwner.rootNode = value; }Copy the code

RenderView.hitTest

There are two points to note in the hitTest method implementation of the root node:

  • The root node is necessarily added to HitTestResult, which passes the hit test by default
  • From here, the following call flow is related to the Child type
    • Child overrides hitTest to call the overridden method
    • If child is not overridden, the default implementation of its parent, RenderBox, is called
Bool hitTest(HitTestResult result, {required Offset position}) {/// Child is a RenderObject. = null) child! .hitTest(BoxHitTestResult.wrap(result), position: position); result.add(HitTestEntry(this)); return true; }Copy the code

RenderBox.hitTest

The default implementation of the method, which is called if child is not overridden, contains calls to the following two methods:

  • The hitTestChildren function determines whether any children have passed the hit test, and if so, adds the children to the HitTestResult and returns true. If not, return false. This method recursively calls the hitTest method of the child component.
  • HitTestSelf () determines whether it has passed the hit test. If the node needs to be certain that it will respond to the event, it can override this function and return true to “force” that it has passed the hit test.
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; }} @protected bool hitTestSelf(Offset position) => false; @protected bool hitTestChildren(BoxHitTestResult result, { required Offset position }) => false;Copy the code

Rewrite the hitTest:

In this example, we customize a widget and override its hitTest method to see the call flow.

void main() { runApp( MyAPP()); } class MyAPP extends StatelessWidget { const MyAPP({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Container( child: DuTestListener(), ); } } class DuTestListener extends SingleChildRenderObjectWidget { DuTestListener({Key? key, this.onPointerDown, Widget? child}) : super(key: key, child: child); final PointerDownEventListener? onPointerDown; @override RenderObject createRenderObject(BuildContext context) => DuTestRenderObject().. onPointerDown = onPointerDown; @override void updateRenderObject( BuildContext context, DuTestRenderObject renderObject) { renderObject.onPointerDown = onPointerDown; } } class DuTestRenderObject extends RenderProxyBox { PointerDownEventListener? onPointerDown; @override bool hitTestSelf(Offset position) => true; Override void handleEvent(PointerEvent event, Covariant HitTestEntry entry) {// If (Event is PointerDownEvent) onPointerDown? .call(event); } @override bool hitTest(BoxHitTestResult result, {required Offset position}) { // TODO: implement hitTest print('ss'); result.add(BoxHitTestEntry(this, position)); return true; }}Copy the code

Click on the screen (black) to display the following call stack:

After the subclass overrides HitTest, RenderView directly calls our overloaded HitTest method, which fully verifies the logic we analyzed above

Common Widget analysis

In this section, we will examine the Center and Column in Flutter and see how Flutter handles hittests of child and children.

Center

Inheritance: Center – > Align – > SingleChildRenderObjectWidget

Override createRenderObject in Align to return the RenderPositionedBox class. RenderPositionedBox does not override the hitTest method itself, but the hitTestChildren method is overridden in RenderShiftedBox, the parent of its parent class

hitTestChildren
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { if (child ! = null) {/// The parent component calculates the offset of some child widgets in the parent widget when passing constraints to the child widgets, Final BoxParentData 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; } addWithPaintOffset bool addWithPaintOffset({ required Offset? offset, required Offset position, required BoxHitTest hitTest, }) {final Offset transformedPosition = Offset == null? position : position - offset; if (offset ! = null) { pushOffset(-offset); } /// callBack final bool isHit = hitTest(this, transformedPosition); if (offset ! = null) { popTransform(); } return isHit; }Copy the code

Replace the build in MyApp in the example above with the following code to look at the call down stack

@override 
Widget build(BuildContext context) { 
  return Container( 
    child: Center(child: DuTestListener()), 
  ); 
} 
Copy the code

The call stack:

RenderView calls the base RenderBox hitTest, which in turn calls the overridden hitTestChildren. Hit tests are performed on widgets recursively in hitTestChildren.

Column

Inheritance: the Column – > Flex – > MultiChildRenderObjectWidget

RenderFlex overrides the createRenderObject in Flex and returns RenderFlex. RenderFlex itself overrides the hitTest method, but rather the hitTestChildren method

hitTestChildren

The internal direct call RenderBoxContainerDefaultsMixin defaultHitTestChildren method

@override bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { return defaultHitTestChildren(result, position: position); } RenderBoxContainerDefaultsMixin.defaultHitTestChildren bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) { // The x, y parameters have the top left of the node's box as the origin. ChildType? child = lastChild; while (child ! = null) { final ParentDataType childParentData = child.parentData! as ParentDataType; 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

Center and Colunm both contain a single widget and multiple widgets, and both override the hitTestChildren method to control hit tests. The main difference is that Colunm’s hitTestChildren uses a while loop to iterate over its child widgets for hit testing. Colunm traverses lastChild first, and if lastChild fails the hit test, it continues to traverse its sibling. If lastChild passes the hit test, this returns true, and its sibling has no chance to be tested. This type of traversal can also be called depth-first traversal.

If sibling nodes are required to pass the hit test, refer to section 8.3 of Flutter Field, which is not expanded here

Replace the build in MyApp in the example above with the following code to look at the call down stack

@override 
Widget build(BuildContext context) { 
  return Container( 
    child: Column( 
      children: [ 
        DuTestListener(), 
        DuTestListener() 
      ], 
    ) 
  ); 
} 
Copy the code

The call stack

Although we include two dutestlisteners, we end up calling the hitTest method of DuTestListener only once, because lastChid has passed the hitTest and its sibling has no chance to be hit tested.

Flow chart:

Summary of hit test:

  • Start at the Render Tree node and walk down the subtree
  • Traversal mode: depth first traversal
  • You can customize the operations related to hit tests by overriding hitTest, hitTestChildren, and hitTestSelf
  • If sibling nodes exist, the traversal starts from the last one. If any sibling node passes the hit test, the traversal is terminated. The sibling nodes that are not traversed have no chance to participate in the traversal.
  • Depth-first traversal tests the child widgets for hits first, so they are added to BoxHitTestResult before the parent widget.
  • All widgets that pass the hit test are added to an array in BoxHitTestResult for event distribution.

Note: the return value of the hitTest method does not affect whether or not the hitTest passes; only widgets added to BoxHitTestResult pass the hitTest.

Dispatching events

After completion of all the nodes of the hit test code returns to GestureBinding. _handlePointerEventImmediately, will test by hitTestResult stored in a global Map object _hitTests, Key is event.pointer, and the dispatchEvent method is called for event distribution.

GestrueBinding.dispatchEvent

Void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { assert(! locked); if (hitTestResult == null) { assert(event is PointerAddedEvent || event is PointerRemovedEvent); pointerRouter.route(event); return; } for (final HitTestEntry entry in hitTestResult.path) { entry.target.handleEvent(event.transformed(entry.transform), entry); }}Copy the code

The dispatchEvent function iterates through the nodes that pass the hit test and then calls the corresponding handleEvent method. Subclasses can override the handleEvent method to listen for the event distribution.

Look at the call stack again as an example:

Starting with the dispatchEvent method as we would like, we call handleEvent in our custom widget.

Summary:

  • There is no termination condition for event distribution, and events are distributed in the order they are added as long as they pass the hit test
  • Child widgets are distributed before parent widgets

conclusion

This paper analyzes the response principle of FLUTTER events through the source code call flow and some simple examples. What is discussed here is only the most basic event processing flow, on which Flutter encapsulates more semantic widgets such as event listeners, gesture processing and cascading components. Interested students can take a look at the corresponding source code.

Po pay attention to the technology of objects, do the most fashionable technology people!