The introduction

Those who have experience in mobile terminal development know that touch events on mobile terminal are composed of basic events such as finger pressing, finger moving and finger lifting.

In Flutter, everything is a Widget. The Widget itself does not have the ability to recognize touch events. Widgets that recognize touch events must be assembled via a Listener or GestureDetector.

The GestureDetector is essentially composed of listeners, so let’s take a look at listeners.

Listener

A Listener is a functional Widget that monitors the original touch event. Let’s look at its constructor:

const Listener({
    Key key,
    this.onPointerDown,
    this.onPointerMove,
    this.onPointerEnter,
    this.onPointerExit,
    this.onPointerHover,
    this.onPointerUp,
    this.onPointerCancel,
    this.onPointerSignal,
    this.behavior = HitTestBehavior.deferToChild,
    Widget child,
 })
Copy the code

As can be seen from the constructor, Listener provides a variety of touch events to listen, but we often use onPointerDown, onPointerMove and onPointerUp, respectively corresponding to the three touch events of finger press, finger move and finger lift.

The Child property represents the wrapped Widget.

The behavior property is an important property for this section, but it is not a topic for this section. Before we understand the behavior property, we need to understand a concept called Hit Test.

Hit test

When a finger is pressed, moved, or lifted, a Flutter creates a new object for each event. For example, a Flutter is a PointerDownEvent when a finger is pressed, a PointerMoveEvent when a finger is moved, or a PointerUpEvent when a finger is lifted. For each event object, a hit test is performed on Flutter, which goes through the following steps:

1. The hit test is executed from the bottom Widget, depending on whether the hitTestChildren method (its Children Widget hits) or the hitTestSelf method returns true.

2. Loop the children Widgets of the lowest Widget to execute the child Widget hit test. Whether the Child Widget hits also depends on whether the hitTestChidren method (whether its children Widget hits a test) or the hitTestSelf method returns true.

3. Run the hit test recursively from bottom to top until you find the top hit test Widget and add it to the hit test list. Because it has matched the test, its parent Widget has also matched the test, adding the parent Widget to the list of hit tests. And so on until all the widgets that hit tests are added to the list of hit tests.

For example

To visualize the concept of a hit test, let’s look at the following example.

Listener(
    child: ConstrainedBox(
      constraints: BoxConstraints.tight(Size(200, 200)),
      child: Center(
        child: Text('click me'),
      )
    ),
    onPointerDown: (event) => print("onPointerDown"))Copy the code

In Flutter, each Widget actually corresponds to a RenderObject. For the code above, the diagram above shows the Widget and RenderObject mapping.

1. When Text is clicked, its list of hit tests looks like this: RenderParagraph – > RenderPositionedBox – > RenderConstrainedBox – > RenderPointerListener, So the handleEvent method of RenderPointerListener is executed, and eventually onPointerDown is printed on the console.

Note: Touch events loop through the list of hit tests and execute their handleEvent methods separately. RenderObject of almost all widgets in Flutter directly or indirectly inherits from RenderBox. RenderBox inherits HitTestTarget and overwrites the handleEvent method.

2. When a field outside Text is clicked, its list of hit tests has no RenderPointerListener. Why??

ConstrainedBox The area outside of Text is ConstrainedBox. Does RenderConstrainedBox match the test for ConstrainedBox? Apparently not.

ConstrainedBox has only one child, ConstrainedBox Center. RenderPositionedBox did not hit the test, causing RenderConstrainedBox’s hitTestChildren to return false and its hitTestSelf to return false, So RenderConstrainedBox doesn’t hit the test.

ConstrainedBox. RenderConstrainedBox does not hit the test. RenderPointerListener does not hit the test. So there is no RenderPointerListener in the hit test list.

So the console doesn’t print onPointerDown.

HitTest method is RenderBox (RenderObject subclass) hitTest method.

The above example USES the behaviors of attribute is the default HitTestBehavior deferToChild, if change behaviors attribute will effect what fantastic?

2. Behavior properties

Behavior Indicates the behavior policy during a Hit Test. It is an enumeration, provides three values, respectively is HitTestBehavior deferToChild, HitTestBehavior. Opaque, HitTestBehavior. Translucent.

As mentioned above, a hitTest is to look at the return value of the hitTest of the RenderBox, as in the Listener hitTest method.

bool hitTest(BoxHitTestResult result, { Offset position }) {
    bool hitTarget = false;
    if (size.contains(position)) {
      hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
      if (hitTarget || behavior == HitTestBehavior.translucent)
        result.add(BoxHitTestEntry(this, position));
    }
    return hitTarget;
}

bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;
Copy the code

HitTestBehavior. DeferToChild: whether the Listener hit test, depends on whether the child hit test, this is the default behaviors of the default values.

Hittestbehavior. opaque: If the Listener’s child does not match the test, this property ensures that hitTestSelf returns true. That is, the area where the Listener is stored can respond to the touch event.

HitTestBehavior. Translucent: If the Listener’s child does not match the test, and hitTestSelf returns false, this ensures that the region of the Listener responds to the touch event (added to the hitTest list), but the hitTest method returns false, which does not change.

For example

In the example above, we changed the Listener behavior property to hitTestBehavior.opaque.

Listener(
    child: ConstrainedBox(
      constraints: BoxConstraints.tight(Size(200, 200)),
      child: Center(
        child: Text('click me'OnPointerDown: (event) =>print("onPointerDown"))Copy the code

When we click outside of Text again, we see that RenderPointerListener has been added to the hit list.

If RenderPointerListener executes hitTestSelf, behavior (hitTestBehavior.opaque) returns true. That is, the RenderPointerListener meets the hit test.

So, we can see that the console will print onPointerDown.

Here’s another example

To better understand the behavior property, let’s look at another example.

Stack(
  children: <Widget>[
    Listener(
      child: ConstrainedBox(
        constraints: BoxConstraints.tight(Size(400, 200)),
        child: Container(
          color: Colors.blue,
        )
      ),
      onPointerDown: (event) => print("onPointerDown1"),
    ),
    Listener(
      child: ConstrainedBox(
        constraints: BoxConstraints.tight(Size(400, 200)),
        child: Center(child: Text("dont click me")),
      ),
      onPointerDown: (event) => print("onPointerDown2"), / / behaviors: HitTestBehavior opaque, 1 / / / / comment behaviors: HitTestBehavior. Translucent, 2)] / / comment,),Copy the code

Widget
RenderObject

1, behaviors as the default HitTestBehavior. DeferToChild attributes, when click outside the Text region, its accuracy test list is as follows: RenderDecoratedBox – > RenderConstrainedBox – > RenderPointerListener – > RenderStack.

RenderStack’s hitTestChildren first looks for the topmost child in the Stack to see if it hits the test. Obviously, the first child, the second Listener, did not hit the test.

It then looks for the second child, the first Listener, to see if the test hits. The first Listener contains a Container that has the color property set, so the Container corresponds to the RenderDecoratedBox, which passed the hit test, and the Listener also passed the hit test.

So the console will just print onPointerDown1.

Opaque = {RenderPointerListener->RenderStack -> Opaque = {RenderPointerListener-> opaque = {RenderPointerListener-> opaque = {RenderPointerListener-> opaque = {RenderPointerListener-> opaque = {RenderPointerListener-> opaque = {RenderPointerListener-> opaque = {RenderPointerListener-> opaque = {RenderPointerListener-> opaque = {RenderPointerListener-> opaque = {RenderPointerListener-> opaque

RenderStack’s hitTestChildren first looks for the topmost child in the Stack to see if it hits the test. The first child, the second Listener, passes the hit test by adding the hitTestBehavior.opaque property.

RenderStack’s hitTestChildren returns true, and it does not test the second child, the first Listener, for a hit.

So the console just prints onPointerDown2.

Open 3 and closed to note 1, 2, behaviors for HitTestBehavior. Translucent properties, when click outside the Text region, its accuracy test list is as follows: RenderPointerListener – > RenderDecoratedBox – > RenderConstrainedBox – > RenderPointerListener – > RenderStack.

RenderStack’s hitTestChildren first looks for the topmost child in the Stack to see if it hits the test. The first child, that is, the second Listener added HitTestBehavior. Translucent properties, after passed the hit test, join a test list. It is important to note, however, that the hitTest method of the RenderPointerListener returns false even though it passed the hitTest.

The RenderStack then looks for the second child, the first Listener, to see if it hits the test. According to the above analysis, it passed the hit test. So the entire list of hit tests is RenderPointerListener->RenderDecoratedBox->RenderConstrainedBox->RenderPointerListener->RenderStack.

So the console prints onPointerDown2, and then onPointerDown1.

conclusion

The Listener component of Flutter is a wrapper around all touchable widgets. A hit test needs to be performed on the Widget to determine how the touch event will be delivered. The Listener provides the behavior attribute to flexibly change the performance of the Listener during a matching test and provide different touch performance.