Flutter (56): Raw pointer event processing

Time: Column:Mobile & Frontend views:272

56.1 Introduction to Raw Pointer Events

This section introduces raw pointer events (Pointer Event, typically touch events on mobile devices), while the next section will cover gesture handling.

In mobile platforms, the raw pointer event model is generally consistent across various platforms or UI systems. A complete event is divided into three phases: finger press, finger movement, and finger release. Higher-level gestures (like tap, double-tap, drag, etc.) are built upon these raw events.

When a pointer is pressed, Flutter performs a hit test on the application to determine which components (widgets) are located at the point of contact on the screen. The pointer down event (and any subsequent events) is then dispatched to the innermost component identified by the hit test. From there, the event bubbles up through the component tree, similar to the event bubbling mechanism in web development. However, Flutter does not provide a mechanism to cancel or stop the "bubbling" process, whereas the browser's bubbling can be halted. Note that only components that pass the hit test can trigger events; we will delve deeper into the hit test process in the next section.

Note: The term "Hit Test" has various translations in Chinese, such as "命中测试" and "点击测试." We need not be overly concerned with the names; it's sufficient to understand that they represent "Hit Test."

56.2 Listener Component

In Flutter, the Listener widget can be used to listen for raw touch events. According to our categorization of components in this book, Listener is also a functional component. Below is the constructor definition for Listener:

Listener({
  Key? key,
  this.onPointerDown, // Pointer down callback
  this.onPointerMove, // Pointer move callback
  this.onPointerUp,   // Pointer up callback
  this.onPointerCancel,// Pointer event cancel callback
  this.behavior = HitTestBehavior.deferToChild, // We'll discuss this parameter later
  Widget? child,
})

Let's look at an example where the code allows you to see the position of the finger relative to a container while moving.

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

  @override
  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),
    );
  }
}

The effect after running the code is shown in Figure :

8-1.1806ace6.png

By moving your finger within the blue rectangular area, you can see the current pointer offset. When pointer events are triggered, the parameters PointerDownEvent, PointerMoveEvent, and PointerUpEvent are all subclasses of PointerEvent. The PointerEvent class contains information about the current pointer. Note that "Pointer" refers to the event trigger, which can be a mouse, touchpad, or finger.

For example:

  • position: This indicates the pointer's offset relative to the global coordinates.

  • localPosition: This indicates the pointer's offset relative to the local layout coordinates.

  • delta: The distance between two pointer move events (PointerMoveEvent).

  • pressure: The pressure level; this property is more meaningful if the phone screen supports pressure sensors (like the iPhone's 3D Touch). If not, it will always be 1.

  • orientation: The direction of pointer movement, represented as an angle.

These are just some common properties of PointerEvent. Besides these, there are many more properties that readers can check in the API documentation.

There’s also a behavior property that determines how child components respond to hit tests. We will cover this property in detail in section 8.3.

56.3 Ignoring Pointer Events

If we want to prevent a certain subtree from responding to pointer events, we can use IgnorePointer and AbsorbPointer. Both components can stop the subtree from receiving pointer events, but the difference is that AbsorbPointer itself participates in hit testing, while IgnorePointer does not. This means that AbsorbPointer can receive pointer events (but its subtree cannot), while IgnorePointer cannot. Here’s a simple example:

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 you click the Container, it is within the subtree of AbsorbPointer, so it won’t respond to the pointer event, and the log won’t output "in." However, AbsorbPointer itself can receive pointer events, so it will output "up." If you replace AbsorbPointer with IgnorePointer, then neither will output.