An overview of the

On the mobile end, the event model of each platform or UI system is basically the same, that is, a complete event is divided into three stages: finger press, move, lift, and other double click, drag and so on are based on these events

When a pointer is pressed, A Hit Test is performed on the application to determine which widgets exist where the pointer is in contact with the screen. The pointer pressing event (and subsequent events of the pointer) are distributed to the innermost component found by the Hit Test, and then from there. Events bubble up the component tree. These events are distributed from the innermost component to all components in the root path of the component tree. The Web development browser has a similar event bubble mechanism, but there is no mechanism in Flutter to cancel or stop the bubbling process, which the browser can stop.

Note: Only components that pass the hit test can fire events

Raw pointer event handling

A Listener can be used in a Flutter to listen for raw touch events. A Listener is also a functional component, as described in <Flutter Field >.

Listener({
  Key key,
  this.onPointerDown, // Finger down callback
  this.onPointerMove, // Finger movement callback
  this.onPointerUp,// Finger lift callback
  this.onPointerCancel,// Touch the event to cancel the callback
  this.behavior = HitTestBehavior.deferToChild, // How does it behave during hit tests
  Widget child
})
Copy the code
  • Behavior will be introduced later

Example:

class EventTest extends StatefulWidget {
  @override
  _EventTestState createState() => _EventTestState();
}

class _EventTestState extends State<EventTest> {
  PointerEvent _event;

  @override
  Widget build(BuildContext context) {
    return Listener(
      child: Container(
        margin: EdgeInsets.only(top: 50), color: Colors.blue, alignment: Alignment.center, child: Text(_event? .toString() ??"", style: TextStyle(color: Colors.white)), ), onPointerDown: (PointerDownEvent event) => setState(() => {_event = event}), onPointerMove: (PointerMoveEvent event) => setState(() => {_event = event}), onPointerUp: (PointerUpEvent event) => setState(() => {_event = event}), ); }}Copy the code

The effect is as follows:

If you move your finger in the blue area, you can see the current pointer offset. When the PointerEvent is triggered, the parameters PointerDownEvent, PointerMoveEvent, PointerUpEvent are all subclasses of PointerEvent, PointerEvent contains information about the current pointer, such as:

  • Position: It is the offset of the mouse relative to the global coordinates
  • Delta: Distance between two pointer movement events
  • Pressure: pressure. This attribute is only meaningful if the mobile phone screen supports the pressure sensor. If the mobile phone does not support the pressure sensor, it is always 1.
  • Orientation: orientation of pointer movement, which is an Angle value

The above are just a few common attributes, but there are many other attributes that you can check out the API for yourself

behavior

It decides how the child responds to the hit test, and its value is HitTestBehavior, which is an enumerated class with three enumerated values, right

  • DeferToChild: Child components are tested one by one, and if any of the child components pass the test, the current component passes, which means that when a pointer event is applied to a child, its parent component must also receive the event

  • Opaque: During a hit test, the current component is originally opaque (even if it is transparent). The result is that the entire area of the current Widget is the click area. Chestnut:

    Listener(
        child: ConstrainedBox(
            constraints: BoxConstraints.tight(Size(300.0.150.0)),
            child: Center(child: Text("Box A"))),//behavior: HitTestBehavior.opaque,
        onPointerDown: (event) => print("down A")),Copy the code

    In the previous example, only clicking on the Text field will trigger the click event, because deferToChild will determine if the test hit the child component, which in this case is Text(“Box A”).

    If we want the entire 300×150 region to be clickable, we can set behavior to hitTestBehavior.opaque.

    Note: This property cannot be used to intercept (ignore) events in the component tree, it only determines the component size when the test is hit

  • 3. When a component hits a clear region, always tests itself as well as the bottom visible region. This means that when the top component transparency area is clicked, both the top component and the bottom component can receive events, such as:

    Stack(
      children: <Widget>[
        Listener(
          child: ConstrainedBox(
            constraints: BoxConstraints.tight(Size(300.0.200.0)),
            child: DecoratedBox(
                decoration: BoxDecoration(color: Colors.blue)),
          ),
          onPointerDown: (event) => print("down0"),
        ),
        Listener(
          child: ConstrainedBox(
            constraints: BoxConstraints.tight(Size(200.0.100.0)),
            child: Center(child: Text("Top left 200*100 non-text area click")),
          ),
          onPointerDown: (event) => print("down1"),
          / / behaviors: HitTestBehavior. Translucent, / / comment after let go visit can "point through") ",Copy the code

    In chestnut, when you comment out the last line of code and click on the non-text area in the upper left corner (the top component transparency area) in the 200×100 range, the console will only print down0, meaning that the top received no events, only the bottom

    When the comment is released, both the top and bottom receive events when clicked again

Ignore PinterEvent

If we don’t want a subtree to respond to a PointerEvent, we can use IgnorePointer and AbsorbPointer. Both of these components prevent the subtree from receiving pointer events, but the difference is that the AbsorbPointer participates in the hit test. IgnorePointer itself does not participate, which means that the AbsorbPointer itself can accept pointer events (but its sub-tree cannot), whereas IngorePointer cannot.

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

When it hits the Container, it doesn’t respond to the pointer event because it’s on the AbsorbPointer tree,

However, the AbsorbPoniter itself can accept pointer events, so it outputs up. If the AbsorbPointer is replaced with IgnorePointer, neither of them will be output.

Gesture recognition

GestuerDetector

GestureDetector is a functional component for gesture recognition that we can use to recognize various gestures

GestureDetector is actually a semantic encapsulation of pointer events. Let’s take a look at various gesture recognition.

Click, double click, long press

We gesture recognize the Container through the GestureDetector and display the event name on the Container after triggering the corresponding event, as follows:

class _EventTestState extends State<EventTest> {
  // Event name
  String _operation = "";

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        child: Container(
          width: 200,
          color: Colors.blue,
          alignment: Alignment.center,
          height: 100,
          child: Text(_operation, style: TextStyle(color: Colors.white,fontSize: 20)),
        ),
        onTap: () => upDateText("tap"), / / click
        onDoubleTap: () => upDateText("doubleTap"), / / double
        onLongPress: () => upDateText("longPress"), / / long press)); }void upDateText(Stringtext) { setState(() { _operation = text; }); }}Copy the code

Note: When listening on both onTop and onDoubleTap, there is a 200-millisecond delay when the user triggers the TAP event because it is possible to click again to trigger the double click event

If only onTap is listened on, there is no delay

Drag, slide

A complete gesture process refers to the whole process from the user’s finger down to the lift, during which the user may or may not move after pressing down.

GestureDetector makes no distinction between dragging and sliding events, they are essentially the same.

The GestureDetector uses the origin of the component to be listened on (upper left corner) as the origin of the gesture. Gesture recognition starts when a finger is pressed on the listening component. Ex. :

class _EventTestState extends State<EventTest> with SingleTickerProviderStateMixin {

  double _top = 100.0; // Offset from the top
  double _left = 100.0; // The offset from the left
  @override
  Widget build(BuildContext context) {

    return Scaffold(
      body: Stack(
        children: <Widget>[
          Positioned(
            top: _top,
            left: _left,
            child: GestureDetector(
              child: CircleAvatar(child: Text("A")),
              // Finger down callback
              onPanDown: (DragDownDetails e) {
                print('User finger presses${e.globalPosition}');
              },
              // Finger slide callback
              onPanUpdate: (DragUpdateDetails e) {
                // Update offset when sliding
                print('slip');
                setState(() {
                  _left += e.delta.dx;
                  _top += e.delta.dy;
                });
              },
              onPanEnd: (DragEndDetails e) {
                // Slide end, print x, y axis speed
                print(e.velocity); },),) [,),); }}Copy the code
  • GlobalPosition: This property is the offset from the origin of the screen (non-parent component) when the user presses it
  • Delta: Multiple Update events are triggered when the user slides on the screen. Dalta refers to the offset of an Update event slide
  • Velocity: This attribute represents the user’s sliding speed when lifting (including x and Y axes). The above example does not deal with the lifting speed. The common effect is to create a deceleration animation based on the lifting speed of the finger

The effect is as follows:

I/flutter (8239): the user presses the finger to Offset(134.9, 280.7) I/flutter (8239): slide I/chatty (8239): Uid =10152(com.flutter_study) 1. UI identical 302 lines I/flutter (8239): slide I/flutter (8239): Velocity (59.6, 244.0)Copy the code
Single direction drag

In many scenarios, we only need to drag in one direction, such as a vertical list

GestureDetector supports gesture events in a specific direction, such as:

Positioned(
  top: _top,
  child: GestureDetector(
    child: CircleAvatar(child: Text("A")),
    // Finger down callback
    onPanDown: (DragDownDetails e) {
      print('User finger presses${e.globalPosition}');
    },
    onVerticalDragUpdate: (DragUpdateDetails e) {
      setState(() {
        _top += e.delta.dy;
      });
    },
    onPanEnd: (DragEndDetails e) {
      // Slide end, print x, y axis speed
      print(e.velocity); },),)Copy the code

Modify the slide example as shown above

The zoom

The GestureDetector can listen for zooming events as follows:

Center(
  child: GestureDetector(
    child: Image.asset("./images/avatar.jpg", width: _width),
    onScaleUpdate: (ScaleUpdateDetails details) {
      setState(() {
        // The zoom is between 0.8 and 10 times
        _width = 100 * details.scale.clamp(8..10.0); }); },),);Copy the code

The above example is relatively simple, in practice we may also need some other functions, such as double click zoom in and out, the execution of animation, etc., interested can try first

GestureRecognizer

GetstureDetector uses one or more Gesturerecognizers internally to recognize various gestures. Gesturerecognizers convert the original pointer to semantic gestures via a Listener

GestureRecognizer is an abstract class, one gesture corresponds to a subclass. Flutter implements a rich GestureRecognizer that we can use directly.

Such as:

We’re adding event handlers to different parts of a RichText, but TextSpan is not a widget, so we can’t use GestureDetector. But TextSpan has a Recongizer property, and it can receive a GestureRecognizer.

bool _toggle = false; // Change color switch
TapGestureRecognizer _recognizer = TapGestureRecognizer();

Widget bothDirectionTest() {
  return Center(
    child: Text.rich(TextSpan(children: [
      TextSpan(text: Hello world.),
      TextSpan(
          text: Click to change color,
          style: TextStyle(
              fontSize: 30, color: _toggle ? Colors.red : Colors.yellow), recognizer: _recognizer .. onTap = () { setState(() { _toggle = ! _toggle; }); }), TextSpan(text:Hello world.)))); }@override
void dispose() {
    Dispose method to dispose GestureRecognizer
    _recognizer.dispose();
    super.dispose();
}

Copy the code

Note: After using GestureRecognizer, you must call its dispose method to release resources (mainly to cancel the internal timer).

Gesture competition and conflict

competition

If, in the example above, you listen for both horizontal and vertical drag events, which direction will work when you swipe diagonally? It actually depends on the displacement components of the two axes at the time of the first move, the larger of that axis, which axis is going to win out in this sliding event

Flutter actually introduces the concept of Arenal, literally translated as arena. Each GestureRecognizer is a competitor when a sliding event occurs, They all have to compete for the right to handle this event in the arena, and in the end, only one competitor will win.

For example, if you have a ListView and its first child is also a ListView, if you slide the child ListView, will the parent ListView move? The answer is definitely not going to move, only the child ListView is going to move, because the child LsitView has the right to handle the slide event.

The sample

var _top1 = 100.0;
var _left1 = 100.0;

Widget bothDirection() {
  return Stack(
    children: [
      Positioned(
        top: _top1,
        left: _left1,
        child: GestureDetector(
          child: CircleAvatar(child: Text("A")), onVerticalDragUpdate: (DragUpdateDetails details) { setState(() { _top1 += details.delta.dy; }); }, onHorizontalDragUpdate: (DragUpdateDetails details) { setState(() { _left1 += details.delta.dx; }); },),)],); }Copy the code

After running, each drag moves in only one direction, whereas competition occurs when the finger presses for the first time

The winning condition in the above example is that the one whose position has the largest horizontal and vertical component in the first move wins

Gestures conflict

Since there is only one winner in gesture competition, there may be conflicts when there are multiple gesture recognizers;

For example, we have a Widget that can be dragged left or right. Now we also want to detect finger press and lift events on it, as follows:

var _left2 = 100.0;
Widget flictTest() {
  return Stack(
    children: [
      Positioned(
        left: _left2,
        top: 100,
        child: GestureDetector(
          child: CircleAvatar(child: Text("A")),
          onHorizontalDragUpdate: (DragUpdateDetails details) {
            setState(() {
              _left2 += details.delta.dx;
            });
          },
          onHorizontalDragEnd: (details) {
            print('onHorizontalDragEnd');
          },
          onTapDown: (details) {
            print('down');
          },
          onTapUp: (details) {
            print('up'); },),)],); }Copy the code

After dragging, the log is as follows:

0I/flutter ( 4315): down
I/flutter ( 4315): onHorizontalDragEnd
Copy the code

We found that we didn’t print up, and that’s because when we drag, when we press down and the finger doesn’t move, the drag gesture doesn’t have complete semantics, and then the TapDown gesture wins, and then we print down, and when we drag, the drag gesture wins, and when we lift, OnHorizontalDragEnd conflicts with onTap, but since it is in drag semantics, onHorizontalDragEnd wins, so onHorizontalDragEnd is printed.

If our logic code relies heavily on finger pressing and lifting, such as a multicast component, we want to pause the multicast when pressing and resume the multicast when lifting. However, since the cast component itself may already handle the drag gesture, or even support the pinch gesture, then if the external use of onTapDown, onTap to listen will not work.

We can use the same Listener to listen for the original pointer event:

Listener(
    child: GestureDetector(
      child: CircleAvatar(child: Text("A")),
      onHorizontalDragUpdate: (DragUpdateDetails details) {
        setState(() {
          _left2 += details.delta.dx;
        });
      },
      onHorizontalDragEnd: (details) {
        print('onHorizontalDragEnd');
      },
    ),
    onPointerDown: (details){
      print('onPointerDown');
    },
    onPointerUp: (details){
      print('onPointerUp'); },),)Copy the code

Gesture conflict is only at gesture level, and gesture is semantic recognition of the original pointer. Therefore, when encountering complex conflict scenes, the Listener can directly identify the original pointer event to solve the conflict

Event bus

In apps, we often need a broadcast mechanism to announce page events, such as logging out, some pages may need status updates. This is where an event bus can be very useful;

The event bus usually implements the subscriber pattern. The subscriber has two roles: subscriber and publisher. The subscriber can trigger events and listen events through the event bus.

The code is as follows:

typedef void EventCallback(arg);

class EventBus {
  // Private construct
  EventBus._internal();

  static EventBus _singleton = new EventBus._internal();

  // Factory constructor
  factory EventBus() => _singleton;

  // Save time subscriber queue, key: event name (id), value: corresponding actual subscriber queue
  var _eMap = new Map<Object.List<EventCallback>>();

  ///Add subscribers
  void on(eventName, EventCallback f) {
    if (eventName == null || f == null) return; _eMap[eventName] ?? = []; _eMap[eventName].add(f); }///Remove the subscriber
  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); }}///Trigger subscribers
  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); }}}///Define a top-level, global variable, page introduced after the file can directly use the bug
var bus = new EventBus();
Copy the code

Use as follows:

// Listen login failed
bus.on(Event.LOGIN_OUT, (arg) {
  SpUtil.putString(Application.accessToken, null);
  Application.router.navigateTo(context, Routes.login, clearStack: true);
});

// Trigger the failure event
bus.emit(Event.LOGIN_OUT, null);
Copy the code

Note: The standard way to implement patterns in Dart is to use the static variable + factory constructor to ensure that new EventBus() always returns the same instance

The event bus is commonly used for sharing state between components, but there are specialized packages for sharing state between components, such as Redux, and providers.

For some simple applications, the event bus is always designed to meet the business needs. If you feel you need to use the state management package, it is important to figure out whether it is necessary for your APP to use it to prevent over-design from simplifying into complexity.


reference

Refer to self Flutter actual combat