binding.dart 10.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Ian Hickson's avatar
Ian Hickson committed
2 3 4
// 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 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
import 'events.dart';
import 'hit_test.dart';
import 'pointer_router.dart';
17
import 'pointer_signal_resolver.dart';
Ian Hickson's avatar
Ian Hickson committed
18

19
/// A binding for the gesture subsystem.
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 60
///
/// ## 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.
61
mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
62
  @override
Ian Hickson's avatar
Ian Hickson committed
63 64 65
  void initInstances() {
    super.initInstances();
    _instance = this;
66
    window.onPointerDataPacket = _handlePointerDataPacket;
Ian Hickson's avatar
Ian Hickson committed
67 68
  }

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

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

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

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

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

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

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

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

112 113 114 115
  /// The resolver used for determining which widget handles a pointer
  /// signal event.
  final PointerSignalResolver pointerSignalResolver = PointerSignalResolver();

Ian Hickson's avatar
Ian Hickson committed
116 117 118 119
  /// 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.
120
  final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};
Ian Hickson's avatar
Ian Hickson committed
121 122

  void _handlePointerEvent(PointerEvent event) {
123
    assert(!locked);
124
    HitTestResult hitTestResult;
125
    if (event is PointerDownEvent || event is PointerSignalEvent) {
Ian Hickson's avatar
Ian Hickson committed
126
      assert(!_hitTests.containsKey(event.pointer));
127 128
      hitTestResult = HitTestResult();
      hitTest(hitTestResult, event.position);
129 130 131
      if (event is PointerDownEvent) {
        _hitTests[event.pointer] = hitTestResult;
      }
132 133
      assert(() {
        if (debugPrintHitTestResults)
134
          debugPrint('$event: $hitTestResult');
135
        return true;
136
      }());
137
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
138
      hitTestResult = _hitTests.remove(event.pointer);
139
    } else if (event.down) {
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
      // Because events that occur with the pointer down (like
      // PointerMoveEvents) should be dispatched to the same place that their
      // initial PointerDownEvent was, we want to re-use the path we found when
      // the pointer went down, rather than do hit detection each time we get
      // such an event.
      hitTestResult = _hitTests[event.pointer];
    }
    assert(() {
      if (debugPrintMouseHoverEvents && event is PointerHoverEvent)
        debugPrint('$event');
      return true;
    }());
    if (hitTestResult != null ||
        event is PointerHoverEvent ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      dispatchEvent(event, hitTestResult);
Ian Hickson's avatar
Ian Hickson committed
157 158 159 160
    }
  }

  /// Determine which [HitTestTarget] objects are located at a given position.
161
  @override // from HitTestable
162
  void hitTest(HitTestResult result, Offset position) {
163
    result.add(HitTestEntry(this));
Ian Hickson's avatar
Ian Hickson committed
164 165
  }

166 167
  /// Dispatch an event to a hit test result's path.
  ///
168 169 170 171
  /// 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 [hitTestResult] argument may only be null for
  /// [PointerHoverEvent], [PointerAddedEvent], or [PointerRemovedEvent] events.
172
  @override // from HitTestDispatcher
173
  void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
174
    assert(!locked);
175 176 177 178 179 180 181 182 183 184 185
    // No hit test information implies that this is a hover or pointer
    // add/remove event.
    if (hitTestResult == null) {
      assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent);
      try {
        pointerRouter.route(event);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
          exception: exception,
          stack: stack,
          library: 'gesture library',
186
          context: ErrorDescription('while dispatching a non-hit-tested pointer event'),
187 188
          event: event,
          hitTestEntry: null,
189 190
          informationCollector: () sync* {
            yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
191 192 193 194 195
          },
        ));
      }
      return;
    }
196
    for (final HitTestEntry entry in hitTestResult.path) {
197
      try {
198
        entry.target.handleEvent(event.transformed(entry.transform), entry);
199
      } catch (exception, stack) {
200
        FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
201 202 203
          exception: exception,
          stack: stack,
          library: 'gesture library',
204
          context: ErrorDescription('while dispatching a pointer event'),
205 206
          event: event,
          hitTestEntry: entry,
207 208 209
          informationCollector: () sync* {
            yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
            yield DiagnosticsProperty<HitTestTarget>('Target', entry.target, style: DiagnosticsTreeStyle.errorProperty);
210
          },
211
        ));
212 213
      }
    }
Ian Hickson's avatar
Ian Hickson committed
214 215
  }

216
  @override // from HitTestTarget
Ian Hickson's avatar
Ian Hickson committed
217 218 219 220 221 222
  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);
223 224
    } else if (event is PointerSignalEvent) {
      pointerSignalResolver.resolve(event);
Ian Hickson's avatar
Ian Hickson committed
225 226 227
    }
  }
}
228 229

/// Variant of [FlutterErrorDetails] with extra fields for the gesture
230
/// library's binding's pointer event dispatcher ([GestureBinding.dispatchEvent]).
231
///
232 233 234 235
/// See also:
///
///  * [FlutterErrorDetailsForPointerRouter], which is also used by the
///    gesture library.
236 237 238 239 240 241 242 243 244 245
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,
246
    DiagnosticsNode context,
247 248
    this.event,
    this.hitTestEntry,
249
    InformationCollector informationCollector,
250
    bool silent = false,
251 252 253 254 255 256
  }) : super(
    exception: exception,
    stack: stack,
    library: library,
    context: context,
    informationCollector: informationCollector,
257
    silent: silent,
258 259 260 261 262 263
  );

  /// 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
264 265
  /// the exception. May be null if no hit test entry is associated with the
  /// event (e.g. hover and pointer add/remove events).
266
  ///
267 268 269 270
  /// The target object itself is given by the [HitTestEntry.target] property of
  /// the hitTestEntry object.
  final HitTestEntry hitTestEntry;
}