binding.dart 8.19 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';
import 'dart:collection';
7
import 'dart:ui' as ui show window, PointerDataPacket;
Ian Hickson's avatar
Ian Hickson committed
8

9
import 'package:flutter/foundation.dart';
Ian Hickson's avatar
Ian Hickson committed
10 11 12

import 'arena.dart';
import 'converter.dart';
13
import 'debug.dart';
Ian Hickson's avatar
Ian Hickson committed
14 15 16 17
import 'events.dart';
import 'hit_test.dart';
import 'pointer_router.dart';

18
/// A binding for the gesture subsystem.
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
///
/// ## Lifecycle of pointer events and the gesture arena
///
/// ### [PointerDownEvent]
///
/// When a [PointerDownEvent] is received by the [GestureBinding] (from
/// [Window.onPointerDataPacket], as interpreted by the
/// [PointerEventConverter]), a [hitTest] is performed to determine which
/// [HitTestTarget] nodes are affected. (Other bindings are expected to
/// implement [hitTest] to defer to [HitTestable] objects. For example, the
/// rendering layer defers to the [RenderView] and the rest of the render object
/// hierarchy.)
///
/// The affected nodes then are given the event to handle ([dispatchEvent] calls
/// [HitTestTarget.handleEvent] for each affected node). If any have relevant
/// [GestureRecognizer]s, they provide the event to them using
/// [GestureRecognizer.addPointer]. This typically causes the recognizer to
/// register with the [PointerRouter] to receive notifications regarding the
/// pointer in question.
///
/// Once the hit test and dispatching logic is complete, the event is then
/// passed to the aforementioned [PointerRouter], which passes it to any objects
/// that have registered interest in that event.
///
/// Finally, the [gestureArena] is closed for the given pointer
/// ([GestureArenaManager.close]), which begins the process of selecting a
/// gesture to win that pointer.
///
/// ### Other events
///
/// A pointer that is [PointerEvent.down] may send further events, such as
/// [PointerMoveEvent], [PointerUpEvent], or [PointerCancelEvent]. These are
/// sent to the same [HitTestTarget] nodes as were found when the down event was
/// received (even if they have since been disposed; it is the responsibility of
/// those objects to be aware of that possibility).
///
/// Then, the events are routed to any still-registered entrants in the
/// [PointerRouter]'s table for that pointer.
///
/// When a [PointerUpEvent] is received, the [GestureArenaManager.sweep] method
/// is invoked to force the gesture arena logic to terminate if necessary.
60
mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
61
  @override
Ian Hickson's avatar
Ian Hickson committed
62 63 64
  void initInstances() {
    super.initInstances();
    _instance = this;
65
    ui.window.onPointerDataPacket = _handlePointerDataPacket;
Ian Hickson's avatar
Ian Hickson committed
66 67
  }

68 69 70 71 72 73
  @override
  void unlocked() {
    super.unlocked();
    _flushPointerEventQueue();
  }

74
  /// The singleton instance of this object.
75 76
  static GestureBinding get instance => _instance;
  static GestureBinding _instance;
Ian Hickson's avatar
Ian Hickson committed
77

78
  final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
79

80
  void _handlePointerDataPacket(ui.PointerDataPacket packet) {
81 82
    // We convert pointer data to logical pixels so that e.g. the touch slop can be
    // defined in a device-independent manner.
83
    _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, ui.window.devicePixelRatio));
84 85
    if (!locked)
      _flushPointerEventQueue();
86 87 88 89 90 91 92
  }

  /// Dispatch a [PointerCancelEvent] for the given pointer soon.
  ///
  /// The pointer event will be dispatch before the next pointer event and
  /// before the end of the microtask but not within this function call.
  void cancelPointer(int pointer) {
93
    if (_pendingPointerEvents.isEmpty && !locked)
94
      scheduleMicrotask(_flushPointerEventQueue);
95
    _pendingPointerEvents.addFirst(PointerCancelEvent(pointer: pointer));
Ian Hickson's avatar
Ian Hickson committed
96 97
  }

98 99 100 101 102 103
  void _flushPointerEventQueue() {
    assert(!locked);
    while (_pendingPointerEvents.isNotEmpty)
      _handlePointerEvent(_pendingPointerEvents.removeFirst());
  }

Ian Hickson's avatar
Ian Hickson committed
104
  /// A router that routes all pointer events received from the engine.
105
  final PointerRouter pointerRouter = PointerRouter();
Ian Hickson's avatar
Ian Hickson committed
106 107 108

  /// The gesture arenas used for disambiguating the meaning of sequences of
  /// pointer events.
109
  final GestureArenaManager gestureArena = GestureArenaManager();
Ian Hickson's avatar
Ian Hickson committed
110 111 112 113 114

  /// State for all pointers which are currently down.
  ///
  /// The state of hovering pointers is not tracked because that would require
  /// hit-testing on every frame.
115
  final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};
Ian Hickson's avatar
Ian Hickson committed
116 117

  void _handlePointerEvent(PointerEvent event) {
118
    assert(!locked);
119
    HitTestResult result;
Ian Hickson's avatar
Ian Hickson committed
120 121
    if (event is PointerDownEvent) {
      assert(!_hitTests.containsKey(event.pointer));
122
      result = HitTestResult();
Ian Hickson's avatar
Ian Hickson committed
123 124
      hitTest(result, event.position);
      _hitTests[event.pointer] = result;
125 126 127 128
      assert(() {
        if (debugPrintHitTestResults)
          debugPrint('$event: $result');
        return true;
129
      }());
130 131 132 133 134
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
      result = _hitTests.remove(event.pointer);
    } else if (event.down) {
      result = _hitTests[event.pointer];
    } else {
135
      return; // We currently ignore add, remove, and hover move events.
Ian Hickson's avatar
Ian Hickson committed
136
    }
137 138
    if (result != null)
      dispatchEvent(event, result);
Ian Hickson's avatar
Ian Hickson committed
139 140 141
  }

  /// Determine which [HitTestTarget] objects are located at a given position.
142
  @override // from HitTestable
143
  void hitTest(HitTestResult result, Offset position) {
144
    result.add(HitTestEntry(this));
Ian Hickson's avatar
Ian Hickson committed
145 146
  }

147 148 149 150 151 152
  /// Dispatch an event to a hit test result's path.
  ///
  /// This sends the given event to every [HitTestTarget] in the entries
  /// of the given [HitTestResult], and catches exceptions that any of
  /// the handlers might throw. The `result` argument must not be null.
  @override // from HitTestDispatcher
Ian Hickson's avatar
Ian Hickson committed
153
  void dispatchEvent(PointerEvent event, HitTestResult result) {
154
    assert(!locked);
Ian Hickson's avatar
Ian Hickson committed
155
    assert(result != null);
156 157 158 159
    for (HitTestEntry entry in result.path) {
      try {
        entry.target.handleEvent(event, entry);
      } catch (exception, stack) {
160
        FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
161 162 163 164 165 166 167 168 169 170 171 172 173
          exception: exception,
          stack: stack,
          library: 'gesture library',
          context: 'while dispatching a pointer event',
          event: event,
          hitTestEntry: entry,
          informationCollector: (StringBuffer information) {
            information.writeln('Event:');
            information.writeln('  $event');
            information.writeln('Target:');
            information.write('  ${entry.target}');
          }
        ));
174 175
      }
    }
Ian Hickson's avatar
Ian Hickson committed
176 177
  }

178
  @override // from HitTestTarget
Ian Hickson's avatar
Ian Hickson committed
179 180 181 182 183 184 185 186 187
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    pointerRouter.route(event);
    if (event is PointerDownEvent) {
      gestureArena.close(event.pointer);
    } else if (event is PointerUpEvent) {
      gestureArena.sweep(event.pointer);
    }
  }
}
188 189

/// Variant of [FlutterErrorDetails] with extra fields for the gesture
190
/// library's binding's pointer event dispatcher ([GestureBinding.dispatchEvent]).
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
///
/// See also [FlutterErrorDetailsForPointerRouter], which is also used by the
/// gesture library.
class FlutterErrorDetailsForPointerEventDispatcher extends FlutterErrorDetails {
  /// Creates a [FlutterErrorDetailsForPointerEventDispatcher] object with the given
  /// arguments setting the object's properties.
  ///
  /// The gesture library calls this constructor when catching an exception
  /// that will subsequently be reported using [FlutterError.onError].
  const FlutterErrorDetailsForPointerEventDispatcher({
    dynamic exception,
    StackTrace stack,
    String library,
    String context,
    this.event,
    this.hitTestEntry,
207
    InformationCollector informationCollector,
208
    bool silent = false
209 210 211 212 213 214 215 216 217 218 219 220 221 222
  }) : super(
    exception: exception,
    stack: stack,
    library: library,
    context: context,
    informationCollector: informationCollector,
    silent: silent
  );

  /// The pointer event that was being routed when the exception was raised.
  final PointerEvent event;

  /// The hit test result entry for the object whose handleEvent method threw
  /// the exception.
223
  ///
224 225 226 227
  /// The target object itself is given by the [HitTestEntry.target] property of
  /// the hitTestEntry object.
  final HitTestEntry hitTestEntry;
}