1

As a UI framework, Flutter itself has its own event processing method. This article mainly describes how touch events are recognized and distributed by widgets after they are passed from native to Flutter. As for how the native system listens for touch events and transmits events to Flutter, those who are interested can learn about how different host systems handle it 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 the widget can respond to events
  • Distribute events to widgets that pass the hit test

Subsequent touch events are directly referred to as events

listen event

The event is passed to Flutter by the native system through the message channel, so Flutter must have a corresponding monitoring method or callback. From the source code of the Flutter startup process, you can view the following code in mixin GestureBinding :

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

Among them, window.onPointerDataPacket is the callback to monitor the event, and window is the interface that Flutter connects to the host operating system, which contains some information about the current device and system and some callbacks of the Flutter Engine. Some of its properties are shown below. For other properties, you can check the official documentation by yourself. Note that the window and here are not the window classes in the dart:html standard library.

class Window { 
     
  // 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。 
  // DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5  
  double get devicePixelRatio => _devicePixelRatio; 
   
  // Flutter UI绘制区域的大小 
  Size get physicalSize => _physicalSize; 
 
  // 当前系统默认的语言Locale 
  Locale get locale; 
     
  // 当前系统字体缩放比例。   
  double get textScaleFactor => _textScaleFactor;   
     
  // 当绘制区域大小改变回调 
  VoidCallback get onMetricsChanged => _onMetricsChanged;   
  // Locale发生变化回调 
  VoidCallback get onLocaleChanged => _onLocaleChanged; 
  // 系统字体缩放变化回调 
  VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged; 
  // 绘制前回调,一般会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用 
  FrameCallback get onBeginFrame => _onBeginFrame; 
  // 绘制回调   
  VoidCallback get onDrawFrame => _onDrawFrame; 
  // 点击或指针事件回调 
  PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket; 
  // 调度Frame,该方法执行后,onBeginFrame和onDrawFrame将紧接着会在合适时机被调用, 
  // 此方法会直接调用Flutter engine的Window_scheduleFrame方法 
  void scheduleFrame() native 'Window_scheduleFrame'; 
  // 更新应用在GPU上的渲染,此方法会直接调用Flutter engine的Window_render方法 
  void render(Scene scene) native 'Window_render'; 
 
  // 发送平台消息 
  void sendPlatformMessage(String name, 
                           ByteData data, 
                           PlatformMessageResponseCallback callback) ; 
  // 平台通道消息处理回调   
  PlatformMessageCallback get onPlatformMessage => _onPlatformMessage; 
   
  ... //其它属性及回调 
    
} 

Now we have the entry function _handlePointerDataPacket of the event on the Flutter side. Through this function, we can see how Flutter operates after receiving the event. It is relatively simple to look at the code directly.

_handlePointerDataPacket

Convert the event once and add it to a queue

///_pendingPointerEvents: Queue<PointerEvent>类型的队列 
///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(); 
} 

_flushPointerEventQueue

Traversing the above queue, locked can be understood as a simple semaphore (lock), calling the corresponding handlePointerEvent, and calling the _handlePointerEventImmediately method directly in the handlePointerEvent.

void _flushPointerEventQueue() { 
  assert(!locked); 
  while (_pendingPointerEvents.isNotEmpty) 
    handlePointerEvent(_pendingPointerEvents.removeFirst()); 
} 

///handlePointerEvent :默认啥也没干就是调用了_handlePointerEventImmediately方法 
///简化后的代码 
void handlePointerEvent(PointerEvent event) { 
  _handlePointerEventImmediately(event); 
} 

_handlePointerEventImmediately

core method : start different processes according to different event types, here we only care about the PointerDownEvent event.

It can be seen that when flutter listens to PointerDownEvent, it will start the hit test process for the specified location.

Flutter contains multiple event types: you can view specific information in lib->src->gesture->event.dart

// PointerDownEvent: 手指在屏幕按下是产生的事件
void _handlePointerEventImmediately(PointerEvent event) { 
  HitTestResult? hitTestResult; 
  if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {//down 
    assert(!_hitTests.containsKey(event.pointer)); 
    ///存储通过命中测试的widget 
    hitTestResult = HitTestResult(); 
    ///开始命中测试 
    hitTest(hitTestResult, event.position); 
    ///测试完成后会将通过命中测试的结果存放到一个全局map对象里 
    if (event is PointerDownEvent) { 
      _hitTests[event.pointer] = hitTestResult; 
    } 
  } 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); 
  } 
} 

Main contents of this stage:

  • Registered callback for listening events: _handlePointerDataPacket
  • After receiving the event, put the converted event into a queue: _flushPointerEventQueue
  • Traverse the queue to start the hit test process: _handlePointerEventImmediately-> hitTest(hitTestResult, event.position)

hit test

The purpose is to determine which render objects (renderObjects) are at the given event location, and in this process, the objects that pass the hit test will be stored in the HitTestResult object . Let's see how hit testing is performed inside flutter through the source code call process. We can control those in these processes.

Prepare

Before starting the hit test source code analysis, look at the following code. This is the core method of calling runApp initialization in the main method of the Flutter entry function. Here WidgetsFlutterBinding implements multiple mixins, and many of these mixins implement hitTest method, in this case, the one far from the with keyword is executed first, so the hitTest method called in _handlePointerEventImmediately is in RendererBinding instead of GestureBinding. For details, you can learn about the calling relationship when there are multiple mixins in dart and each mixin contains the same method. Simply put, the mixin with the last with will be 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!; 
  } 
} 

RendererBinding. hitTest: Start method of hit test

The main function is to call the hitTest method of the root node of the rendering tree

@override 
void hitTest(HitTestResult result, Offset position) { 
  assert(renderView != null); 
  assert(result != null); 
  assert(position != null); 
  /// renderView:渲染树根节点,继承自RenderObject 
  renderView.hitTest(result, position: position); 
  super.hitTest(result, position); 
} 

RendererBinding.renderView:

the root node of the render 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; 
} 

RenderView.hitTest

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

  • The root node is bound to be 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, it will call the default implementation of parent class RenderBox
bool hitTest(HitTestResult result, { required Offset position }) { 
///child是一个 RenderObject 对象 
  if (child != null) 
    child!.hitTest(BoxHitTestResult.wrap(result), position: position); 
  result.add(HitTestEntry(this)); 
  return true; 
} 

RenderBox.hitTest

The method implemented by default, if the child is not overridden, this method will be called, which mainly includes the following two method calls:

  • The function of hitTestChildren is to judge whether any child node has passed the hit test. If so, it will add the child component to HitTestResult and return true; if not, return false directly. In this method, the hitTest method of the child component is called recursively.
  • hitTestSelf() determines whether it passes the hit test. If the node needs to ensure that it can respond to events, it can rewrite this function and return true, which is equivalent to "forcibly declaring" 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; 
} 
 
/// RenderBox中默认实现都是返回的false 
@protected 
bool hitTestSelf(Offset position) => false; 
 
@protected 
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) => false; 

Override hitTest:

In this example, we customize a widget, rewrite its hitTest method, and look at the calling process.

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; 
  } 
} 

Clicking on the screen (black) shows the following call stack:

After the subclass rewrites HitTest, after RenderView, it directly calls our overloaded hitTest method, which fully confirms the logic we analyzed above.

Common widget analysis

In this section, we will analyze Center and Column in Flutter, and see how Flutter handles two types of hitTests: child and children.

Center

Inheritance: Center->Align->SingleChildRenderObjectWidget

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

hitTestChildren
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { 
  if (child != null) { 
  ///父组件在传递约束到子widget时,会计算一些子widget在父widget中的偏移,这些数据通常存在BoxParentData中 
  ///这里就使用子widget在父widget中的偏移 
    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); 
        ///递归调用child的hitTest方法 
        ///transformed转换后的位置 
        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; 
} 

Replace the build in MyApp in the above example with the following code, and look at the call stack

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

call stack:

It is very clear, because the Center-related parent class does not rewrite the hitTest method, so the hitTest in the base class RenderBox is called directly in the renderView, and the rewritten hitTestChildren is called in this hitTest, and the widget is hit recursively in the hitTestChildren. .

Column

Inheritance: Column->Flex->MultiChildRenderObjectWidget

RenderFlex overrides createRenderObject in Flex to return RenderFlex. RenderFlex itself does not override the hitTest method, but overrides the hitTestChildren method

hitTestChildren

The RenderBoxContainerDefaultsMixin.defaultHitTestChildren method is directly called internally

@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; 
} 

One of Center and Colunm contains a single widget, the other contains multiple widgets, and both rewrite the hitTestChildren method to control the hit test. The main difference between the two is that Colunm's hitTestChildren uses while loop to traverse its own child widgets. hit test. Moreover, the Colunm traversal order is to traverse the lastchild first. If the lastchild fails the hit test, it will continue to traverse its sibling nodes. If the lastchild passes the hit test, this returns true directly, and its sibling nodes have no chance to perform the hit test. This traversal method also It can be called depth-first traversal.

If you need sibling nodes to pass the hit test, you can refer to the description in section 8.3 of <Flutter combat>, which is not expanded here

Replace the build in MyApp in the above example with the following code, and look at the call stack

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

call stack

Although we include two DuTestListeners, the hitTest method of DuTestListener will only be called once in the end, because lastChid has passed the hit test, and its sibling nodes have no chance to perform the hit test.

flow chart:

Hit Test Summary:

  • Traverse the subtree down from the node of the Render Tree
  • Traversal method: depth-first traversal
  • You can customize hit test related operations by overriding hitTest, hitTestChildren, hitTestSelf
  • When there are sibling nodes, the traversal starts from the last one, and if any one passes the hit test, the traversal is terminated, and the untraversed sibling nodes have no chance to participate.
  • The process of depth-first traversal will hit test the child widget first, so the child widget will be added to the BoxHitTestResult before the parent widget.
  • All widgets that pass the hit test will be added to an array in BoxHitTestResult for event dispatch.

Note: The return value of the hitTest method does not affect whether the hit test is passed. Only the widgets added to the BoxHitTestResult pass the hit test.

event distribution

Upon completion of the hit test all the nodes, the code is returned to the GestureBinding._handlePointerEventImmediately , will be hit by the test HitTestResult the Map object is stored in a global _hitTests years, key to event.pointer, then call the dispatchEvent method 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); 
  } 
} 

You can see from the source code that the role of the dispatchEvent function is to traverse the nodes that pass the hit test, and then call the corresponding handleEvent method. Subclasses can override handleEvent method to monitor the distribution of events.

Still take the above code as an example to see the call stack:

Consistent with what we think, start with the dispatchEvent method and call the handleEvent in our custom widget.

summary:

  • There is no termination condition for event distribution, as long as it passes the hit test, events will be distributed in the order of joining
  • Child widgets are dispatched before parent widgets

Summary

This article mainly analyzes the response principle of events in flutter through the calling process of the source code combined with some simple cases. The discussion here is only the most basic event processing process. Flutter encapsulates event monitoring, gesture processing and cascading components on these basic processes. Semantic widget, interested students can take a look at the corresponding source code.

Text/A Bao
Pay attention to Dewu Technology and be the most fashionable technical person!


得物技术
851 声望1.5k 粉丝