binding.dart 17 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 7
import 'dart:async';
import 'dart:collection';
8
import 'dart:ui' as ui show PointerDataPacket;
Ian Hickson's avatar
Ian Hickson committed
9

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

import 'arena.dart';
import 'converter.dart';
15
import 'debug.dart';
Ian Hickson's avatar
Ian Hickson committed
16 17 18
import 'events.dart';
import 'hit_test.dart';
import 'pointer_router.dart';
19
import 'pointer_signal_resolver.dart';
20 21
import 'resampler.dart';

22
typedef _HandleSampleTimeChangedCallback = void Function();
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

// Class that handles resampling of touch events for multiple pointer
// devices.
//
// SchedulerBinding's `currentSystemFrameTimeStamp` is used to determine
// sample time.
class _Resampler {
  _Resampler(this._handlePointerEvent, this._handleSampleTimeChanged);

  // Resamplers used to filter incoming pointer events.
  final Map<int, PointerEventResampler> _resamplers = <int, PointerEventResampler>{};

  // Flag to track if a frame callback has been scheduled.
  bool _frameCallbackScheduled = false;

  // Current frame time for resampling.
  Duration _frameTime = Duration.zero;

  // Last sample time and time stamp of last event.
  //
  // Only used for debugPrint of resampling margin.
  Duration _lastSampleTime = Duration.zero;
  Duration _lastEventTime = Duration.zero;

  // Callback used to handle pointer events.
  final HandleEventCallback _handlePointerEvent;

  // Callback used to handle sample time changes.
51
  final _HandleSampleTimeChangedCallback _handleSampleTimeChanged;
52

53
  // Add `event` for resampling or dispatch it directly if
54
  // not a touch event.
55
  void addOrDispatch(PointerEvent event) {
56 57 58
    final SchedulerBinding? scheduler = SchedulerBinding.instance;
    assert(scheduler != null);
      // Add touch event to resampler or dispatch pointer event directly.
59 60 61 62 63 64 65 66 67 68 69
    if (event.kind == PointerDeviceKind.touch) {
      // Save last event time for debugPrint of resampling margin.
      _lastEventTime = event.timeStamp;

      final PointerEventResampler resampler = _resamplers.putIfAbsent(
        event.device,
        () => PointerEventResampler(),
      );
      resampler.addEvent(event);
    } else {
      _handlePointerEvent(event);
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
    }
  }

  // Sample and dispatch events.
  //
  // `samplingOffset` is relative to the current frame time, which
  // can be in the past when we're not actively resampling.
  // `currentSystemFrameTimeStamp` is used to determine the current
  // frame time.
  void sample(Duration samplingOffset) {
    final SchedulerBinding? scheduler = SchedulerBinding.instance;
    assert(scheduler != null);

    // Determine sample time by adding the offset to the current
    // frame time. This is expected to be in the past and not
    // result in any dispatched events unless we're actively
    // resampling events.
    final Duration sampleTime = _frameTime + samplingOffset;

    // Iterate over active resamplers and sample pointer events for
    // current sample time.
    for (final PointerEventResampler resampler in _resamplers.values) {
      resampler.sample(sampleTime, _handlePointerEvent);
    }

    // Remove inactive resamplers.
    _resamplers.removeWhere((int key, PointerEventResampler resampler) {
      return !resampler.hasPendingEvents && !resampler.isDown;
    });

    // Save last sample time for debugPrint of resampling margin.
    _lastSampleTime = sampleTime;

    // Schedule a frame callback if another call to `sample` is needed.
    if (!_frameCallbackScheduled && _resamplers.isNotEmpty) {
      _frameCallbackScheduled = true;
      scheduler?.scheduleFrameCallback((_) {
        _frameCallbackScheduled = false;
        // We use `currentSystemFrameTimeStamp` here as it's critical that
        // sample time is in the same clock as the event time stamps, and
        // never adjusted or scaled like `currentFrameTimeStamp`.
        _frameTime = scheduler.currentSystemFrameTimeStamp;
        assert(() {
          if (debugPrintResamplingMargin) {
            final Duration resamplingMargin = _lastEventTime - _lastSampleTime;
              debugPrint('$resamplingMargin');
          }
          return true;
        }());
        _handleSampleTimeChanged();
      });
    }
  }

  // Stop all resampling and dispatched any queued events.
  void stop() {
    for (final PointerEventResampler resampler in _resamplers.values) {
      resampler.stop(_handlePointerEvent);
    }
    _resamplers.clear();
  }
}

// The default sampling offset.
//
// Sampling offset is relative to presentation time. If we produce frames
// 16.667 ms before presentation and input rate is ~60hz, worst case latency
// is 33.334 ms. This however assumes zero latency from the input driver.
// 4.666 ms margin is added for this.
const Duration _defaultSamplingOffset = Duration(milliseconds: -38);
Ian Hickson's avatar
Ian Hickson committed
140

141
/// A binding for the gesture subsystem.
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
///
/// ## 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
174 175 176
/// sent to the same [HitTestTarget] nodes as were found when the
/// [PointerDownEvent] was received (even if they have since been disposed; it is
/// the responsibility of those objects to be aware of that possibility).
177 178 179 180 181 182
///
/// 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.
183
mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
184
  @override
Ian Hickson's avatar
Ian Hickson committed
185 186 187
  void initInstances() {
    super.initInstances();
    _instance = this;
188
    window.onPointerDataPacket = _handlePointerDataPacket;
Ian Hickson's avatar
Ian Hickson committed
189 190
  }

191 192 193 194 195 196
  @override
  void unlocked() {
    super.unlocked();
    _flushPointerEventQueue();
  }

197
  /// The singleton instance of this object.
198 199
  static GestureBinding? get instance => _instance;
  static GestureBinding? _instance;
Ian Hickson's avatar
Ian Hickson committed
200

201
  final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
202

203
  void _handlePointerDataPacket(ui.PointerDataPacket packet) {
204 205
    // We convert pointer data to logical pixels so that e.g. the touch slop can be
    // defined in a device-independent manner.
206
    _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
207 208
    if (!locked)
      _flushPointerEventQueue();
209 210 211 212
  }

  /// Dispatch a [PointerCancelEvent] for the given pointer soon.
  ///
213
  /// The pointer event will be dispatched before the next pointer event and
214 215
  /// before the end of the microtask but not within this function call.
  void cancelPointer(int pointer) {
216
    if (_pendingPointerEvents.isEmpty && !locked)
217
      scheduleMicrotask(_flushPointerEventQueue);
218
    _pendingPointerEvents.addFirst(PointerCancelEvent(pointer: pointer));
Ian Hickson's avatar
Ian Hickson committed
219 220
  }

221 222
  void _flushPointerEventQueue() {
    assert(!locked);
223

224
    while (_pendingPointerEvents.isNotEmpty)
225
      handlePointerEvent(_pendingPointerEvents.removeFirst());
226 227
  }

Ian Hickson's avatar
Ian Hickson committed
228
  /// A router that routes all pointer events received from the engine.
229
  final PointerRouter pointerRouter = PointerRouter();
Ian Hickson's avatar
Ian Hickson committed
230 231 232

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

235 236
  /// The resolver used for determining which widget handles a
  /// [PointerSignalEvent].
237 238
  final PointerSignalResolver pointerSignalResolver = PointerSignalResolver();

Ian Hickson's avatar
Ian Hickson committed
239 240 241 242
  /// 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.
243
  final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};
Ian Hickson's avatar
Ian Hickson committed
244

245 246 247 248 249 250 251 252 253 254 255
  /// Dispatch an event to the targets found by a hit test on its position.
  ///
  /// This method sends the given event to [dispatchEvent] based on event types:
  ///
  ///  * [PointerDownEvent]s and [PointerSignalEvent]s are dispatched to the
  ///    result of a new [hitTest].
  ///  * [PointerUpEvent]s and [PointerMoveEvent]s are dispatched to the result of hit test of the
  ///    preceding [PointerDownEvent]s.
  ///  * [PointerHoverEvent]s, [PointerAddedEvent]s, and [PointerRemovedEvent]s
  ///    are dispatched without a hit test result.
  void handlePointerEvent(PointerEvent event) {
256
    assert(!locked);
257 258 259 260 261 262 263 264 265 266 267 268 269 270

    if (resamplingEnabled) {
      _resampler.addOrDispatch(event);
      _resampler.sample(samplingOffset);
      return;
    }

    // Stop resampler if resampling is not enabled. This is a no-op if
    // resampling was never enabled.
    _resampler.stop();
    _handlePointerEventImmediately(event);
  }

  void _handlePointerEventImmediately(PointerEvent event) {
271
    HitTestResult? hitTestResult;
272
    if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
Ian Hickson's avatar
Ian Hickson committed
273
      assert(!_hitTests.containsKey(event.pointer));
274 275
      hitTestResult = HitTestResult();
      hitTest(hitTestResult, event.position);
276 277 278
      if (event is PointerDownEvent) {
        _hitTests[event.pointer] = hitTestResult;
      }
279 280
      assert(() {
        if (debugPrintHitTestResults)
281
          debugPrint('$event: $hitTestResult');
282
        return true;
283
      }());
284
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
285
      hitTestResult = _hitTests.remove(event.pointer);
286
    } else if (event.down) {
287
      // Because events that occur with the pointer down (like
288
      // [PointerMoveEvent]s) should be dispatched to the same place that their
289 290 291 292 293 294 295 296 297 298 299 300 301
      // 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 PointerAddedEvent ||
        event is PointerRemovedEvent) {
302
      assert(event.position != null);
303
      dispatchEvent(event, hitTestResult);
Ian Hickson's avatar
Ian Hickson committed
304 305 306 307
    }
  }

  /// Determine which [HitTestTarget] objects are located at a given position.
308
  @override // from HitTestable
309
  void hitTest(HitTestResult result, Offset position) {
310
    result.add(HitTestEntry(this));
Ian Hickson's avatar
Ian Hickson committed
311 312
  }

313
  /// Dispatch an event to [pointerRouter] and the path of a hit test result.
314
  ///
315 316 317 318
  /// The `event` is routed to [pointerRouter]. If the `hitTestResult` is not
  /// null, the event is also sent to every [HitTestTarget] in the entries of the
  /// given [HitTestResult]. Any exceptions from the handlers are caught.
  ///
319 320
  /// The `hitTestResult` argument may only be null for [PointerAddedEvent]s or
  /// [PointerRemovedEvent]s.
321
  @override // from HitTestDispatcher
322
  void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
323
    assert(!locked);
324 325 326
    // No hit test information implies that this is a [PointerHoverEvent],
    // [PointerAddedEvent], or [PointerRemovedEvent]. These events are specially
    // routed here; other events will be routed through the `handleEvent` below.
327
    if (hitTestResult == null) {
328
      assert(event is PointerAddedEvent || event is PointerRemovedEvent);
329 330 331 332 333 334 335
      try {
        pointerRouter.route(event);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
          exception: exception,
          stack: stack,
          library: 'gesture library',
336
          context: ErrorDescription('while dispatching a non-hit-tested pointer event'),
337 338
          event: event,
          hitTestEntry: null,
339 340
          informationCollector: () sync* {
            yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
341 342 343 344 345
          },
        ));
      }
      return;
    }
346
    for (final HitTestEntry entry in hitTestResult.path) {
347
      try {
348
        entry.target.handleEvent(event.transformed(entry.transform), entry);
349
      } catch (exception, stack) {
350
        FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
351 352 353
          exception: exception,
          stack: stack,
          library: 'gesture library',
354
          context: ErrorDescription('while dispatching a pointer event'),
355 356
          event: event,
          hitTestEntry: entry,
357 358 359
          informationCollector: () sync* {
            yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
            yield DiagnosticsProperty<HitTestTarget>('Target', entry.target, style: DiagnosticsTreeStyle.errorProperty);
360
          },
361
        ));
362 363
      }
    }
Ian Hickson's avatar
Ian Hickson committed
364 365
  }

366
  @override // from HitTestTarget
Ian Hickson's avatar
Ian Hickson committed
367 368 369 370 371 372
  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);
373 374
    } else if (event is PointerSignalEvent) {
      pointerSignalResolver.resolve(event);
Ian Hickson's avatar
Ian Hickson committed
375 376
    }
  }
377

378 379 380 381 382 383 384 385 386 387
  /// Reset states of [GestureBinding].
  ///
  /// This clears the hit test records.
  ///
  /// This is typically called between tests.
  @protected
  void resetGestureBinding() {
    _hitTests.clear();
  }

388 389
  void _handleSampleTimeChanged() {
    if (!locked) {
390 391 392 393 394 395
      if (resamplingEnabled) {
        _resampler.sample(samplingOffset);
      }
      else {
        _resampler.stop();
      }
396 397 398 399 400 401
    }
  }

  // Resampler used to filter incoming pointer events when resampling
  // is enabled.
  late final _Resampler _resampler = _Resampler(
402
    _handlePointerEventImmediately,
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
    _handleSampleTimeChanged,
  );

  /// Enable pointer event resampling for touch devices by setting
  /// this to true.
  ///
  /// Resampling results in smoother touch event processing at the
  /// cost of some added latency. Devices with low frequency sensors
  /// or when the frequency is not a multiple of the display frequency
  /// (e.g., 120Hz input and 90Hz display) benefit from this.
  ///
  /// This is typically set during application initialization but
  /// can be adjusted dynamically in case the application only
  /// wants resampling for some period of time.
  bool resamplingEnabled = false;

  /// Offset relative to current frame time that should be used for
  /// resampling. The [samplingOffset] is expected to be negative.
  /// Non-negative [samplingOffset] is allowed but will effectively
  /// disable resampling.
  Duration samplingOffset = _defaultSamplingOffset;
Ian Hickson's avatar
Ian Hickson committed
424
}
425 426

/// Variant of [FlutterErrorDetails] with extra fields for the gesture
427
/// library's binding's pointer event dispatcher ([GestureBinding.dispatchEvent]).
428 429 430 431 432 433 434
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({
435
    required Object exception,
436 437 438
    StackTrace? stack,
    String? library,
    DiagnosticsNode? context,
439 440
    this.event,
    this.hitTestEntry,
441
    InformationCollector? informationCollector,
442
    bool silent = false,
443 444 445 446 447 448
  }) : super(
    exception: exception,
    stack: stack,
    library: library,
    context: context,
    informationCollector: informationCollector,
449
    silent: silent,
450 451 452
  );

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

  /// The hit test result entry for the object whose handleEvent method threw
456
  /// the exception. May be null if no hit test entry is associated with the
457 458
  /// event (e.g. [PointerHoverEvent]s, [PointerAddedEvent]s, and
  /// [PointerRemovedEvent]s).
459
  ///
460 461
  /// The target object itself is given by the [HitTestEntry.target] property of
  /// the hitTestEntry object.
462
  final HitTestEntry? hitTestEntry;
463
}