Flutter notes | Flutter events and notifications

raw pointer event handling

hit test

On the mobile side, the original pointer event models of each platform or UI system are basically the same, that is, a complete event is divided into three stages: finger press, finger move, and finger lift, while higher-level gestures ( Such as click, double click, drag, etc.) are based on these primitive events.

When the pointer is pressed, Flutter will perform a hit test (Hit Test) on the application to determine which components (widgets) exist where the pointer is in contact with the screen, and the pointer press event (and subsequent events of the pointer) are then distributed to The innermost component discovered by the hit test, and from there, events bubble up the component tree. These events are distributed from the innermost component to all components on the path to the root of the component tree. This is similar to web development. The event bubbling mechanism of the browser in Flutter is similar, but there is no mechanism in Flutter to cancel or stop the "bubbling" process, and the browser's bubbling can be stopped. Note that only components that pass the hit test will fire the event.

Listener component

It can be used in Flutter Listenerto monitor raw touch events and Listeneris also a functional component. Here is Listenerthe constructor definition:

Listener({
    
    
  Key key,
  this.onPointerDown, //手指按下回调
  this.onPointerMove, //手指移动回调
  this.onPointerUp,//手指抬起回调
  this.onPointerCancel,//触摸事件取消回调
  this.behavior = HitTestBehavior.deferToChild, //先忽略此参数,后面会专门介绍
  Widget child
})

Let's look at an example first, the function of the following code is: When the finger moves on a container, check the position of the finger relative to the container.

class _PointerMoveIndicatorState extends State<PointerMoveIndicator> {
    
    
  PointerEvent? _event;

  
  Widget build(BuildContext context) {
    
    
    return Listener(
      child: Container(
        alignment: Alignment.center,
        color: Colors.blue,
        width: 300.0,
        height: 150.0,
        child: Text(
          '${
    
    _event?.localPosition ?? ''}',
          style: TextStyle(color: Colors.white),
        ),
      ),
      onPointerDown: (PointerDownEvent event) => setState(() => _event = event),
      onPointerMove: (PointerMoveEvent event) => setState(() => _event = event),
      onPointerUp: (PointerUpEvent event) => setState(() => _event = event),
    );
  }
}

Effect after running:

insert image description here

Move your finger in the blue rectangular area to see the current pointer offset. When the pointer event is triggered, the parameters PointerDownEvent, PointerMoveEvent, and PointerUpEventare PointerEventall subclasses, PointerEventwhich include some information about the current pointer. Note Pointerthat "pointer" refers to the event The trigger can be mouse, touchpad, finger.

like:

  • position: It is the offset of the pointer relative to the global coordinates.
  • localPosition: It is the offset of the pointer relative to its own layout coordinates.
  • delta: The distance between two pointer movement events ( PointerMoveEvent).
  • pressure: Press pressure, if the mobile phone screen supports pressure sensors (such as iPhone's 3D Touch), this attribute will be more meaningful, if the mobile phone does not support it, it will always be 1.
  • orientation: The moving direction of the pointer, which is an angle value.

The above are just PointerEventsome commonly used attributes, in addition to these it has many attributes, you can view the API documentation.

There is also a behaviorproperty, which determines how the child component responds to the hit test, and this property will be described in detail later.

ignore pointer events

If we don't want a subtree to respond PointerEvent, we can use IgnorePointerand AbsorbPointer, both of these components can prevent the subtree from receiving pointer events, the difference is that it AbsorbPointerwill participate in the hit test itself, but IgnorePointerit will not participate in itself, which means that AbsorbPointerit is Can receive pointer events (but not its subtree), IgnorePointerbut not.

A simple example is as follows:

Listener(
  child: AbsorbPointer(
    child: Listener(
      child: Container(
        color: Colors.red,
        width: 200.0,
        height: 100.0,
      ),
      onPointerDown: (event)=>print("in"),
    ),
  ),
  onPointerDown: (event)=>print("up"),
)

When clicked Container, because it is on AbsorbPointerthe subtree, it will not respond to pointer events, so the log will not output " in", but AbsorbPointerit can receive pointer events, so it will output " up". If AbsorbPointerreplaced by IgnorePointer, then neither will be output.

Gesture Recognition

GestureDetector

GestureDetectorIt is a functional component for gesture recognition, through which we can recognize various gestures. GestureDetectorIt is encapsulated internally Listenerto recognize semantic gestures. Next, we will introduce the recognition of various gestures in detail.

1. Click, double click, long press

We GestureDetectorperform Containergesture recognition on the phone, and after triggering the corresponding event, Containerthe event name is displayed on the screen. In order to increase the click area, Containerset it to 200×100, the code is as follows:

class _GestureTestState extends State<GestureTest> {
    
    
  String _operation = "No Gesture detected!"; //保存事件名
  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: GestureDetector(
        child: Container(
          alignment: Alignment.center,
          color: Colors.blue,
          width: 200.0,
          height: 100.0,
          child: Text(
            _operation,
            style: TextStyle(color: Colors.white),
          ),
        ),
        onTap: () => updateText("Tap"), //点击
        onDoubleTap: () => updateText("DoubleTap"), //双击
        onLongPress: () => updateText("LongPress"), //长按
      ),
    );
  }

  void updateText(String text) {
    
    
    //更新显示的事件名
    setState(() {
    
    
      _operation = text;
    });
  }
}

running result:

insert image description here

Note: When listening onTapto and onDoubleTapevents at the same time, when the user triggers tapthe event, there will be 200a delay of about milliseconds. This is because the user is likely to click again to trigger the double-click event after clicking, so GestureDetectorit will wait for a while to determine whether it is Double click event. If the user only listens onTap(not listens onDoubleTap) to the event, there is no delay.

2. Drag, slide

A complete gesture process refers to the entire process from pressing the user's finger to lifting it. During this period, the user may or may not move after pressing the finger. GestureDetectorThere is no distinction between drag and swipe events, they are essentially the same. GestureDetectorThe origin (upper left corner) of the component to be monitored will be used as the origin of the gesture. When the user presses the finger on the monitored component, gesture recognition will start.

Let's look at an example of dragging a circular letter A:

class _Drag extends StatefulWidget {
    
    
  
  _DragState createState() => _DragState();
}

class _DragState extends State<_Drag> with SingleTickerProviderStateMixin {
    
    
  double _top = 0.0; //距顶部的偏移
  double _left = 0.0;//距左边的偏移

  
  Widget build(BuildContext context) {
    
    
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          left: _left,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")), 
            onPanDown: (DragDownDetails e) {
    
     // 手指按下时会触发此回调
              // 打印手指按下的位置(相对于屏幕)
              print("用户手指按下:${
      
      e.globalPosition}");
            }, 
            onPanUpdate: (DragUpdateDetails e) {
    
     // 手指滑动时会触发此回调
              // 用户手指滑动时,更新偏移,重新构建
              setState(() {
    
    
                _left += e.delta.dx;
                _top += e.delta.dy;
              });
            },
            onPanEnd: (DragEndDetails e){
    
    
              // 打印滑动结束时在x、y轴上的速度
              print(e.velocity);
            },
          ),
        )
      ],
    );
  }
}

After running, you can drag in any direction, the running effect:

insert image description here

log:

I/flutter ( 8513): 用户手指按下:Offset(26.3, 101.8)
I/flutter ( 8513): Velocity(235.5, 125.8)

Code explanation:

  • DragDownDetails.globalPosition: When the user presses, this property is the offset of the user's pressed position relative to the origin (upper left corner) of the screen (not the parent component).
  • DragUpdateDetails.delta: When the user slides on the screen, multiple Updateevents will be triggered, which refers to the sliding offset of deltaan event.Update
  • DragEndDetails.velocityx、y: This attribute represents the sliding speed (including two axes) when the user lifts the finger. In the example, the speed when the finger is lifted is not processed. The common effect is to make a deceleration animation according to the speed when the user lifts the finger .

single direction drag

In the above example, it is possible to drag in any direction, but in many scenarios, we only need to drag in one direction, such as a vertical list, which can only recognize gesture events in a specific direction. We will use the above GestureDetectorexample Change to only dragging vertically:

class _DragVertical extends StatefulWidget {
    
    
  
  _DragVerticalState createState() => _DragVerticalState();
}

class _DragVerticalState extends State<_DragVertical> {
    
    
  double _top = 0.0; 
  
  Widget build(BuildContext context) {
    
    
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")), 
            onVerticalDragUpdate: (DragUpdateDetails details) {
    
     // 垂直方向拖动事件
              setState(() {
    
    
                _top += details.delta.dy;
              });
            },
          ),
        )
      ],
    );
  }
}

In this way, you can only drag in the vertical direction, and it is the same if you only want to slide in the horizontal direction.

3. Zoom

GestureDetectorZoom events can be monitored. The following example demonstrates a simple image zoom effect:

class _Scale extends StatefulWidget {
    
    
  const _Scale({
    
    Key? key}) : super(key: key); 
  
  _ScaleState createState() => _ScaleState();
}

class _ScaleState extends State<_Scale> {
    
    
  double _width = 200.0; // 通过修改图片宽度来达到缩放效果 
  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: GestureDetector( 
        child: Image.asset("./images/sea.png", width: _width), // 指定宽度,高度自适应
        onScaleUpdate: (ScaleUpdateDetails details) {
    
    
          setState(() {
    
    
            // 缩放倍数在0.8到10倍之间
            _width = 200 * details.scale.clamp(.8, 10.0);
          });
        },
      ),
    );
  }
}

running result:

insert image description here
Now you can zoom in and out by pinching two fingers apart or pinching on the picture.

GestureRecognizer

GestureDetectorInternally, one or more gestures are used to GestureRecognizerrecognize various gestures, and GestureRecognizerthe function is to Listenerconvert raw pointer events into semantic gestures, and GestureDetectordirectly receive a child widget. GestureRecognizerIt is an abstract class, and a gesture recognizer corresponds to a GestureRecognizersubclass. Flutter implements a wealth of gesture recognizers, which we can use directly.

Example: Suppose we want to RichTextadd click event handlers to different parts of a piece of rich text ( ), but TextSpannot one widget, we can’t use it at this time GestureDetector, but TextSpanthere is a recognizerproperty, it can receive one GestureRecognizer.

Suppose we need to change the color of the text on click:

import 'package:flutter/gestures.dart';

class _GestureRecognizer extends StatefulWidget {
    
    
  const _GestureRecognizer({
    
    Key? key}) : super(key: key);

  
  _GestureRecognizerState createState() => _GestureRecognizerState();
}

class _GestureRecognizerState extends State<_GestureRecognizer> {
    
    
  TapGestureRecognizer _tapGestureRecognizer = TapGestureRecognizer();
  bool _toggle = false; //变色开关

  
  void dispose() {
    
    
    //用到GestureRecognizer的话一定要调用其dispose方法释放资源
    _tapGestureRecognizer.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: Text.rich(
        TextSpan(
          children: [
            TextSpan(text: "你好世界"),
            TextSpan(
              text: "点我变色",
              style: TextStyle(
                fontSize: 30.0,
                color: _toggle ? Colors.blue : Colors.red,
              ),
              recognizer: _tapGestureRecognizer
                ..onTap = () {
    
    
                  setState(() {
    
    
                    _toggle = !_toggle;
                  });
                },
            ),
            TextSpan(text: "你好世界"),
          ],
        ),
      ),
    );
  }
}

running result:

insert image description here

Note: GestureRecognizerAfter use, be sure to call its dispose()method to release resources (mainly cancel the internal timer).

Flutter event mechanism

Flutter event processing flow

The Flutter event processing process is mainly divided into two steps. In order to focus on the core process, we use the user touch event as an example to illustrate:

  1. Hit test : When the finger is pressed, PointerDownEventthe event is triggered, and the current rendering ( ) tree is traversed in depth first render object, and each rendering object is "hit tested" ( hit test). If the hit test passes, the rendering object will be added to a HitTestResultlist among.
  2. Event distribution : After the hit test is completed, HitTestResultthe list will be traversed, and the event processing method ( ) of each rendering object will be called handleEventto process PointerDownEventthe event. This process is called "event distribution" ( event dispatch). Then when the finger moves, PointerMoveEventthe event is dispatched.
  3. Event cleaning : When the finger is lifted ( PointerUpEvent) or the event is canceled ( PointerCancelEvent), the corresponding event will be distributed first, and HitTestResultthe list will be cleared after the distribution is completed.

requires attention:

  • Hit testing is PointerDownEventdone when an event fires, a completed event stream is down > move > up (cancle).
  • If both parent and child components listen to the same event, the child component will respond to the event before the parent component . This is because the hit test process is traversed according to the depth-firstHitTestResult rule, so the child rendering object will be added to the list before the parent rendering object, and because the list is traversed from front to back when the event is dispatched HitTestResult, the child component will be earlier than the parent component is called handleEvent.

Let's look at some of the entire event processing process from the code level:

// 触发新事件时,flutter 会调用此方法
void _handlePointerEventImmediately(PointerEvent event) {
    
    
  HitTestResult? hitTestResult;
  if (event is PointerDownEvent ) {
    
    
    hitTestResult = HitTestResult();
    // 发起命中测试
    hitTest(hitTestResult, event.position);
    if (event is PointerDownEvent) {
    
    
      _hitTests[event.pointer] = hitTestResult;
    }
  } else if (event is PointerUpEvent || event is PointerCancelEvent) {
    
    
    //获取命中测试的结果,然后移除它
    hitTestResult = _hitTests.remove(event.pointer);
  } else if (event.down) {
    
     // PointerMoveEvent
    //直接获取命中测试的结果
    hitTestResult = _hitTests[event.pointer];
  }
  // 事件分发
  if (hitTestResult != null) {
    
    
    dispatchEvent(event, hitTestResult);
  }
}

The above code is only the core code, and the complete code is located GestureBindingin the implementation. Let's introduce some hit testing and event distribution processes respectively.

Hit Test Details

1. The starting point of the hit test

Whether an object can respond to events depends on whether it is added to the list during its hit test HitTestResult. If it is not added, the subsequent event distribution will not be distributed to itself. Let's take a look at the process of hit testing: when a user event occurs, Flutter will RenderViewcall it from the root node ( ) hitTest().


void hitTest(HitTestResult result, Offset position) {
    
    
  //从根节点开始进行命中测试
  renderView.hitTest(result, position: position); 
  // 会调用 GestureBinding 中的 hitTest()方法,我们将在下一节中介绍。
  super.hitTest(result, position); 
}

The above code is located RenderBindingin , the core code is only two lines, the overall hit test is divided into two steps, let's explain:

  • The first step: renderViewis RenderViewthe corresponding RenderObjectobject, the main function RenderObjectof the object hitTestmethod is: starting from this node, recursively traverse each node on the subtree (rendering tree) in depth-first order and hit test them. This process is called " render tree hit testing ".

    Note: For the convenience of expression, "rendering tree hit test" can also be expressed as a component tree or node tree hit test, but we need to know that the logic of the hit test is in , not RenderObjectin Widgetor Element.

  • Step 2: After the rendering tree hit test is completed, the method of will be called GestureBinding, hitTestwhich is mainly used to handle gestures, which we will introduce later.

2. Rendering tree hit testing process

The hit test process of the rendering tree is the recursive process of hitTestcontinuously calling the child node method in the parent node method .hitTest

Here is the RenderViewsource hitTest()code:

// 发起命中测试,position 为事件触发的坐标(如果有的话)。
bool hitTest(HitTestResult result, {
    
     Offset position }) {
    
    
  if (child != null)
    child.hitTest(result, position: position); //递归对子树进行命中测试
  //根节点会始终被添加到HitTestResult列表中
  result.add(HitTestEntry(this)); 
  return true;
}

Since RenderViewthere is only one child, child.hitTestjust call directly. If a rendering object has multiple child nodes, the hit test logic is: if any child node passes the hit test, or if the current node "forces" itself to pass the hit test, the current node will pass the hit test.

Let's take RenderBoxit as an example to see its hitTest()implementation:

bool hitTest(HitTestResult result, {
    
      Offset position }) {
    
    
  ...  
  if (_size.contains(position)) {
    
     // 判断事件的触发位置是否位于组件范围内
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
    
    
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
  }
  return false;
}

In the above code:

  • hitTestChildren()The function is to judge whether any child node has passed the hit test, if yes, it will add the child component to and HitTestResultreturn at the same time true; if not, it will return directly false. In this method, the method of the child component will be called recursively hitTest.
  • hitTestSelf()Determine 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.

It should be noted that the sign that a node passes the hit test is that it is added to HitTestResultthe list, not hitTestits return value, although in most cases the node will return if it passes the hit test true, but since developers can override when customizing components hitTestYes, so it is possible to return when the hit test is passed false, or when the hit test is not passed true. Of course, this is not good. We should avoid it as much as possible when customizing components, but in some cases that need to customize the hit test process It may be necessary to break this tacit understanding in scenarios, such as the components we will implement later HitTestBlocker.

So the overall logic is:

  1. First judge whether the trigger position of the event is within the scope of the component, if not, the hit test will not pass, hitTestreturn at this time false, if yes, go to the second step.
  2. It will first call hitTestChildren()to determine whether any child nodes pass the hit test, if so, add the current node to HitTestResultthe list, and hitTestreturn at this time true. That is, as long as a child node passes the hit test, its parent node (the current node) will also pass the hit test.
  3. If no child node passes the hit test, hitTestSelfthe return value of the method will be used. If the return value is true, the current node passes the hit test, otherwise it will not.

If the current node has child nodes that pass the hit test or the current node itself passes the hit test, the current node is added to HitTestResult. And because the methods hitTestChildren()of subcomponents will be called recursively hitTest, the hit test order of the component tree is depth-first, that is, if the hit test is passed, the subcomponent will be added to the component before the parent component HitTestResult.

Let's take a look at the default implementation of these two methods as follows:


bool hitTestChildren(HitTestResult result, {
    
     Offset position }) => false;


bool hitTestSelf(Offset position) => false;

You can see that these two methods return by default false. If the component contains multiple subcomponents, the hitTestChildren()method must be rewritten, and the method of each subcomponent should be called in this method hitTest. For example, let's take a look at RenderBoxContainerDefaultsMixinthe implementation in :

// 子类的 hitTestChildren() 中会直接调用此方法
bool defaultHitTestChildren(BoxHitTestResult result, {
    
     required Offset position }) {
    
    
   // 遍历所有子组件(子节点从后向前遍历)
  ChildType? child = lastChild;
  while (child != null) {
    
    
    final ParentDataType childParentData = child.parentData! as ParentDataType;
    // isHit 为当前子节点调用hitTest() 的返回值
    final bool isHit = result.addWithPaintOffset(
      offset: childParentData.offset,
      position: position,
      //调用子组件的 hitTest方法,
      hitTest: (BoxHitTestResult result, Offset? transformed) {
    
    
        return child!.hitTest(result, position: transformed!);
      },
    );
    // 一旦有一个子节点的 hitTest() 方法返回 true,则终止遍历,直接返回true
    if (isHit) return true;
    child = childParentData.previousSibling;
  }
  return false;
}

  bool addWithPaintOffset({
    
    
    required Offset? offset,
    required Offset position,
    required BoxHitTest hitTest,
  }) {
    
    
    ...// 省略无关代码
    final bool isHit = hitTest(this, transformedPosition);
    return isHit; // 返回 hitTest 的执行结果
  }

We can see that the main logic of the above code is to traverse and call the method of subcomponents hitTest(), and at the same time provide an interruption mechanism : that is, as long as there is a child node that hitTest()returns during the traversal process true:

  1. will terminate the child node traversal, which means that the sibling nodes before the child node will have no chance to pass the hit test. Note that the traversal of sibling nodes is in reverse order .

  2. The parent node also passes the hit test. Because the child node hitTest()returns truethe parent node hitTestChildrenwill also return true, which will eventually lead to hitTestthe return of the parent node true, and the parent node is added to HitTestResult.

When the child node hitTest()returns false, continue to traverse the sibling nodes in front of the child node and perform hit tests on them. If all child nodes return , the falseparent node will call its own hitTestSelfmethod. If the method also returns false, the parent node is considered to have failed the hit test.

Consider the following two questions:

  1. Why should this interrupt be formulated?

    Because the layout space occupied by sibling nodes does not coincide under normal circumstances, there will only be one node at the coordinate position clicked by the user, so once it is found (passed the hit test, hitTestreturn true), there is no need to judge other sibling nodes up. But there are exceptions. For example, in Stackthe layout, the layout space of sibling components will overlap. If we want the component at the bottom to respond to events, we must have a mechanism that allows us to ensure that: even if a node is found, it will The traversal should not be terminated, which means that all subcomponent hitTestmethods must return false! To this end, this process is customized in Flutter HitTestBehavior, which we will introduce later.

  2. Why is the traversal of sibling nodes in reverse order?

    As 1mentioned above, sibling nodes generally do not overlap, and once overlapping occurs, the latter component will often be on top of the front component. When clicked, the latter component should respond to the event, while the front covered component cannot respond. , so the hit test should give priority to testing the following nodes, because once the test is passed, it will not continue to traverse. If we follow the forward traversal, it will appear that the covered component can respond to the event, but the above component cannot, which is obviously not in line with expectations.

Let's go back hitTestChildrento , if it is not rewritten hitTestChildren, it will return directly by default false, which means that the descendant nodes will not be able to participate in the hit test, which is equivalent to the event being intercepted, which is exactly the principle that IgnorePointerand AbsorbPointercan intercept the event delivery.

If hitTestSelfis returned true, the current node itself will be added to the regardless of whether there are nodes that pass the hit test among the child nodes HitTestResult. Therefore, the difference between IgnorePointerand AbsorbPointeris that the former hitTestSelfreturns false, while the latter returns true.

After the hit test is complete, all nodes that pass the hit test are added to HitTestResult.

event distribution

The event distribution process is very simple, that is, traverse HitTestResultand call the method of each node handleEvent:

// 事件分发
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
    
    
  ... 
  for (final HitTestEntry entry in hitTestResult.path) {
    
    
    entry.target.handleEvent(event.transformed(entry.transform), entry);
  }
}

So components only need to override handleEventthe method to handle events.

HitTestBehavior

1. Introduction to HitTestBehavior

Let's first implement a PointerDownEventcomponent that can listen to :

class PointerDownListener extends SingleChildRenderObjectWidget {
    
    
  PointerDownListener({
    
    Key? key, this.onPointerDown, Widget? child})
      : super(key: key, child: child);

  final PointerDownEventListener? onPointerDown;

  
  RenderObject createRenderObject(BuildContext context) =>
      RenderPointerDownListener()..onPointerDown = onPointerDown;

  
  void updateRenderObject(
      BuildContext context, RenderPointerDownListener renderObject) {
    
    
    renderObject.onPointerDown = onPointerDown;
  }
}

class RenderPointerDownListener extends RenderProxyBox {
    
    
  PointerDownEventListener? onPointerDown;

  
  bool hitTestSelf(Offset position) => true; //始终通过命中测试

  
  void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
    
    
    //事件分发时处理事件
    if (event is PointerDownEvent) onPointerDown?.call(event);
  }
}

Because we make hitTestSelfthe return value of is always true, so no matter whether the child node passes the hit test or not, it will pass, so it will be called PointerDownListenerwhen the event is subsequently distributed . We can trigger the callback when the event type is determined in it. The test code is as follows:handleEventPointerDownEvent

class PointerDownListenerRoute extends StatelessWidget {
    
    
  const PointerDownListenerRoute({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return PointerDownListener(
      child: Text('Click me'),
      onPointerDown: (e) => print('down'),
    );
  }
}

After clicking the text, the console will print ' down'.

ListenerThe implementation of PointerDownListeneris similar to that of , with two differences:

  1. ListenerThere are more types of events to monitor.
  2. ListenerThe hitTestSelfis not always returned true.

Here we need to focus on the second point. ListenerThe component has a behaviorparameter, which we haven't introduced before, so let's introduce it in detail below. By looking at Listenerthe source code, I found that its rendering object RenderPointerListenerinherits RenderProxyBoxWithHitTestBehaviorthe class:

abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox {
    
    
  //[behavior] 的默认值为 [HitTestBehavior.deferToChild].
  RenderProxyBoxWithHitTestBehavior({
    
    
    this.behavior = HitTestBehavior.deferToChild,
    RenderBox? child,
  }) : super(child);

  HitTestBehavior behavior;

  
  bool hitTest(BoxHitTestResult result, {
    
     required Offset position }) {
    
    
    bool hitTarget = false;
    if (size.contains(position)) {
    
    
      hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
      if (hitTarget || behavior == HitTestBehavior.translucent) //1
        result.add(BoxHitTestEntry(this, position)); // 通过命中测试
    }
    return hitTarget;
  }

  
  bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque; //2

}

We see that it will be used behaviorin hitTestand hitTestSelf, and its value will affect Listenerthe hit test results of . Let's take a look at behaviorwhat the values ​​are:

//在命中测试过程中 Listener 组件如何表现。
enum HitTestBehavior {
    
    
  // 组件是否通过命中测试取决于子组件是否通过命中测试
  deferToChild,
  // 组件必然会通过命中测试,同时其 hitTest 返回值始终为 true
  opaque,
  // 组件必然会通过命中测试,但其 hitTest 返回值可能为 true 也可能为 false
  translucent,
}

It has three values. Let's hitTestanalyze the effects of different values ​​in combination with implementation:

  1. behaviorWhen deferToChild, hitTestSelfreturns false, whether the current component can pass the hit test depends entirely on hitTestChildrenthe return value of . That is to say, as long as one child node passes the hit test, the current component will pass the hit test.
  2. behaviorWhen opaqueis , hitTestSelfreturns true, hitTargetthe value is always true, the current component passes the hit test.
  3. behaviorWhen translucent, hitTestSelfreturns false, hitTargetthe value depends on hitTestChildrenthe return value of at this time, but no matter hitTargetwhat the value of is, the current node will be added to HitTestResult.

Note that the current component will pass the hit test behaviorfor opaqueand translucent. The difference between them is that hitTest()the return value ( hitTarget) of may be different, so the difference depends on hitTest()what the return value will affect. We have already introduced this in detail above. Next, we pass An example to understand.

2. Example: implement App watermark

The effect is as shown in the figure:

insert image description here

The implementation idea is to overlay a watermark mask on the top layer of the page, which we can Stackachieve by passing the watermark component as the last child Stack:

class WaterMaskTest extends StatelessWidget {
    
    
  const WaterMaskTest({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Stack(
      children: [
        wChild(1, Colors.white, 200),
        WaterMark(
         painter: TextWaterMarkPainter(text: 'wendux', rotate: -20),
        ),
      ],
    );
  }

  Widget wChild(int index, color, double size) {
    
    
    return Listener(
      onPointerDown: (e) => print(index),
      child: Container(
        width: size,
        height: size,
        color: Colors.grey,
      ),
    );
  }
}

WaterMarkIt is the component to implement the watermark. We will introduce the specific logic later, and now we only need to know that it WaterMarkis used in DecoratedBox. The effect is realized, but Stackwhen we click on the first subcomponent (gray rectangle area), we find that there is no output on the console, which is not as expected, because the watermark component is at the top and the event is "blocked" by it. Let's analyze this process:

  1. When clicking, Stackthere are two sub-components. This is the click test for the second sub-component (watermark component). Since it is used in the watermark component, after checking the source code, it is found that if the DecoratedBoxuser clicks on the DecoratedBoxupper position, it hitTestSelfwill return true, so The watermark component passes the hit test.

  2. After the watermark component passes the hit test, it will cause Stackthe of hitTestChildren()to return directly (stop traversing other child nodes), so Stackthe first child component of will not participate in the hit test, so it will not respond to the event.

hitTestThe reason is found, and the solution is to find a way to allow the first subcomponent to participate in the hit test. In this case, we have to find a way to return the second subcomponent false. So we can wrap IgnorePointerit with WaterMask.

IgnorePointer(child: WaterMark(...))

After modification, rerun and find that the first subcomponent can respond to events.

If we want Stackall subcomponents of to respond to events, how should we achieve it? Of course, this is likely to be a pseudo-requirement , which is rarely encountered in real scenarios, but considering this issue can deepen our understanding of the Flutter event processing process.

class StackEventTest extends StatelessWidget {
    
    
  const StackEventTest({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Stack(
      children: [
        wChild(1),
        wChild(2),
      ],
    );
  }

  Widget wChild(int index) {
    
    
    return Listener(
      onPointerDown: (e) => print(index),
      child: Container(
        width: 100,
        height: 100,
        color: Colors.grey,
      ),
    );
  }
}

After running, click on the gray box and guess what the console will print?

  • The answer is that only one ' 2' will be printed. The reason is that Stackthe second child node is traversed first Listener, because Containerthe hitTestwill return true(actually Containeris a combined component. In this example, Containerone will be generated eventually ColoredBox, and the one participating in the hit test is ColoredBoxcorresponding RenderObject), So Listenerthe of hitTestChildrenwill be returned true, and eventually the Listenerof hitTestwill be returned true, so the first child node will not receive the event.

What if we specify the property Listenerof the as or ? In fact, the result is still the same, because as long as the of will be returned , the final of will be returned , and the first node will not be hit tested again.behavioropaquetranslucentContainerhitTesttrueListenerhitTestChildrentrue

What are the specific scenarios where opaqueand translucentcan reflect the difference? Theoretically, there is only a difference between the two when Listenerthe sub-node hitTestreturns , but the components with UI in Flutter will basically return falsewhen the user clicks on it, so it is difficult to find specific scenarios, but in order to test their difference , we can forcibly create a scene, such as the following code:hitTesttrue

class HitTestBehaviorTest extends StatelessWidget {
    
    
  const HitTestBehaviorTest({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Stack(
      children: [
        wChild(1),
        wChild(2),
      ],
    );
  }

  Widget wChild(int index) {
    
    
    return Listener(
      //behavior: HitTestBehavior.opaque, // 放开此行,点击只会输出 2
      behavior: HitTestBehavior.translucent, // 放开此行,点击会同时输出 2 和 1
      onPointerDown: (e) => print(index),
      child: SizedBox.expand(),
    );
  }
}

SizedBoxThere is no sub-element, when it is clicked, its hitTestwill return false, at this time Listenerthe behavioris set to opaqueand translucentthere will be a difference (see comments).

Because the above similar cases hardly appear in actual scenarios, if you want Stackall subcomponents of to respond to events, you must ensure Stackthat all children of hitTestare returned false. Although IgnorePointeryou can do this by wrapping all subcomponents with , but IgnorePointeralso At the same time, hit testing will no longer be performed on subcomponents, which means that its subcomponent tree will not be able to respond to events. For example, after the following code is run, clicking the gray area will not produce any output:

class AllChildrenCanResponseEvent extends StatelessWidget {
    
    
  const AllChildrenCanResponseEvent({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Stack(
      children: [
        IgnorePointer(child: wChild(1, 200)),
        IgnorePointer(child: wChild(2, 200)),
      ],
    );
  }

  Widget wChild(int index, double size) {
    
    
    return Listener(
      onPointerDown: (e) => print(index),
      child: Container(
        width: size,
        height: size,
        color: Colors.grey,
      ),
    );
  }
}

ContainerAlthough we have listened to the event in the child node , the child node is IgnorePointerin , so the child node has no chance to participate in the hit test, so it will not respond to any event. It seems that there is no ready-made component that can meet the requirements, so we can implement a component by ourselves and then customize it hitTestto meet our requirements.

3. HitTestBlocker

Below we define a component that can intercept hitTestvarious processes HitTestBlocker.

class HitTestBlocker extends SingleChildRenderObjectWidget {
    
    
  HitTestBlocker({
    
    
    Key? key,
    this.up = true,
    this.down = false,
    this.self = false,
    Widget? child,
  }) : super(key: key, child: child);

  /// up 为 true 时 , `hitTest()` 将会一直返回 false.
  final bool up; 
  /// down 为 true 时, 将不会调用 `hitTestChildren()`.
  final bool down; 
  /// `hitTestSelf` 的返回值
  final bool self;

  
  RenderObject createRenderObject(BuildContext context) {
    
    
    return RenderHitTestBlocker(up: up, down: down, self: self);
  }

  
  void updateRenderObject(BuildContext context, RenderHitTestBlocker renderObject) {
    
    
    renderObject
      ..up = up
      ..down = down
      ..self = self;
  }
}

class RenderHitTestBlocker extends RenderProxyBox {
    
    
  RenderHitTestBlocker({
    
    this.up = true, this.down = true, this.self = true});

  bool up;
  bool down;
  bool self;

  
  bool hitTest(BoxHitTestResult result, {
    
    required Offset position}) {
    
    
    bool hitTestDownResult = false;
    if (!down) {
    
    
      hitTestDownResult = hitTestChildren(result, position: position);
    }
    bool pass = hitTestSelf(position) || (hitTestDownResult && size.contains(position));
    if (pass) {
    
    
      result.add(BoxHitTestEntry(this, position));
    } 
    return !up && pass;
  }

  
  bool hitTestSelf(Offset position) => self;
}

We can use HitTestBlockerdirect replacement IgnorePointerto realize that all subcomponents can respond to events. The code is as follows:


Widget build(BuildContext context) {
    
    
  return Stack(
    children: [
      // IgnorePointer(child: wChild(1, 200)),
      // IgnorePointer(child: wChild(2, 200)),
      HitTestBlocker(child: wChild(1, 200)),
      HitTestBlocker(child: wChild(2, 200)),
    ],
  );
}

After clicking, the console will output 2and at the same time 1, the principle is also very simple:

  1. HitTestBlockerwill hitTestreturn false, which can ensure Stackthat all child nodes of can participate in the hit test;

  2. HitTestBlockerhitTestwill be called in again , hitTestChildrenso HitTestBlockerthe descendant nodes of will have the opportunity to participate in the hit test, so Containerthe events on will be triggered normally.

HitTestBlockeris a very flexible class, it can intercept each stage of the hit test, and HitTestBlockercan fully realize the functions of IgnorePointerand through, for example, when both of and are , the function is the same as that of .AbsorbPointerHitTestBlockerupdowntrueIgnorePointer

4. The presence of gestures

Let's modify the above code a little bit, Listenerreplace with GestureDetector, the code is as follows:

class GestureHitTestBlockerTest extends StatelessWidget {
    
    
  const GestureHitTestBlockerTest({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Stack(
      children: [
        HitTestBlocker(child: wChild(1, 200)),
        HitTestBlocker(child: wChild(2, 200)),
      ],
    );
  }

  Widget wChild(int index, double size) {
    
    
    return GestureDetector( // 将 Listener 换为 GestureDetector
      onTap: () => print('$index'),
      child: Container(
        width: size,
        height: size,
        color: Colors.grey,
      ),
    );
  }
}

Can you guess what will be output after clicking? The answer is only output 2! This is because although Stackboth subcomponents of will participate and pass the hit test, GestureDetectorwill decide whether to respond to the event in the event distribution phase (rather than the hit test phase), and GestureDetectorhave a separate mechanism for handling gesture conflicts, which we will introduce below .

Summarize:

  1. Components can only respond to events if they pass the hit test.
  2. Whether a component passes the hit test depends on hitTestChildren(...) || hitTestSelf(...)the value of .
  3. The hit testing order of components in the component tree is depth-first.
  4. The order of the component child node hit test is reversed, and once a child node is hitTestreturned true, the traversal will be terminated, and subsequent child nodes will not have the opportunity to participate in the hit test. This principle can Stackbe understood in conjunction with components.
  5. In most cases the effect Listenerof HitTestBehavioris opaqueor translucentis the same, only when its child node's hitTestis returned as falseis the difference.
  6. HitTestBlockeris a very flexible component through which we can intervene in various stages of hit testing.

Gesture principle and gesture conflict

Principle of Gesture Recognition

Gesture recognition and processing are all in the event distribution stage, GestureDetectorwhich is one StatelessWidget, including RawGestureDetector, let's look at its buildmethod implementation:


Widget build(BuildContext context) {
    
    
  final  gestures = <Type, GestureRecognizerFactory>{
    
    };
  // 构建 TapGestureRecognizer 
  if (onTapDown != null ||
      onTapUp != null ||
      onTap != null ||
      ... //省略
  ) {
    
    
    gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
      () => TapGestureRecognizer(debugOwner: this),
      (TapGestureRecognizer instance) {
    
    
        instance
          ..onTapDown = onTapDown
          ..onTapUp = onTapUp
          ..onTap = onTap
          //省略
      },
    );
  }
  
  return RawGestureDetector(
    gestures: gestures, // 传入手势识别器
    behavior: behavior, // 同 Listener 中的 HitTestBehavior
    child: child,
  );
}

Note that we deleted a lot of code above, and only kept TapGestureRecognizerthe relevant code (click gesture recognizer). Let's take click gesture recognition as an example to talk about the whole process. The event will be monitored RawGestureDetectorthrough the component , and the relevant source code is as follows:ListenerPointerDownEvent


Widget build(BuildContext context) {
    
    
  ... // 省略无关代码
  Widget result = Listener(
    onPointerDown: _handlePointerDown,
    behavior: widget.behavior ?? _defaultBehavior,
    child: widget.child,
  );
}  
 
void _handlePointerDown(PointerDownEvent event) {
    
    
  for (final GestureRecognizer recognizer in _recognizers!.values)
    recognizer.addPointer(event);
}  

Let's take a look at TapGestureRecognizerseveral related methods of , because of TapGestureRecognizerthe multi-layer inheritance relationship, here is a simplified version:

class CustomTapGestureRecognizer1 extends TapGestureRecognizer {
    
    

  void addPointer(PointerDownEvent event) {
    
    
    //会将 handleEvent 回调添加到 pointerRouter 中
    GestureBinding.instance!.pointerRouter.addRoute(event.pointer, handleEvent);
  }
  
  
  void handleEvent(PointerEvent event) {
    
    
    //会进行手势识别,并决定是是调用 acceptGesture 还是 rejectGesture,
  }
  
  
  void acceptGesture(int pointer) {
    
    
    // 竞争胜出会调用
  }

  
  void rejectGesture(int pointer) {
    
    
    // 竞争失败会调用
  }
}

It can be seen that when PointerDownEventthe event is triggered, it will be called TapGestureRecognizer, addPointerand the method addPointerwill be added to and saved in . In this way, when the gesture changes, you only need to take out the method for gesture recognition.handleEventpointerRouterpointerRouterGestureRecognizerhandleEvent

Under normal circumstances, the object directly affected by the gesture should handle the gesture, so a simple principle is that only one gesture recognizer should be effective for the same gesture. For this reason, gesture recognition reflects the concept of the gesture arena (Arena ) . simply speaking:

  1. Each gesture recognizer ( GestureRecognizer) is a "competitor" ( GestureArenaMember). When a pointer event occurs, they must compete in the "arena" for the right to handle the event. By default, only one "competitor" will eventually win (win).

  2. GestureRecognizerGestures are recognized in the handleEvent, and if a gesture occurs, competitors can announce whether they have won, and once a competitor wins, the arena manager ( ) GestureArenaManagerwill notify the other competitors that they have lost.

  3. The winner's acceptGesturewill be called, and the rest rejectGesturewill be called.

In the previous section we said that the hit test starts from RenderBindingthe hitTest:


void hitTest(HitTestResult result, Offset position) {
    
    
  // 从根节点开始进行命中测试
  renderView.hitTest(result, position: position); 
  // 会调用 GestureBinding 中的 hitTest()方法 
  super.hitTest(result, position); 
}

GestureBindingThe method in is called after the render tree hit test is complete hitTest():

 // from HitTestable
void hitTest(HitTestResult result, Offset position) {
    
    
  result.add(HitTestEntry(this));
}

It's very simple, GestureBindingand it has passed the hit test. In this way, in the event distribution phase, GestureBindingwill handleEventalso be called. Since it is added to at the end HitTestResult, in the event distribution phase GestureBinding, handleEventwill be called at the end:

 
void handleEvent(PointerEvent event, HitTestEntry entry) {
    
    
  // 会调用在 pointerRouter 中添加的 GestureRecognizer 的 handleEvent
  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);
  }
}

gestureArenaIs GestureArenaManagerthe class instance responsible for managing the arena.

The key code above is the first line, the function will call the previously added in , pointerRouterdifferent will recognize different gestures, and then it will interact with (if the current wins, you need to notify other competitors that they have failed), In the end, if the current wins, its will be called eventually, and if it fails, its will be called, because this part of the code will be different , just know what it does, if you are interested, you can check the source code yourself.GestureRecognizerhandleEventGestureRecognizerhandleEventgestureArenaGestureRecognizergestureArenaGestureRecognizeracceptGesturerejectGestureGestureRecognizer

gesture competition

If a component is monitored for horizontal and vertical drag gestures at the same time, which direction of the drag gesture callback will be triggered when we drag obliquely? In fact, it depends on the displacement components on the two axes during the first movement, whichever axis is larger, which axis will win the competition in this sliding event. As mentioned above, each gesture recognizer ( GestureRecognizer) is a "competitor" ( GestureArenaMember). When a pointer event occurs, they all compete in the "arena" for the right to handle this event. By default, there is only one The "competitor" will win (win).

For example, suppose there is one ListView, and its first child component is also ListView. If you slide this child now ListView, ListViewwill the parent move? The answer is no, only the child ListViewwill move at this time, because the child ListViewwill win at this time and get the right to handle the sliding event.

Let's look at a simple example:

GestureDetector( // GestureDetector2
  onTapUp: (x)=>print("2"), // 监听父组件 tapUp 手势
  child: Container(
    width:200,
    height: 200,
    color: Colors.red,
    alignment: Alignment.center,
    child: GestureDetector( //GestureDetector1
      onTapUp: (x)=>print("1"), // 监听子组件 tapUp 手势
      child: Container(
        width: 50,
        height: 50,
        color: Colors.grey,
      ),
    ),
  ),
);

When we click on the subcomponent (gray area), the console will only print " 1", but not " 2". This is because after the finger is lifted, there will be a competition GestureDetector1with GestureDetector2. The winning rule is " subcomponent first " , so GestureDetector1wins, since only one "competitor" can win, so GestureDetector2 is ignored. The way to resolve the conflict in this example is very simple, just GestureDetectorreplace with Listener, the specific reason will be explained later.

Let's look at another example, let's take the drag gesture as an example, and recognize the drag gesture in the horizontal and vertical directions at the same time. When the user presses the finger, it will trigger a competition (horizontal direction and vertical direction), once a certain direction "wins" , it will move in that direction until the current drag gesture ends. code show as below:

class _BothDirectionTest extends StatefulWidget {
    
    
  
  _BothDirectionTestState createState() => _BothDirectionTestState();
}

class _BothDirectionTestState extends State<_BothDirectionTest> {
    
    
  double _top = 0.0;
  double _left = 0.0;

  
  Widget build(BuildContext context) {
    
    
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          left: _left,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")),
            //垂直方向拖动事件
            onVerticalDragUpdate: (DragUpdateDetails details) {
    
    
              setState(() {
    
    
                _top += details.delta.dy;
              });
            },
            onHorizontalDragUpdate: (DragUpdateDetails details) {
    
    
              setState(() {
    
    
                _left += details.delta.dx;
              });
            },
          ),
        )
      ],
    );
  }
}

After this example runs, each drag will only move in one direction (horizontal or vertical), and the competition occurs when the finger is pressed for the first time (move), the specific "winning" condition in this example is: When the first move The one with the larger components of the displacement in the horizontal and vertical directions wins.

Multiple gesture conflicts

Since there is only one winner in the gesture competition, when we GestureDetectormonitor multiple gestures through one, conflicts may also occur. Suppose there is one widget, it can be dragged left and right, now we also want to detect the event of finger pressing and lifting on it, the code is as follows:

class GestureConflictTestRouteState extends State<GestureConflictTestRoute> {
    
    
  double _left = 0.0;
  
  Widget build(BuildContext context) {
    
    
    return Stack(
      children: <Widget>[
        Positioned(
          left: _left,
          child: GestureDetector(
              child: CircleAvatar(child: Text("A")), // 要拖动和点击的widget
              onHorizontalDragUpdate: (DragUpdateDetails details) {
    
    
                setState(() {
    
    
                  _left += details.delta.dx;
                });
              },
              onHorizontalDragEnd: (details){
    
    
                print("onHorizontalDragEnd");
              },
              onTapDown: (details){
    
    
                print("down");
              },
              onTapUp: (details){
    
    
                print("up");
              },
          ),
        )
      ],
    );
  }
}

Now we press and hold the circle "A" to drag and then lift up the finger, the console log is as follows:

I/flutter (17539): down
I/flutter (17539): onHorizontalDragEnd

We found that " up" is not printed, this is because when dragging, when the finger is just pressed and there is no movement, the drag gesture does not have complete semantics. At this time, the TapDowngesture wins (win), and " down" is printed at this time, while the drag When moving, the dragging gesture will win, and when the finger is lifted, there is a conflict onHorizontalDragEndwith and onTapUp, but because it is in the semantics of dragging, it onHorizontalDragEndwins, so " onHorizontalDragEnd" will be printed.

If our code logic is strongly dependent on finger pressing and lifting, for example, in a carousel component, we want to pause the carousel when the finger is pressed, and resume the carousel when it is lifted, but due to the The image broadcast component itself may have handled the drag gesture (such as supporting manual sliding switching), and may even support the zoom gesture. At this time, it will not work if we use it externally to onTapDownmonitor onTapUp. What should we do at this time? In fact, it is very simple, just by Listenerlistening to the original pointer event:

Positioned(
  top:80.0,
  left: _leftB,
  child: Listener(
    onPointerDown: (details) {
    
    
      print("down");
    },
    onPointerUp: (details) {
    
    
      //会触发
      print("up");
    },
    child: GestureDetector(
      child: CircleAvatar(child: Text("B")),
      onHorizontalDragUpdate: (DragUpdateDetails details) {
    
    
        setState(() {
    
    
          _leftB += details.delta.dx;
        });
      },
      onHorizontalDragEnd: (details) {
    
    
        print("onHorizontalDragEnd");
      },
    ),
  ),
)

Resolve Gesture Conflicts

Gestures are the semantic recognition of raw pointers. Gesture conflicts are only at the gesture level, that is to say, there will only be conflicts between multiple in the component tree . If they are not used at all , there will be no so-called conflicts. Because every node can receive the eventGestureDetectorGestureDetector , just in GestureDetectororder to identify the semantics, it will decide which child nodes should ignore the event and which nodes should take effect.

There are two ways to resolve gesture conflicts:

  1. use Listener. This is equivalent to jumping out of the set of rules for gesture recognition.

  2. Custom gesture gesture recognizer ( Recognizer).

1. Resolve gesture conflicts through Listener

ListenerThe reason for resolving gesture conflicts is that the competition is only for semantic gestures , but Listenerfor listening to raw pointer events . Raw pointer events are not semantic gestures, so the logic of gesture competition will not be followed at all, so they will not affect each other. Taking the above two Containernested examples as an example, the adopted Listenersolution is:

Listener(  // 将 GestureDetector 换为 Listener 即可
  onPointerUp: (x) => print("2"),
  child: Container(
    width: 200,
    height: 200,
    color: Colors.red,
    alignment: Alignment.center,
    child: GestureDetector(
      onTap: () => print("1"),
      child: Container(
        width: 50,
        height: 50,
        color: Colors.grey,
      ),
    ),
  ),
);

The code is very simple, just GestureDetectorreplace with Listener, you can change both, or just one. It can be seen that Listenerit is very simple to resolve conflicts by directly identifying raw pointer events. Therefore, when encountering gesture conflicts, we should give priority to them Listener.

2. Resolve gesture conflicts by customizing the Recognizer

The way to customize the gesture recognizer is more troublesome. The principle is that when the winner of the gesture competition is determined, the acceptGesturemethod of the winner will be called to indicate "declare success", and then the method of other gesture recognition will be called rejectGestureto indicate "declare failure" .

In this case, we can customize the gesture recognizer ( Recognizer), and then rewrite its method: call the method rejectGestureinsideacceptGesture , which is equivalent to forcing it to become a successful competitor when it fails, so that its callback will also execute. (It is a relatively rogue approach)

Let's first customize tapthe gesture recognizer ( Recognizer):

class CustomTapGestureRecognizer extends TapGestureRecognizer {
    
    
  
  void rejectGesture(int pointer) {
    
     
    //super.rejectGesture(pointer); // 不,我不要失败,我要成功 
    super.acceptGesture(pointer);   // 宣布成功
  }
}

// 创建一个新的GestureDetector,用我们自定义的 CustomTapGestureRecognizer 替换默认的
RawGestureDetector customGestureDetector({
    
    
  GestureTapCallback? onTap,
  GestureTapDownCallback? onTapDown,
  Widget? child,
}) {
    
    
  return RawGestureDetector(
    child: child,
    gestures: {
    
    
      CustomTapGestureRecognizer:
          GestureRecognizerFactoryWithHandlers<CustomTapGestureRecognizer>(
        () => CustomTapGestureRecognizer(),
        (detector) {
    
    
          detector.onTap = onTap;
        },
      )
    },
  );
}

We use RawGestureDetectorto customize customGestureDetector, GestureDetectorand we also use RawGestureDetectorto package various Recognizerto achieve, we need to customize whichever one Recognizer, just add whichever one.

Now let's look at modifying the calling code:

customGestureDetector( // 替换 GestureDetector
  onTap: () => print("2"),
  child: Container(
    width: 200,
    height: 200,
    color: Colors.red,
    alignment: Alignment.center,
    child: GestureDetector(
      onTap: () => print("1"),
      child: Container(
        width: 50,
        height: 50,
        color: Colors.grey,
      ),
    ),
  ),
);

This is OK. It should be noted that this example also shows that there can be multiple winners in a gesture processing process.

Gesture source code analysis

Specifically, there are two problems that Flutter needs to solve: one is how to determine the Widget that handles gestures (accurately speaking RenderObject); the other is to determine which gestures to respond to, the most typical one is the distinction between single click and double click.

The key classes and their relationships related to Flutter gesture processing are shown in the figure:

insert image description here

In Figure 8-7, GestureDetectorit is the entry point for developers to respond to gesture events, and the Widgetcorresponding underlying drawing node ( RenderObject) is RenderPointerListener, this class indirectly implements HitTestTargetthe interface, that is, this class is a target that can be tested by clicking. By implementing HitTestTargetmethods handleEvent, RenderPointerListenerwill participate in _GestureArenagesture competitions within the gesture arena ( ).

Specifically, during the creation RenderPointerListenerprocess, the RawGestureDetectorStatecorresponding GestureRecognizerinstance will be created according to the callback parameters provided by the developer, GestureRecognizerand inherited from GestureArenaMember, this class is _GestureArenaheld, which is a unified abstract representation of gesture competition.

GestureRecognizerThere are many subcategories, see Figure 8-8 for details. Among them, OneSequenceGestureRecognizerit is the most frequently contacted by developers GestureRecognizer.

_GestureArenaResponsible for managing each member in a gesture arena ( GestureArenaMember), GestureArenaManagerresponsible for managing all gesture arenas. Therefore, GestureArenaManageronly one instance of the global is required, GestureBindingheld by . GestureBindingAt the same time, it is also the entry point for processing gesture events sent by Engine. It _hitTestsholds the click test result ( ) of a gesture event through the field HitTestResult. Each click test result is actually HitTestEntrya list of objects, corresponding to one-to-one, HitTestEntryand HitTestTargetthe latter is exactly the previous mentioned RenderPointerListener. GestureDetectorIn this way, the closed loop from the UI element ( ) to the gesture competition model ( GestureArenaMemberetc.) is completed .

insert image description here

GestureRecognizerIt is the base class of all gesture processors. As can be seen from Figure 8-7, it is GestureRecognizerinherited from GestureArenaMemberand will be used as the basic unit of gesture competition. GestureRecognizerThere are many subclasses, and each subclass is responsible for realizing the identification of corresponding events. OneSequenceGestureRecognizerRepresents one-time gestures, such as click ( TapGestureRecognizer), long press ( LongPressGestureRecognizer), drag ( DragGestureRecognizer), etc.; double-click ( DoubleTapGestureRecognizer) is a very special gesture event, which will be analyzed in detail later; MultiDragGestureRecognizerit represents more complex gesture events (such as pinch zoom) , this article will not do an in-depth analysis.

Gesture processing is mainly divided into two stages: the first stage is HitTestTargetthe collection of targets ( ); the second stage is the competition of gestures. The following are analyzed in turn.

target collection

The click event is generated by Embedder, converted by Engine into unified data and handed over to Flutter for processing. The processing entry in Framework is as _handlePointerEventImmediatelyshown in Listing 8-45.

// 代码清单8-45 flutter/packages/flutter/lib/src/gestures/binding.dart
void _handlePointerEventImmediately(PointerEvent event) {
    
     // GestureBinding
  HitTestResult? hitTestResult;
  if (event is PointerDownEvent || event is PointerSignalEvent || event is 
      PointerHoverEvent) {
    
    
    hitTestResult = HitTestResult(); // 第1类事件,开始形成一个手势的事件类型
    hitTest(hitTestResult, event.position); // 单击测试,即收集那些可以响应本次单击的实例
    if (event is PointerDownEvent) {
    
     // PointerDown类型的事件,通常是一个手势的开始
      _hitTests[event.pointer] = hitTestResult; // 存储单击测试结果,以备后续使用
    }
  } else if (event is PointerUpEvent || event is PointerCancelEvent) {
    
     
	// 第2类事件,根据event pointer取得第1类事件获得的Hit TestResult,并将其移除
    hitTestResult = _hitTests.remove(event.pointer); 
	// 接收到手势结束的事件,移除本次结果
  } else if (event.down) {
    
     // 第3类事件,其他处于down类型的事件,如滑动、鼠标拖曳等
    hitTestResult = _hitTests[event.pointer]; // 取出形成手势时存储的单击测试结果
  }
  if (hitTestResult != null || event is PointerAddedEvent || event is Pointer RemovedEvent) {
    
    
    dispatchEvent(event, hitTestResult); // 向可响应手势的集合分发本次事件,见代码清单8-47
  }
}

The above logic adopts different strategies for different events. For the first type of event, it will try to collect a list of click test results ( HitTestResultfields path), recording which objects currently respond to this click. For the second type of event, it will be directly event.pointerobtained by taking out the first type of event HitTestResultand removing it. For the third type of event, it is considered to be an intermediate state of the first two types, and the click test result can be directly taken out and used.

For the case of hitTestResultnot doing so null, it will try to distribute the event, which will be described in detail later. Here, first analyze hitTestthe logic of the method. Among them, the implementation of GestureBinding、RendererBinding、RenderViewand RenderBoxis particularly critical, as shown in Listing 8-46.

// 代码清单8-46 flutter/packages/flutter/lib/src/gestures/binding.dart
 // GestureBinding
void hitTest(HitTestResult result, Offset position) {
    
    
  result.add(HitTestEntry(this));
}
 // RendererBinding
void hitTest(HitTestResult result, Offset position) {
    
    
  renderView.hitTest(result, position: position); // 触发Render Tree的根节点
  super.hitTest(result, position); // 将导致执行GestureBinding的hitTest方法
}
bool hitTest(HitTestResult result, {
    
     required Offset position }) {
    
     // RenderView
  if (child != null) // Render Tree的根节点将触发子节点的hitTest方法
    child!.hitTest(BoxHitTestResult.wrap(result), position: position);
  result.add(HitTestEntry(this)); // 最后将自身加入单击测试结果
  return true;
}
bool hitTest(BoxHitTestResult result, {
    
     required Offset position }) {
    
     // RenderBox
  if (_size!.contains(position)) {
    
     // 单击位置是否在当前Layout的范围内,这是必要条件
    if (hitTestChildren(result, position: position) || // 子节点通过了单击测试
   hitTestSelf(position)) {
    
     // 自身通过了单击测试,这是充分条件
      result.add(BoxHitTestEntry(this, position)); // 生成一个单击测试入口,加入结果
      return true;
    }
  }
  return false;
}

Considering the inheritance relationship, RendererBindingit hitTestwill be executed first, and its logic is mainly the method renderViewof execution. As the root node of the Render Tree , it will traverse each node to perform a single-click test. The most typical method is to recursively test each child node and Do a click test by itself, and then join the queue one by one. Note that it will always be added to the queue as the last element, which is very critical for subsequent gesture competition.hitTestrenderViewRenderBoxhitTestGestureBinding

Note that in the above logic, the click position within RenderBoxthe Layoutrange is not a sufficient condition for adding the click test result, and usually requires its own hitTestSelfmethod to return true, which RenderBoxprovides an entry for subclasses to freely decide whether to participate in subsequent gesture competition.

gesture competition

After obtaining all the objects that can respond to the click (stored in HitTestResult), the method GestureBindingwill be triggered dispatchEventto complete the distribution of this event. The logic is shown in Listing 8-47.

// 代码清单8-47 flutter/packages/flutter/lib/src/gestures/binding.dart
 // GestureBinding
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
    
    
  if (hitTestResult == null) {
    
     // 说明是PointerHoverEvent、PointerAddedEvent
    try {
    
     // 或者PointerRemovedEvent,在此统一路由分发,其他情况通过handleEvent方法处理
      pointerRouter.route(event);
    } catch (exception, stack) {
    
     ...... }
    return;
  }
  for (final HitTestEntry entry in hitTestResult.path) {
    
     
    try {
    
    
      entry.target.handleEvent(event.transformed(entry.transform), entry);
    } catch (exception, stack) {
    
     ...... }
  }
}

For the case of hitTestResultnot doing so , the method of each object nullwill be called in turn , and the subclass that needs to handle the gesture can participate in the gesture competition by implementing this method, and process the gesture after winning the competition.HitTestTargethandleEventHitTestTarget

Commonly GestureDetectorused internally in daily development RenderPointerListener, this class implements handleEventthe method and undertakes the distribution of gestures. In addition GestureBinding, as the core dispatching class and the last one of gestures HitTestTarget, this class is also implemented, as shown in the code listing 8-48.

// 代码清单8-48 flutter/packages/flutter/lib/src/rendering/proxy_box.dart
 // from RenderPointerListener
void handleEvent(PointerEvent event, HitTestEntry entry) {
    
    
  assert(debugHandleEvent(event, entry));
  if (event is PointerDownEvent)
    return onPointerDown?.call(event); // 最终在代码清单8-49中进行调用
  if (event is PointerMoveEvent)
  return onPointerMove?.call(event);
  // SKIP PointerUpEvent、PointerCancelEvent等事件
}
 // from GestureBinding
void handleEvent(PointerEvent event, HitTestEntry entry) {
    
    
  pointerRouter.route(event); // 无条件路由给已注册成员,注册逻辑见代码清单8-54
  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); // 解析
  }
}

GestureBindingContains an important member - gestureArena, which is responsible for managing all gesture competitions, called the gesture arena . The generation and registration of gesture arena members ( ) RenderPointerListenerwill be completed in its own process, and then the common click event will be used as an example for analysis.handleEventGestureArenaMember

In the above logic, it is introduced when the object onPointerDownis created , and its essence is a method, as shown in Listing 8-49.ListenerRawGestureDetectorState

// 代码清单8-49 flutter/packages/flutter/lib/src/widgets/gesture_detector.dart
void _handlePointerDown(PointerDownEvent event) {
    
     // RawGestureDetectorState
  for (final GestureRecognizer recognizer in _recognizers!.values)
    recognizer.addPointer(event); // 成功将事件从HitTestTarget传递到GestureRecognizer
}
void addPointer(PointerDownEvent event) {
    
     // GestureRecognizer
  _pointerToKind[event.pointer] = event.kind;
  if (isPointerAllowed(event)) {
    
     // 通常为true
    addAllowedPointer(event);
  } else {
    
    
    handleNonAllowedPointer(event);
  }
}

void addAllowedPointer(PointerDownEvent event) {
    
     } // 子类实现

GestureDetectoronTapVarious parameters such as , , etc. are provided onDoubleTap, which will be converted into GestureRecognizervarious subclasses of and added to _recognizersthe field.

A single click ( onTap) event can be divided into one PointerDownEventand one PointerUpEvent, PointerDownEventwhich will trigger the above logic, and onTapthe corresponding gesture recognition class is TapGestureRecognizer, which addAllowedPointerwill eventually startTrackingPointercall _addPointerToArenathe method, as shown in Listing 8-50.

// 代码清单8-50 flutter/packages/flutter/lib/src/gestures/recognizer.dart
GestureArenaEntry _addPointerToArena(int pointer) {
    
     // OneSequenceGestureRecognizer
  if (_team != null)  // 当前recognizer隶属于某一GestureArenaTeam对象
    return _team!.add(pointer, this); // 暂不考虑这种情况
  return GestureBinding.instance!.gestureArena.add(pointer, this); // 加入手势竞技场
}

There is only one member method that is finally called by the above logic, GestureBindingand its specific logic is shown in Listing 8-51.gestureArenaaddgestureArena

// 代码清单8-51 flutter/packages/flutter/lib/src/gestures/arena.dart
GestureArenaEntry add(int pointer, GestureArenaMember member) {
    
     // GestureArenaManager
  final _GestureArena state = _arenas.putIfAbsent(pointer, () {
    
    
    return _GestureArena(); // 产生一个手势竞技场
  });
  state.add(member); // 加入当前手势竞技场
  return GestureArenaEntry._(this, pointer, member);
}

_GestureArenaThe instance represents a specific arena. If it does not exist currently, a new one will be created, and then the current will be GestureArenaMemberadded. If multiple nested uses are used GestureDetector, then multiple will be added in sequence GestureRecognizer.

Regardless of the previous logic, the last logic is called GestureBindingbecause handleEventit was the last to be added to the clicktestresults ( HitTestResult) list, as shown in Listing 8-48. If it is PointerDownEventan event, the arena will be closed, because the previous addition work HitTestTargethas been completed GestureArenaMember; if it is PointerUpEventan event, the arena will be cleared, because the gesture has ended at this time (the special case of double-click event will be analyzed later).

This stage needs to solve a key problem - when there are more than one GestureArenaMember(usually TapGestureRecognizer), who will respond.

First analyze the closure of the arena, as shown in Listing 8-52.

// 代码清单8-52 flutter/packages/flutter/lib/src/gestures/arena.dart
void close(int pointer) {
    
     // GestureArenaManager,关闭pointer对应的手势竞技场
  final _GestureArena? state = _arenas[pointer];
  if (state == null) return;
  state.isOpen = false; // 标记关闭
  _tryToResolveArena(pointer, state); // 决出竞技场内的胜者
}
void _tryToResolveArena(int pointer, _GestureArena state) {
    
    
  if (state.members.length == 1) {
    
     // 只有一个成员,直接决出胜者
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  } else if (state.members.isEmpty) {
    
     // 没有成员,移除当前手势竞技场
    _arenas.remove(pointer);
  } else if (state.eagerWinner != null) {
    
     // 存在eagerWinner,定为胜者
    _resolveInFavorOf(pointer, state, state.eagerWinner!);
  }
}
void _resolveByDefault(int pointer, _GestureArena state) {
    
    
  if (!_arenas.containsKey(pointer)) return; // 已被移除
  final List<GestureArenaMember> members = state.members;
  state.members.first.acceptGesture(pointer); // 直接取第1个成员作为胜者
}

The above logic is mainly the closing of the gesture arena. During the closing phase, an attempt will be made to determine the winner of the gesture arena. Take Case1 in Figure 8-9 as an example, A responds to the click event as the winner in the closing stage of the arena. As for Case2 and Case3, they will be analyzed in detail later.

insert image description here

When the arena is closed, PointerUpthe gesture arena will be cleaned up when the event arrives, as shown in Listing 8-53.

// 代码清单8-53 flutter/packages/flutter/lib/src/gestures/arena.dart
void sweep(int pointer) {
    
     // GestureArenaManager,清理pointer对应的手势竞技场
  final _GestureArena? state = _arenas[pointer];
  if (state == null) return; // 已移除,避免重复处理
  if (state.isHeld) {
    
     // 被挂起,直接返回
    state.hasPendingSweep = true;
    return;
  }
  _arenas.remove(pointer); // 移除pointer对应的手势竞技场
  if (state.members.isNotEmpty) {
    
    
    state.members.first.acceptGesture(pointer); // 取第1个成员作为胜者,与_resolve
                                                // ByDefault一致
    for (int i = 1; i < state.members.length; i++)
      state.members[i].rejectGesture(pointer); // 触发败者的rejectGesture方法
  }
}

The above logic is mainly to clean up the gesture arena, and return directly if it is suspended. For the case of not being suspended, we can see from the analysis of code list 8-46 that the first element, which is the RenderBoxinnermost element of , will be taken, which is in line with our development experience.

In the case of only one member, the Gesture Arena will closedecide the winner directly in the stage, while if there are multiple members, the Gesture Arena will sweeptake the first member in the stage (if it is not suspended) as the winner.

double click event

From the analysis so far, it can be found that the above logic can be completely handled for ordinary single-click events, but what about double-click events? According to the above logic, before the second click starts, the first click is treated as a single click event. The mystery of the solution is in isHeldthe field, and the detailed analysis will begin below.

From Code Listing 8-49, we can see that the double-click event will trigger DoubleTapGestureRecognizerthe addAllowedPointermethod, which will call _trackTapthe method, as shown in Code Listing 8-54.

// 代码清单8-54 flutter/packages/flutter/lib/src/gestures/multitap.dart
 // DoubleTapGestureRecognizer
void addAllowedPointer(PointerDownEvent event) {
    
    
  if (_firstTap != null) {
    
     // 已经记录了一次单击,即当前为第2次单击,接下来进入以下逻辑 
    if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) {
    
    
      return; // 超时,不认为是双击
    } else if (!_firstTap!.hasElapsedMinTime() || !_firstTap!.hasSameButton(event)) {
    
    
      _reset(); // 在短时间(kDoubleTapMinTime)内单击相同位置认为是单击,重置
      return _trackTap(event); // 重新追踪
    } else if (onDoubleTapDown != null) {
    
     // 认为是双击,触发对应回调onDoubleTapDown
      final TapDownDetails details = TapDownDetails( ...... ); // 注意区别于onDoubleTap
      invokeCallback<void>('onDoubleTapDown', () => onDoubleTapDown!(details));
    }
  } // if
  _trackTap(event); // 对于首次单击,直接开始追踪,主要逻辑是挂起竞技场
}
void _trackTap(PointerDownEvent event) {
    
     // DoubleTapGestureRecognizer
  _stopDoubleTapTimer(); // 见代码清单8-57
  final _TapTracker tracker = _TapTracker( // 开始追踪单击事件
    event: event, // 触发本次单击事件的PointerDown事件
    // 加入竞技场,封装一个GestureArenaEntry对象并返回
    entry: GestureBinding.instance!.gestureArena.add(event.pointer, this),
    doubleTapMinTime: kDoubleTapMinTime, // 双击的最小时间间隔,默认为40ms
  );
  _trackers[event.pointer] = tracker;
  tracker.startTrackingPointer(_handleEvent, event.transform);  // 开始追踪,见代码清单8-56
}

The above logic adds the current gesture to the arena and adds a route to the current event _handleEvent. As can be seen from the code list 8-48, GestureBindingbefore the closing and cleaning logic of the gesture arena, it will be pointerRoutertriggered by routing the current event _handleEvent. The specific logic is shown in the code list 8-55.

The above logic is critical to implementing double clicks: logic that sweepfires prior to the stage prior to the end of the first click event to suspend the arena and avoid immediately deciding the winner._handleEvent

// 代码清单8-55 flutter/packages/flutter/lib/src/gestures/multitap.dart
void _handleEvent(PointerEvent event) {
    
     // DoubleTapGestureRecognizer
  final _TapTracker tracker = _trackers[event.pointer]!; 
// 找到代码清单8-54中的_TapTracker对象
  if (event is PointerUpEvent) {
    
    
    if (_firstTap == null) // 首次单击抬起时触发
      _registerFirstTap(tracker); // 见代码清单8-56
    else // 第2次单击抬起时触发
      _registerSecondTap(tracker); // 见代码清单8-56
  } else if (event is PointerMoveEvent) {
    
     // 移除PointerMove事件
    if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop))
      _reject(tracker); // 如果移动一段距离则不再认为是单击事件,这符合用户体验
  } else if (event is PointerCancelEvent) {
    
    
    _reject(tracker);
  }
}

It can be seen from the above code that because the first click is _firstTap, the logic of sum will be triggered nullrespectively when the first click ends and the second click ends , as shown in Code Listing 8-56._registerFirstTap_registerSecondTap

// 代码清单8-56 flutter/packages/flutter/lib/src/gestures/multitap.dart
void _registerFirstTap(_TapTracker tracker) {
    
     // 首次点击,触发DoubleTapGestureRecognizer
  _startDoubleTapTimer(); // 启动一个定时器,在一定时间后重置,见代码清单8-57
  GestureBinding.instance!.gestureArena.hold(tracker.pointer); // 挂起当前竞技场
  _freezeTracker(tracker); // 目标任务已触发,注销当前路由
  _trackers.remove(tracker.pointer); // 移除tracker
  _clearTrackers(); // 触发_trackers内其他tracker的_reject方法
  _firstTap = tracker; // 标记首次单击事件产生,作用于代码清单8-54
}
void _registerSecondTap(_TapTracker tracker) {
    
     // 第2次单击
  _firstTap!.entry.resolve(GestureDisposition.accepted); // 第1次单击的tracker,
                                                         // 见代码清单8-58
  tracker.entry.resolve(GestureDisposition.accepted); // 第2次单击的tracker
  _freezeTracker(tracker); // 清理掉第2次单击所注册的路由
  _trackers.remove(tracker.pointer); // 移除
  _checkUp(tracker.initialButtons); // 触发双击事件对应的回调
  _reset(); // 重置,将释放之前挂起的竞技场,见代码清单8-57
}
void _freezeTracker(_TapTracker tracker) {
    
    
  tracker.stopTrackingPointer(_handleEvent);
}
void _clearTrackers() {
    
    
  _trackers.values.toList().forEach(_reject);
}
void _checkUp(int buttons) {
    
     // 和前面介绍的onDoubleTapDown不同,此时胜者已经决出
  if (onDoubleTap != null) invokeCallback<void>('onDoubleTap', onDoubleTap!);
}
void startTrackingPointer(PointerRoute route, Matrix4? transform) {
    
     // _TapTracker
  if (!_isTrackingPointer) {
    
     // 避免重复注册
    _isTrackingPointer = true;
    GestureBinding.instance!.pointerRouter.addRoute(pointer, route, transform); // 注册路由
  } // 由代码清单8-48中GestureBinding的handleEvent方法可知 第2次单击将首先触发route参数,即_handleEvent方法
void stopTrackingPointer(PointerRoute route) {
    
     // _TapTracker
  if (_isTrackingPointer) {
    
    
    _isTrackingPointer = false;
    GestureBinding.instance!.pointerRouter.removeRoute(pointer, route); // 注销路由
  }
}

The above logic starts a timer when the first click event occurs , which is used to trigger the reset ( _resetmethod) logic after a certain amount of time . This is because if two consecutive clicks exceed a certain time interval, it is not counted as a double click, as shown in Listing 8-57.

_registerFirstTapThere are some other logic in , mainly to suspend the arena where the current gesture is located, because it has not yet been determined whether it is a double-click event, and also to _freezeTrackercomplete the logout of the route, because the responsibility of the current route (suspend the arena) has been completed . Generally speaking, the logic of routing logout is in the logic triggered by routing registration, which ensures that registration and logout occur in pairs.

_registerSecondTapIt will be triggered by routing in the second click and trigger the arena's winning logic. This part will be analyzed in detail later.

First analyze the logic of starting the timer, as shown in Listing 8-57.

// 代码清单8-57 flutter/packages/flutter/lib/src/gestures/multitap.dart
void _startDoubleTapTimer() {
    
     // DoubleTapGestureRecognizer
  _doubleTapTimer ??= Timer(kDoubleTapTimeout, _reset); // 双击最大间隔时间,默认为300ms 超过这一时间后将触发_reset
}  
void _reset() {
    
    
  _stopDoubleTapTimer(); // 重置定时器
  if (_firstTap != null) {
    
    
    if (_trackers.isNotEmpty) _checkCancel();
    final _TapTracker tracker = _firstTap!;
    _firstTap = null;
    _reject(tracker);
    GestureBinding.instance!.gestureArena.release(tracker.pointer); // 释放首次单击所在的竞技场
  }
  _clearTrackers();
}
void _stopDoubleTapTimer() {
    
    
  if (_doubleTapTimer != null) {
    
    
    _doubleTapTimer!.cancel();
    _doubleTapTimer = null;
  }
}

The above logic is very clear, mainly to _resetcomplete the cleaning and reset of relevant members through methods after timeout. Next, analyze the winning logic of the Gesture Arena, which is _registerSecondTaptriggered by , as shown in Listing 8-58.

// 代码清单8-58 flutter/packages/flutter/lib/src/gestures/arena.dart
void resolve(GestureDisposition disposition) {
    
     // GestureArenaEntry
  _arena._resolve(_pointer, _member, disposition);
}
void _resolve(int pointer, GestureArenaMember member, GestureDisposition 
    disposition) {
    
    
  final _GestureArena? state = _arenas[pointer];
  if (state == null) return; // 目标竞技场已被移除,说明已经完成决胜
  if (disposition == GestureDisposition.rejected) {
    
    
    state.members.remove(member);
    member.rejectGesture(pointer);
    if (!state.isOpen) _tryToResolveArena(pointer, state);
  } else {
    
    
    if (state.isOpen) {
    
     // 竞技场还处在开放状态,没有关闭,则设置eagerWinner
      state.eagerWinner ??= member; // 竞技场关闭时处理,见代码清单8-52
    } else {
    
     // 直接决出胜者
      _resolveInFavorOf(pointer, state, member); // 见代码清单8-59
    }
  } // if
}

Generally speaking, the arena has been closed but not cleaned up at this point, so _resolveInFavorOfthe logic will enter, as shown in Listing 8-59.

// 代码清单8-59 flutter/packages/flutter/lib/src/gestures/arena.dart
void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember 
    member) {
    
    
  _arenas.remove(pointer); // InFavorOf,即支持传入的参数member成为竞技场的胜者
  for (final GestureArenaMember rejectedMember in state.members) {
    
    
    if (rejectedMember != member) rejectedMember.rejectGesture(pointer);
  }
  member.acceptGesture(pointer); // 触发胜者处理响应手势的逻辑 acceptGesture方法由具体子类实现
}  

The above logic is mainly to trigger acceptGesturethe method of the winner in the arena. For DoubleTapGestureRecognizer, the acceptGesturemethod is empty, because the logic of responding to the double-click event has been _checkUptriggered by the method of code list 8-56.

Due to the particularity of its own logic, the double-click event is relatively obscure to analyze from the code. The following takes Case2 in Figure 8-9 as an example for analysis. For area B in Figure 8-9, when the first click event occurs, instances of , , and Tapwill DoubleTapjoin the arena, but since the arena is suspended in Listing 8-56, when the finger is lifted and the click ends The arena will not be cleared. At this time , the event can be confirmed as failed. When the second click occurs, the arena will be released, and at the same time, it will judge whether it meets the conditions (within the specified time), and if so, the corresponding callback will be triggered; if the first click is not satisfied, it will be executed with form trigger.DargGestureRecognizerDragDoubleTapTap

As can be seen, the core of the double-click event is the brief hang of the arena . So far, the double-click event analysis is complete.

Drag event and list sliding

DragDrag ( ) events are more common than double-click events . The first problem to be solved is how the drag event wins the competition with the click event, which can be solved by analyzing the TapGestureRecognizermethod handleEvent, which is in its parent class PrimaryPointerGestureRecognizer, as shown in Listing 8-60.

// 代码清单8-60 flutter/packages/flutter/lib/src/gestures/recognizer.dart
 // PrimaryPointerGestureRecognizer
void handleEvent(PointerEvent event) {
    
    
  if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
    
    
    final bool isPreAcceptSlopPastTolerance =
        !_gestureAccepted && // 当前Recognizer尚在竞争手势
        preAcceptSlopTolerance != null && // 判断为非单击的阈值,默认为18像素,下同
        _getGlobalDistance(event) > preAcceptSlopTolerance!;
    final bool isPostAcceptSlopPastTolerance =
        _gestureAccepted &&  // 当前Recognizer已经成为胜者,例如只有一个竞技场成员时
        postAcceptSlopTolerance != null && // 此时如果发现单击变滑动,则仍要拒绝
        _getGlobalDistance(event) > postAcceptSlopTolerance!;
    if (event is PointerMoveEvent &&
          (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
    
    
      resolve(GestureDisposition.rejected); // 如果一次单击中滑动距离超过阈值则拒绝
      stopTrackingPointer(primaryPointer!);
    } else {
    
    
      handlePrimaryPointer(event);
    }
  } // if
  stopTrackingIfPointerNoLongerDown(event);
}

It can be seen from the above logic that if a sliding ( ) event occurs Moveand moves a certain distance, it Tapwill refuse to process it. So, if you don't lift up immediately after clicking, but slide a certain distance, the click event will not happen (even if you don't set the logic to respond to the drag event).

The above logic has ensured that the click event Recognizerwill not compete with the drag event, so how does the drag event recognize and respond to the real drag gesture? Next, start the analysis, as shown in Listing 8-61.

// 代码清单8-61 flutter/packages/flutter/lib/src/gestures/recognizer.dart
 // DragGestureRecognizer
void handleEvent(PointerEvent event) {
    
    
  // SKIP 与速度相关的计算
  if (event is PointerMoveEvent) {
    
    
    if (event.buttons != _initialButtons) {
    
    
      _giveUpPointer(event.pointer);
      return;
    }
    if (_state == _DragState.accepted) {
    
     // 分支1:已经胜出
      _checkUpdate( // 直接更新拖曳信息
        sourceTimeStamp: event.timeStamp,
        delta: _getDeltaForDetails(event.localDelta),
        primaryDelta: _getPrimaryValueFromOffset(event.localDelta),
        globalPosition: event.position,
        localPosition: event.localPosition,
      );
    } else {
    
     // 分支2:未胜出,正常情况下会先触发本分支
      _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); // 接受当前GestureRecognizer
    }
  }
  // SKIP PointerUpEvent / PointerCancelEvent处理:调用_giveUpPointer
}
void _checkUpdate({
    
     ...... }) {
    
    
  assert(_initialButtons == kPrimaryButton);
  final DragUpdateDetails details = DragUpdateDetails( ...... );
  if (onUpdate != null) // 赋值,见代码清单8-62
    invokeCallback<void>('onUpdate', () => onUpdate!(details)); // 见代码清单8-63
}

globalDistanceMovedIn the above logic, when the finger is pressed and swipe for the first time, the drag gesture does not win, so it will enter branch 2 , and then judge whether it exceeds a certain threshold through the calculated size, that is, the distance that has been slid, and if it exceeds, then compete in the gesture won.

After the winner is determined, a callback _checkUpdateis triggered onUpdate, which PointerMoveencapsulates the details of the event into an DragUpdateDetailsobject and calls the function.

The scrolling of the list is based on this mechanism, and its key classes and their relationships are shown in Figure 8-10.

insert image description here

In Figure 8-10, ScrollableStateit is an ancestor node Viewportin the Widget Tree . As its name suggests, it is also the key to provide the ability to slide the list. Specifically, an instance is held ScrollableStateby the field , and it will generate a corresponding subclass according to the sliding direction , generally speaking ._gestureRecognizersGestureRecognizerFactoryGestureRecognizerDragGestureRecognizer

RenderViewportBaseDetermine the current scroll distance by offsetthe field, and then layout each child node, then the core problem becomes ScrollableStatehow to GestureRecognizerconvert the provided dragging information into the distance of the list in the sliding direction ( ViewportOffset) and trigger the layout update.

To answer this question, you need to understand a transitive relationship in Figure 8-10, that is, ScrollableStateScrollDragController( Dragsubclass of) → ScrollPositionWithSingleContext( ScrollActivityDelegateimplementation class of) , ScrollPositionWithSingleContextwhich is ViewportOffseta subclass of . In this way, the scrolling information can be converted into RenderViewportBasean offset value. Additionally, ViewportOffsetinherited from ChangeNotifier, it can RenderViewportBasenotify itself that the scroll distance has changed. In this way, the gesture event drives the sliding update of the list.

In Figure 8-10, the subclasses of and are responsible for implementing various sliding boundary effects, such as Clamping on the Android platform and Bouncing on the iOS platform ScrollPhysics. And by holding references to each other through fields , they are also a path for the interaction between the producer of the drag event and the ultimate consumer, but not as dedicated as the connected path. Represents the sliding context, that is, the class that actually produces the sliding effect.ScrollActivityScrollableStateScrollPosition_positioncontextScrollDragControllerScrollContext

The list is nested in ScrollableState, and this object will generate one RawGestureDetector, and its _gestureRecognizersmembers will generate corresponding gesture processors according to the current axis direction, as shown in the code list 8-62.

// 代码清单8-62 flutter/packages/flutter/lib/src/widgets/scrollable.dart
void setCanDrag(bool canDrag) {
    
     // ScrollableState
  if (canDrag == _lastCanDrag && (!canDrag || widget.axis == _lastAxisDirection))
    return;
  if (!canDrag) {
    
    
    _gestureRecognizers = const <Type, GestureRecognizerFactory>{
    
    };
    _handleDragCancel();
  } else {
    
    
    switch (widget.axis) {
    
    
      case Axis.vertical:
        _gestureRecognizers = <Type, GestureRecognizerFactory>{
    
    
          VerticalDragGestureRecognizer: 
              GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
            () => VerticalDragGestureRecognizer(),
            (VerticalDragGestureRecognizer instance) {
    
    
              instance
                ..onDown = _handleDragDown
                ..onStart = _handleDragStart
                ..onUpdate = _handleDragUpdate
                // SKIP 其他回调注册
            },
          ),
        };
        break;
      case Axis.horizontal: // SKIP,HorizontalDragGestureRecognizer的注册
    } // switch
  } // if
  _lastCanDrag = canDrag;
  _lastAxisDirection = widget.axis;
  if (_gestureDetectorKey.currentState != null)
    _gestureDetectorKey.currentState!.replaceGestureRecognizers(_gestureRecognizers);
}

The above logic is applyContentDimensionstriggered by the method. It can be seen from the code list 8-61 that when a new PointerMoveevent arrives, _handleDragUpdateit will respond to the movement event, and this method will call the method Dragof the object update. The specific logic is in ScrollDragControllerthe class, as shown in the code list 8-63.

// 代码清单8-63 flutter/packages/flutter/lib/src/widgets/scroll_activity.dart
 // ScrollDragController
void update(DragUpdateDetails details) {
    
     // 见代码清单8-61的_checkUpdate方法
  _lastDetails = details;
  double offset = details.primaryDelta!; // 本次在主轴方向滑动的距离
  if (offset != 0.0) {
    
    
    _lastNonStationaryTimestamp = details.sourceTimeStamp;
  }
  _maybeLoseMomentum(offset, details.sourceTimeStamp);
  offset = _adjustForScrollStartThreshold(offset, details.sourceTimeStamp);
  if (offset == 0.0) {
    
     return; }
  if (_reversed) offset = -offset; // 逆向滑动
  delegate.applyUserOffset(offset); // 见代码清单8-64
}

delegateThe specific implementation of is ScrollPositionWithSingleContext, and its applyUserOffsetlogic is shown in the code list 8-64.

// 代码清单8-64 flutter/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart
 // ScrollPositionWithSingleContext
void applyUserOffset(double delta) {
    
     // 更新滚动方向
  updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.
      reverse);
  setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta)); // 父类ScrollPosition实现
} // pixels是ViewportOffset的成员,表示当前拖曳事件在主轴方向导致的总偏移,即滑动距离
double setPixels(double newPixels) {
    
     //(ScrollPosition)更新滑动距离
  if (newPixels != pixels) {
    
    
    final double overscroll = applyBoundaryConditions(newPixels); // 计算滑动边缘距离
    final double oldPixels = pixels;
    _pixels = newPixels - overscroll; 
    // 注意,这里的_pixels不是直接根据newPixels进行更新
    if (_pixels != oldPixels) {
    
    
      notifyListeners(); // 通知列表更新,触发markNeedsLayout方法,见代码清单7-11
      didUpdateScrollPositionBy(pixels - oldPixels);
    }
    if (overscroll != 0.0) {
    
    
      didOverscrollBy(overscroll);
      return overscroll;
    }
  }
  return 0.0;
}
 // ScrollPhysics的子类通过实现本方法达到不同的边缘滑动效果
double applyBoundaryConditions(double value) {
    
    
  final double result = physics.applyBoundaryConditions(this, value);
  return result;
}

The above logic will update ViewportOffsetthe _pixelsfield, and RenderViewportBasewhen assigning its own offsetfield, it has been markNeedsLayoutadded as ViewportOffseta listener. It can be seen from the above logic that the method notifyListenerswill be triggered performLayoutto drive the update of the list layout.

Next, take Case3 in Figure 8-9 as an example for analysis. When the finger is pressed, the click event and Dragthe drag event will join the arena, and if the movement exceeds a certain threshold during this time, the drag event will win (see Listing 8-61), and the click event will be rejected (see Listing 8 -60).

So far, the analysis of drag event and list sliding mechanism has been completed.

event bus

In App, we often need a broadcast mechanism for cross-page event notification. For example, in an App that requires login, the page will pay attention to user login or logout events to perform some status updates. At this time, an event bus will be very useful. The event bus usually implements the subscriber mode. The subscriber mode includes two roles: publisher and subscriber. Events can be triggered and listened to through the event bus.

Below we implement a simple global event bus, we use the singleton mode, the code is as follows:

// 订阅者回调签名
typedef void EventCallback(arg);

class EventBus {
    
    
  //私有构造函数
  EventBus._internal();

  //保存单例
  static EventBus _singleton = EventBus._internal();

  //工厂构造函数
  factory EventBus()=> _singleton;

  //保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列
  final _emap = Map<Object, List<EventCallback>?>();

  //添加订阅者
  void on(eventName, EventCallback f) {
    
    
    _emap[eventName] ??=  <EventCallback>[];
    _emap[eventName]!.add(f);
  }

  //移除订阅者
  void off(eventName, [EventCallback? f]) {
    
    
    var list = _emap[eventName];
    if (eventName == null || list == null) return;
    if (f == null) {
    
    
      _emap[eventName] = null;
    } else {
    
    
      list.remove(f);
    }
  }

  //触发事件,事件触发后该事件所有订阅者会被调用
  void emit(eventName, [arg]) {
    
    
    var list = _emap[eventName];
    if (list == null) return;
    int len = list.length - 1;
    //反向遍历,防止订阅者在回调中移除自身带来的下标错位
    for (var i = len; i > -1; --i) {
    
    
      list[i](arg);
    }
  }
}


//定义一个top-level(全局)变量,页面引入该文件后可以直接使用bus
var bus = EventBus();

Example usage:

//页面A中
...
 //监听登录事件
bus.on("login", (arg) {
    
    
  // do something
});

//登录页B中
...
//登录成功后触发登录事件,页面A中订阅者会被调用
bus.emit("login", userInfo);

Note: The standard way to implement the singleton pattern in Dart is to use staticvariables + factory constructors , so that you can EventBus()always return the same instance. You should understand and master this method.

The event bus is usually used for state sharing between components, but there are also some special packages for state sharing between components, such as redux, mobx, and those introduced earlier Provider. For some simple applications, the event bus is sufficient to meet business needs. If you decide to use the state management package, you must think clearly whether it is really necessary for your app to prevent "simplification into complexity" and over-design.

Of course, you can choose to use the popular library event_bus on the pub.dev community as the event bus in actual production projects .

Notification Notification

Notification (Notification) is an important mechanism in Flutter. In the widget tree, each node can distribute notifications. The notification will be passed up the current node , and all parent nodes can NotificationListenerlisten to the notification. In Flutter, this mechanism of passing notifications from child to parent is called Notification Bubbling. Notification bubbling and user touch event bubbling are similar, but there is one difference: notification bubbling can be aborted, but user touch event cannot .

Note: The principle of notification bubbling is similar to that of browser event bubbling in web development. Events are transmitted layer by layer from the source. We can listen to notifications/events at any position on the upper node, or terminate the bubbling process. After bubbling is terminated, the notification will no longer be passed upwards.

Listen for notifications

Notifications are used in many places in Flutter, such as Scrollablethe component, which distributes scroll notifications ( ScrollNotification) when sliding, and determines the position of the scroll bar Scrollbarby monitoring .ScrollNotification

Here's an example of listening to scroll notifications for a scrollable component:

NotificationListener(
  onNotification: (notification){
    
    
    switch (notification.runtimeType){
    
    
      case ScrollStartNotification: print("开始滚动"); break;
      case ScrollUpdateNotification: print("正在滚动"); break;
      case ScrollEndNotification: print("滚动停止"); break;
      case OverscrollNotification: print("滚动到边界"); break;
    }
  },
  child: ListView.builder(
    itemCount: 100,
    itemBuilder: (context, index) {
    
    
      return ListTile(title: Text("$index"),);
    }
  ),
);

The scrolling notifications in the above example such as ScrollStartNotification, , ScrollUpdateNotificationetc. are all inherited from the class ScrollNotification. Different types of notification subclasses will contain different information, such as ScrollUpdateNotificationan scrollDeltaattribute that records the displacement of the movement. For other notification attributes, you can check the SDK documentation yourself.

In the above example, we NotificationListenerlisten to the child ListView's scrolling notification through NotificationListenerthe definition as follows:

class NotificationListener<T extends Notification> extends StatelessWidget {
    
    
  const NotificationListener({
    
    
    Key key,
    required this.child,
    this.onNotification,
  }) : super(key: key);
 ...//省略无关代码 
}

We can see that:

  1. NotificationListenerInherits from the StatelessWidgetclass, so it can be nested directly into Widgetthe tree.

  2. NotificationListenerA template parameter can be specified, and the template parameter type must be inherited from Notification; when a template parameter is explicitly specified, NotificationListeneronly notifications of that parameter type will be received. For example, if we change the above example code to:

// 指定监听通知的类型为滚动结束通知(ScrollEndNotification)
NotificationListener<ScrollEndNotification>(
  onNotification: (notification){
    
     // 只会在滚动结束时才会触发此回调 
    print(notification);
  },
  child: ListView.builder(
    itemCount: 100,
    itemBuilder: (context, index) {
    
    
      return ListTile(title: Text("$index"),);
    }
  ),
);

After the above code is run, it will only print out the notification information on the console when the scrolling ends.

  1. onNotificationThe callback is a notification processing callback, and its function signature is as follows:
typedef NotificationListenerCallback<T extends Notification> = bool Function(T notification);

Its return value type is a Boolean value. When the return value is true, stop bubbling, and its parent Widgetwill no longer receive the notification; when the return value is , falsecontinue to bubble up notifications.

In the implementation of Flutter's UI framework, in addition to the scrollable components that will be sent during the scrolling process ScrollNotification, there are some other notifications, such as SizeChangedLayoutNotification, KeepAliveNotification, LayoutChangedNotificationetc. It is through this notification mechanism that Flutter enables the parent element to be displayed at some specific time. to do something.

custom notification

In addition to Flutter's internal notifications, we can also customize notifications. Let's see how to implement custom notifications:

  1. Define a notification class to inherit from Notificationthe class;
class MyNotification extends Notification {
    
    
  MyNotification(this.msg);
  final String msg;
}
  1. Distribution notice.

    NotificationThere is a dispatch(context)method, which is used to distribute notifications. As we said, it contextis actually Elementan interface for operations, which Elementcorresponds to nodes on the tree, and notifications will bubble up from the contextcorresponding nodes.Element

Let's look at a complete example:

class NotificationRoute extends StatefulWidget {
    
    
  
  NotificationRouteState createState() {
    
    
    return NotificationRouteState();
  }
}

class NotificationRouteState extends State<NotificationRoute> {
    
    
  String _msg="";
  
  Widget build(BuildContext context) {
    
    
    //监听通知  
    return NotificationListener<MyNotification>(
      onNotification: (notification) {
    
    
        setState(() {
    
    
          _msg += notification.msg+"  ";
        });
       return true;
      },
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
//           ElevatedButton(
//           onPressed: () => MyNotification("Hi").dispatch(context),
//           child: Text("Send Notification"),
//          ),  
            Builder(
              builder: (context) {
    
    
                return ElevatedButton(
                  // 按钮点击时分发通知  
                  onPressed: () => MyNotification("Hi").dispatch(context),
                  child: Text("Send Notification"),
                );
              },
            ),
            Text(_msg)
          ],
        ),
      ),
    );
  }
}

class MyNotification extends Notification {
    
    
  MyNotification(this.msg);
  final String msg;
}

In the above code, every time we click the button MyNotification, a type of notification will be distributed. We Widgetlisten to the notification on the root, and after receiving the notification, we will Textdisplay the notification on the screen.

Note: The commented part of the code does not work properly, because this contextis the root Context, NotificationListenerbut the subtree of the listener, so we use Builderto build ElevatedButtonto get the button position context.

running result:

insert image description here

Prevent notifications from bubbling

Let's change the above example to:

class NotificationRouteState extends State<NotificationRoute> {
    
    
  String _msg="";
  
  Widget build(BuildContext context) {
    
     
    return NotificationListener<MyNotification>( // 监听通知
      onNotification: (notification){
    
    
        print(notification.msg); // 打印通知
        return false;
      },
      child: NotificationListener<MyNotification>(
        onNotification: (notification) {
    
    
          setState(() {
    
    
            _msg += notification.msg + "  ";
          });
          return false; // 返回false表示不阻止冒泡,返回true会阻止冒泡
        },
        child: ...//省略重复代码
      ),
    );
  }
}

The two in the above list NotificationListenerare nested, and the child NotificationListener's onNotificationcallback returns false, indicating that bubbling is not prevented, so the parent NotificationListenerwill still be notified, so the console will print out the notification information; if the return value of the child NotificationListener's callback is changed to , then The parent will no longer print the notification, because the child has terminated the notification bubbling.onNotificationtrueNotificationListenerNotificationListener

Bubble principle

We introduced the phenomenon and use of notification bubbling above, now we go deeper and introduce how to realize notification bubbling in the Flutter framework. In order to clarify this problem, we must look at the source code. We start from the source of the notification distribution, and then follow the vine. Since the notification is sent through Notificationthe dispatch(context)method, let's take a look at dispatch(context)what is done in the method. The following is the relevant source code:

void dispatch(BuildContext target) {
    
    
  target?.visitAncestorElements(visitAncestor);
}

dispatch(context)contextThe current visitAncestorElementsmethod is called in , which traverses the parent elements from the current upwardsElement ; there is a traversal callback parameter, which will be executed for the parent elements traversed during the traversal process. The termination condition of the traversal is: the root has been traversed or a certain traversal callback returns .visitAncestorElementsElementfalse

visitAncestorElementsThe traversal callback passed to the method in the source code is visitAncestora method, let's look at visitAncestorthe implementation of the method:

// 遍历回调,会对每一个父级Element执行此回调
bool visitAncestor(Element element) {
    
    
  // 判断当前element对应的Widget是否是NotificationListener。
  
  // 由于NotificationListener是继承自StatelessWidget, 故先判断是否是StatelessElement
  if (element is StatelessElement) {
    
    
    // 是StatelessElement,则获取element对应的Widget,判断 是否是NotificationListener 。
    final StatelessWidget widget = element.widget;
    if (widget is NotificationListener<Notification>) {
    
    
      // 是NotificationListener,则调用该NotificationListener的_dispatch方法
      if (widget._dispatch(this, element)) 
        return false;
    }
  }
  return true;
}

visitAncestorWidgetIt will judge whether each traversed parent is NotificationListener, if not, return to truecontinue traversing upwards, if yes, call NotificationListenerthe _dispatchmethod, let's look at _dispatchthe source code of the method:

  bool _dispatch(Notification notification, Element element) {
    
    
    // 如果通知监听器不为空,并且当前通知类型是该NotificationListener
    // 监听的通知类型,则调用当前NotificationListener的onNotification
    if (onNotification != null && notification is T) {
    
    
      final bool result = onNotification(notification);
      // 返回值决定是否继续向上遍历
      return result == true; 
    }
    return false;
  }

NotificationListenerThe callback we can see onNotificationis finally _dispatchexecuted in the method, and then it will determine whether to continue bubbling up according to the return value . The implementation of the above source code is actually not complicated. By reading these source codes, some additional points can be noticed:

  1. ContextThere are also Elementmethods for traversing the tree.
  2. We can Element.widgetget elementthe corresponding relationship of nodes widget; we have repeatedly talked about the corresponding relationship of Widgetand , and we can use these source codes to deepen our understanding.Element

reference:

Guess you like

Origin blog.csdn.net/lyabc123456/article/details/130910088
Recommended