1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:fake_async/fake_async.dart';
6/// @docImport 'package:flutter/rendering.dart';
7/// @docImport 'package:flutter/widgets.dart';
8///
9/// @docImport 'recognizer.dart';
10library;
11
12import 'dart:async';
13import 'dart:collection';
14import 'dart:ui' as ui show PointerDataPacket;
15
16import 'package:flutter/foundation.dart';
17import 'package:flutter/scheduler.dart';
18
19import 'arena.dart';
20import 'converter.dart';
21import 'debug.dart';
22import 'events.dart';
23import 'hit_test.dart';
24import 'pointer_router.dart';
25import 'pointer_signal_resolver.dart';
26import 'resampler.dart';
27
28export 'dart:ui' show Offset;
29
30export 'package:flutter/foundation.dart' show DiagnosticsNode, InformationCollector;
31
32export 'arena.dart' show GestureArenaManager;
33export 'events.dart' show PointerEvent;
34export 'hit_test.dart' show HitTestEntry, HitTestResult, HitTestTarget;
35export 'pointer_router.dart' show PointerRouter;
36export 'pointer_signal_resolver.dart' show PointerSignalResolver;
37
38typedef _HandleSampleTimeChangedCallback = void Function();
39
40/// Class that implements clock used for sampling.
41class SamplingClock {
42 /// Returns current time.
43 DateTime now() => DateTime.now();
44
45 /// Returns a new stopwatch that uses the current time as reported by `this`.
46 ///
47 /// See also:
48 ///
49 /// * [GestureBinding.debugSamplingClock], which is used in tests and
50 /// debug builds to observe [FakeAsync].
51 Stopwatch stopwatch() => Stopwatch(); // flutter_ignore: stopwatch (see analyze.dart)
52 // Ignore context: This is replaced by debugSampling clock in the test binding.
53}
54
55// Class that handles resampling of touch events for multiple pointer
56// devices.
57//
58// The `samplingInterval` is used to determine the approximate next
59// time for resampling.
60// SchedulerBinding's `currentSystemFrameTimeStamp` is used to determine
61// sample time.
62class _Resampler {
63 _Resampler(this._handlePointerEvent, this._handleSampleTimeChanged, this._samplingInterval);
64
65 // Resamplers used to filter incoming pointer events.
66 final Map<int, PointerEventResampler> _resamplers = <int, PointerEventResampler>{};
67
68 // Flag to track if a frame callback has been scheduled.
69 bool _frameCallbackScheduled = false;
70
71 // Last frame time for resampling.
72 Duration _frameTime = Duration.zero;
73
74 // Time since `_frameTime` was updated.
75 Stopwatch _frameTimeAge = Stopwatch(); // flutter_ignore: stopwatch (see analyze.dart)
76 // Ignore context: This is tested safely outside of FakeAsync.
77
78 // Last sample time and time stamp of last event.
79 //
80 // Only used for debugPrint of resampling margin.
81 Duration _lastSampleTime = Duration.zero;
82 Duration _lastEventTime = Duration.zero;
83
84 // Callback used to handle pointer events.
85 final HandleEventCallback _handlePointerEvent;
86
87 // Callback used to handle sample time changes.
88 final _HandleSampleTimeChangedCallback _handleSampleTimeChanged;
89
90 // Interval used for sampling.
91 final Duration _samplingInterval;
92
93 // Timer used to schedule resampling.
94 Timer? _timer;
95
96 // Add `event` for resampling or dispatch it directly if
97 // not a touch event.
98 void addOrDispatch(PointerEvent event) {
99 // Add touch event to resampler or dispatch pointer event directly.
100 if (event.kind == PointerDeviceKind.touch) {
101 // Save last event time for debugPrint of resampling margin.
102 _lastEventTime = event.timeStamp;
103
104 final PointerEventResampler resampler = _resamplers.putIfAbsent(
105 event.device,
106 () => PointerEventResampler(),
107 );
108 resampler.addEvent(event);
109 } else {
110 _handlePointerEvent(event);
111 }
112 }
113
114 // Sample and dispatch events.
115 //
116 // The `samplingOffset` is relative to the current frame time, which
117 // can be in the past when we're not actively resampling.
118 //
119 // The `samplingClock` is the clock used to determine frame time age.
120 void sample(Duration samplingOffset, SamplingClock clock) {
121 final SchedulerBinding scheduler = SchedulerBinding.instance;
122
123 // Initialize `_frameTime` if needed. This will be used for periodic
124 // sampling when frame callbacks are not received.
125 if (_frameTime == Duration.zero) {
126 _frameTime = Duration(milliseconds: clock.now().millisecondsSinceEpoch);
127 _frameTimeAge = clock.stopwatch()..start();
128 }
129
130 // Schedule periodic resampling if `_timer` is not already active.
131 if (_timer?.isActive != true) {
132 _timer = Timer.periodic(_samplingInterval, (_) => _onSampleTimeChanged());
133 }
134
135 // Calculate the effective frame time by taking the number
136 // of sampling intervals since last time `_frameTime` was
137 // updated into account. This allows us to advance sample
138 // time without having to receive frame callbacks.
139 final int samplingIntervalUs = _samplingInterval.inMicroseconds;
140 final int elapsedIntervals = _frameTimeAge.elapsedMicroseconds ~/ samplingIntervalUs;
141 final int elapsedUs = elapsedIntervals * samplingIntervalUs;
142 final Duration frameTime = _frameTime + Duration(microseconds: elapsedUs);
143
144 // Determine sample time by adding the offset to the current
145 // frame time. This is expected to be in the past and not
146 // result in any dispatched events unless we're actively
147 // resampling events.
148 final Duration sampleTime = frameTime + samplingOffset;
149
150 // Determine next sample time by adding the sampling interval
151 // to the current sample time.
152 final Duration nextSampleTime = sampleTime + _samplingInterval;
153
154 // Iterate over active resamplers and sample pointer events for
155 // current sample time.
156 for (final PointerEventResampler resampler in _resamplers.values) {
157 resampler.sample(sampleTime, nextSampleTime, _handlePointerEvent);
158 }
159
160 // Remove inactive resamplers.
161 _resamplers.removeWhere((int key, PointerEventResampler resampler) {
162 return !resampler.hasPendingEvents && !resampler.isDown;
163 });
164
165 // Save last sample time for debugPrint of resampling margin.
166 _lastSampleTime = sampleTime;
167
168 // Early out if another call to `sample` isn't needed.
169 if (_resamplers.isEmpty) {
170 _timer!.cancel();
171 return;
172 }
173
174 // Schedule a frame callback if another call to `sample` is needed.
175 if (!_frameCallbackScheduled) {
176 _frameCallbackScheduled = true;
177 // Add a post frame callback as this avoids producing unnecessary
178 // frames but ensures that sampling phase is adjusted to frame
179 // time when frames are produced.
180 scheduler.addPostFrameCallback((_) {
181 _frameCallbackScheduled = false;
182 // We use `currentSystemFrameTimeStamp` here as it's critical that
183 // sample time is in the same clock as the event time stamps, and
184 // never adjusted or scaled like `currentFrameTimeStamp`.
185 _frameTime = scheduler.currentSystemFrameTimeStamp;
186 _frameTimeAge.reset();
187 // Reset timer to match phase of latest frame callback.
188 _timer?.cancel();
189 _timer = Timer.periodic(_samplingInterval, (_) => _onSampleTimeChanged());
190 // Trigger an immediate sample time change.
191 _onSampleTimeChanged();
192 }, debugLabel: 'Resampler.startTimer');
193 }
194 }
195
196 // Stop all resampling and dispatched any queued events.
197 void stop() {
198 for (final PointerEventResampler resampler in _resamplers.values) {
199 resampler.stop(_handlePointerEvent);
200 }
201 _resamplers.clear();
202 _frameTime = Duration.zero;
203 _timer?.cancel();
204 }
205
206 void _onSampleTimeChanged() {
207 assert(() {
208 if (debugPrintResamplingMargin) {
209 final Duration resamplingMargin = _lastEventTime - _lastSampleTime;
210 debugPrint('$resamplingMargin');
211 }
212 return true;
213 }());
214 _handleSampleTimeChanged();
215 }
216}
217
218// The default sampling offset.
219//
220// Sampling offset is relative to presentation time. If we produce frames
221// 16.667 ms before presentation and input rate is ~60hz, worst case latency
222// is 33.334 ms. This however assumes zero latency from the input driver.
223// 4.666 ms margin is added for this.
224const Duration _defaultSamplingOffset = Duration(milliseconds: -38);
225
226// The sampling interval.
227//
228// Sampling interval is used to determine the approximate time for subsequent
229// sampling. This is used to sample events when frame callbacks are not
230// being received and decide if early processing of up and removed events
231// is appropriate. 16667 us for 60hz sampling interval.
232const Duration _samplingInterval = Duration(microseconds: 16667);
233
234/// A binding for the gesture subsystem.
235///
236/// ## Lifecycle of pointer events and the gesture arena
237///
238/// ### [PointerDownEvent]
239///
240/// When a [PointerDownEvent] is received by the [GestureBinding] (from
241/// [dart:ui.PlatformDispatcher.onPointerDataPacket], as interpreted by the
242/// [PointerEventConverter]), a [hitTest] is performed to determine which
243/// [HitTestTarget] nodes are affected. (Other bindings are expected to
244/// implement [hitTest] to defer to [HitTestable] objects. For example, the
245/// rendering layer defers to the [RenderView] and the rest of the render object
246/// hierarchy.)
247///
248/// The affected nodes then are given the event to handle ([dispatchEvent] calls
249/// [HitTestTarget.handleEvent] for each affected node). If any have relevant
250/// [GestureRecognizer]s, they provide the event to them using
251/// [GestureRecognizer.addPointer]. This typically causes the recognizer to
252/// register with the [PointerRouter] to receive notifications regarding the
253/// pointer in question.
254///
255/// Once the hit test and dispatching logic is complete, the event is then
256/// passed to the aforementioned [PointerRouter], which passes it to any objects
257/// that have registered interest in that event.
258///
259/// Finally, the [gestureArena] is closed for the given pointer
260/// ([GestureArenaManager.close]), which begins the process of selecting a
261/// gesture to win that pointer.
262///
263/// ### Other events
264///
265/// A pointer that is [PointerEvent.down] may send further events, such as
266/// [PointerMoveEvent], [PointerUpEvent], or [PointerCancelEvent]. These are
267/// sent to the same [HitTestTarget] nodes as were found when the
268/// [PointerDownEvent] was received (even if they have since been disposed; it is
269/// the responsibility of those objects to be aware of that possibility).
270///
271/// Then, the events are routed to any still-registered entrants in the
272/// [PointerRouter]'s table for that pointer.
273///
274/// When a [PointerUpEvent] is received, the [GestureArenaManager.sweep] method
275/// is invoked to force the gesture arena logic to terminate if necessary.
276mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
277 @override
278 void initInstances() {
279 super.initInstances();
280 _instance = this;
281 platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;
282 }
283
284 /// The singleton instance of this object.
285 ///
286 /// Provides access to the features exposed by this mixin. The binding must
287 /// be initialized before using this getter; this is typically done by calling
288 /// [runApp] or [WidgetsFlutterBinding.ensureInitialized].
289 static GestureBinding get instance => BindingBase.checkInstance(_instance);
290 static GestureBinding? _instance;
291
292 @override
293 void unlocked() {
294 super.unlocked();
295 _flushPointerEventQueue();
296 }
297
298 final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
299
300 void _handlePointerDataPacket(ui.PointerDataPacket packet) {
301 // We convert pointer data to logical pixels so that e.g. the touch slop can be
302 // defined in a device-independent manner.
303 try {
304 _pendingPointerEvents.addAll(
305 PointerEventConverter.expand(packet.data, _devicePixelRatioForView),
306 );
307 if (!locked) {
308 _flushPointerEventQueue();
309 }
310 } catch (error, stack) {
311 FlutterError.reportError(
312 FlutterErrorDetails(
313 exception: error,
314 stack: stack,
315 library: 'gestures library',
316 context: ErrorDescription('while handling a pointer data packet'),
317 ),
318 );
319 }
320 }
321
322 double? _devicePixelRatioForView(int viewId) {
323 return platformDispatcher.view(id: viewId)?.devicePixelRatio;
324 }
325
326 /// Dispatch a [PointerCancelEvent] for the given pointer soon.
327 ///
328 /// The pointer event will be dispatched before the next pointer event and
329 /// before the end of the microtask but not within this function call.
330 void cancelPointer(int pointer) {
331 if (_pendingPointerEvents.isEmpty && !locked) {
332 scheduleMicrotask(_flushPointerEventQueue);
333 }
334 _pendingPointerEvents.addFirst(PointerCancelEvent(pointer: pointer));
335 }
336
337 void _flushPointerEventQueue() {
338 assert(!locked);
339
340 while (_pendingPointerEvents.isNotEmpty) {
341 handlePointerEvent(_pendingPointerEvents.removeFirst());
342 }
343 }
344
345 /// A router that routes all pointer events received from the engine.
346 final PointerRouter pointerRouter = PointerRouter();
347
348 /// The gesture arenas used for disambiguating the meaning of sequences of
349 /// pointer events.
350 final GestureArenaManager gestureArena = GestureArenaManager();
351
352 /// The resolver used for determining which widget handles a
353 /// [PointerSignalEvent].
354 final PointerSignalResolver pointerSignalResolver = PointerSignalResolver();
355
356 /// State for all pointers which are currently down.
357 ///
358 /// This map caches the hit test result done when the pointer goes down
359 /// ([PointerDownEvent] and [PointerPanZoomStartEvent]). This hit test result
360 /// will be used throughout the entire pointer interaction; that is, the
361 /// pointer is seen as pointing to the same place even if it has moved away
362 /// until pointer goes up ([PointerUpEvent] and [PointerPanZoomEndEvent]).
363 /// This matches the expected gesture interaction with a button, and allows
364 /// devices that don't support hovering to perform as few hit tests as
365 /// possible.
366 ///
367 /// On the other hand, hovering requires hit testing on almost every frame.
368 /// This is handled in [RendererBinding] and [MouseTracker], and will ignore
369 /// the results cached here.
370 final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};
371
372 /// Dispatch an event to the targets found by a hit test on its position.
373 ///
374 /// This method sends the given event to [dispatchEvent] based on event types:
375 ///
376 /// * [PointerDownEvent]s and [PointerSignalEvent]s are dispatched to the
377 /// result of a new [hitTest].
378 /// * [PointerUpEvent]s and [PointerMoveEvent]s are dispatched to the result of hit test of the
379 /// preceding [PointerDownEvent]s.
380 /// * [PointerHoverEvent]s, [PointerAddedEvent]s, and [PointerRemovedEvent]s
381 /// are dispatched without a hit test result.
382 void handlePointerEvent(PointerEvent event) {
383 assert(!locked);
384
385 if (resamplingEnabled) {
386 _resampler.addOrDispatch(event);
387 _resampler.sample(samplingOffset, samplingClock);
388 return;
389 }
390
391 // Stop resampler if resampling is not enabled. This is a no-op if
392 // resampling was never enabled.
393 _resampler.stop();
394 _handlePointerEventImmediately(event);
395 }
396
397 void _handlePointerEventImmediately(PointerEvent event) {
398 HitTestResult? hitTestResult;
399 if (event is PointerDownEvent ||
400 event is PointerSignalEvent ||
401 event is PointerHoverEvent ||
402 event is PointerPanZoomStartEvent) {
403 assert(
404 !_hitTests.containsKey(event.pointer),
405 'Pointer of ${event.toString(minLevel: DiagnosticLevel.debug)} unexpectedly has a HitTestResult associated with it.',
406 );
407 hitTestResult = HitTestResult();
408 hitTestInView(hitTestResult, event.position, event.viewId);
409 if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
410 _hitTests[event.pointer] = hitTestResult;
411 }
412 assert(() {
413 if (debugPrintHitTestResults) {
414 debugPrint('${event.toString(minLevel: DiagnosticLevel.debug)}: $hitTestResult');
415 }
416 return true;
417 }());
418 } else if (event is PointerUpEvent ||
419 event is PointerCancelEvent ||
420 event is PointerPanZoomEndEvent) {
421 hitTestResult = _hitTests.remove(event.pointer);
422 } else if (event.down || event is PointerPanZoomUpdateEvent) {
423 // Because events that occur with the pointer down (like
424 // [PointerMoveEvent]s) should be dispatched to the same place that their
425 // initial PointerDownEvent was, we want to re-use the path we found when
426 // the pointer went down, rather than do hit detection each time we get
427 // such an event.
428 hitTestResult = _hitTests[event.pointer];
429 }
430 assert(() {
431 if (debugPrintMouseHoverEvents && event is PointerHoverEvent) {
432 debugPrint('$event');
433 }
434 return true;
435 }());
436 if (hitTestResult != null || event is PointerAddedEvent || event is PointerRemovedEvent) {
437 dispatchEvent(event, hitTestResult);
438 }
439 }
440
441 /// Determine which [HitTestTarget] objects are located at a given position in
442 /// the specified view.
443 @override // from HitTestable
444 void hitTestInView(HitTestResult result, Offset position, int viewId) {
445 result.add(HitTestEntry(this));
446 }
447
448 @override // from HitTestable
449 @Deprecated(
450 'Use hitTestInView and specify the view to hit test. '
451 'This feature was deprecated after v3.11.0-20.0.pre.',
452 )
453 void hitTest(HitTestResult result, Offset position) {
454 hitTestInView(result, position, platformDispatcher.implicitView!.viewId);
455 }
456
457 /// Dispatch an event to [pointerRouter] and the path of a hit test result.
458 ///
459 /// The `event` is routed to [pointerRouter]. If the `hitTestResult` is not
460 /// null, the event is also sent to every [HitTestTarget] in the entries of the
461 /// given [HitTestResult]. Any exceptions from the handlers are caught.
462 ///
463 /// The `hitTestResult` argument may only be null for [PointerAddedEvent]s or
464 /// [PointerRemovedEvent]s.
465 @override // from HitTestDispatcher
466 @pragma('vm:notify-debugger-on-exception')
467 void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
468 assert(!locked);
469 // No hit test information implies that this is a [PointerAddedEvent] or
470 // [PointerRemovedEvent]. These events are specially routed here; other
471 // events will be routed through the `handleEvent` below.
472 if (hitTestResult == null) {
473 assert(event is PointerAddedEvent || event is PointerRemovedEvent);
474 try {
475 pointerRouter.route(event);
476 } catch (exception, stack) {
477 FlutterError.reportError(
478 FlutterErrorDetailsForPointerEventDispatcher(
479 exception: exception,
480 stack: stack,
481 library: 'gesture library',
482 context: ErrorDescription('while dispatching a non-hit-tested pointer event'),
483 event: event,
484 informationCollector: () => <DiagnosticsNode>[
485 DiagnosticsProperty<PointerEvent>(
486 'Event',
487 event,
488 style: DiagnosticsTreeStyle.errorProperty,
489 ),
490 ],
491 ),
492 );
493 }
494 return;
495 }
496 for (final HitTestEntry entry in hitTestResult.path) {
497 try {
498 entry.target.handleEvent(event.transformed(entry.transform), entry);
499 } catch (exception, stack) {
500 FlutterError.reportError(
501 FlutterErrorDetailsForPointerEventDispatcher(
502 exception: exception,
503 stack: stack,
504 library: 'gesture library',
505 context: ErrorDescription('while dispatching a pointer event'),
506 event: event,
507 hitTestEntry: entry,
508 informationCollector: () => <DiagnosticsNode>[
509 DiagnosticsProperty<PointerEvent>(
510 'Event',
511 event,
512 style: DiagnosticsTreeStyle.errorProperty,
513 ),
514 DiagnosticsProperty<HitTestTarget>(
515 'Target',
516 entry.target,
517 style: DiagnosticsTreeStyle.errorProperty,
518 ),
519 ],
520 ),
521 );
522 }
523 }
524 }
525
526 @override // from HitTestTarget
527 void handleEvent(PointerEvent event, HitTestEntry entry) {
528 pointerRouter.route(event);
529 if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
530 gestureArena.close(event.pointer);
531 } else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
532 gestureArena.sweep(event.pointer);
533 } else if (event is PointerSignalEvent) {
534 pointerSignalResolver.resolve(event);
535 }
536 }
537
538 /// Reset states of [GestureBinding].
539 ///
540 /// This clears the hit test records.
541 ///
542 /// This is typically called between tests.
543 @protected
544 void resetGestureBinding() {
545 _hitTests.clear();
546 }
547
548 void _handleSampleTimeChanged() {
549 if (!locked) {
550 if (resamplingEnabled) {
551 _resampler.sample(samplingOffset, samplingClock);
552 } else {
553 _resampler.stop();
554 }
555 }
556 }
557
558 /// Overrides the sampling clock for debugging and testing.
559 ///
560 /// This value is ignored in non-debug builds.
561 @protected
562 SamplingClock? get debugSamplingClock => null;
563
564 /// Provides access to the current [DateTime] and `StopWatch` objects for
565 /// sampling.
566 ///
567 /// Overridden by [debugSamplingClock] for debug builds and testing. Using
568 /// this object under test will maintain synchronization with [FakeAsync].
569 SamplingClock get samplingClock {
570 SamplingClock value = SamplingClock();
571 assert(() {
572 final SamplingClock? debugValue = debugSamplingClock;
573 if (debugValue != null) {
574 value = debugValue;
575 }
576 return true;
577 }());
578 return value;
579 }
580
581 // Resampler used to filter incoming pointer events when resampling
582 // is enabled.
583 late final _Resampler _resampler = _Resampler(
584 _handlePointerEventImmediately,
585 _handleSampleTimeChanged,
586 _samplingInterval,
587 );
588
589 /// Enable pointer event resampling for touch devices by setting
590 /// this to true.
591 ///
592 /// Resampling results in smoother touch event processing at the
593 /// cost of some added latency. Devices with low frequency sensors
594 /// or when the frequency is not a multiple of the display frequency
595 /// (e.g., 120Hz input and 90Hz display) benefit from this.
596 ///
597 /// This is typically set during application initialization but
598 /// can be adjusted dynamically in case the application only
599 /// wants resampling for some period of time.
600 bool resamplingEnabled = false;
601
602 /// Offset relative to current frame time that should be used for
603 /// resampling. The [samplingOffset] is expected to be negative.
604 /// Non-negative [samplingOffset] is allowed but will effectively
605 /// disable resampling.
606 Duration samplingOffset = _defaultSamplingOffset;
607}
608
609/// Variant of [FlutterErrorDetails] with extra fields for the gesture
610/// library's binding's pointer event dispatcher ([GestureBinding.dispatchEvent]).
611class FlutterErrorDetailsForPointerEventDispatcher extends FlutterErrorDetails {
612 /// Creates a [FlutterErrorDetailsForPointerEventDispatcher] object with the given
613 /// arguments setting the object's properties.
614 ///
615 /// The gesture library calls this constructor when catching an exception
616 /// that will subsequently be reported using [FlutterError.onError].
617 const FlutterErrorDetailsForPointerEventDispatcher({
618 required super.exception,
619 super.stack,
620 super.library,
621 super.context,
622 this.event,
623 this.hitTestEntry,
624 super.informationCollector,
625 super.silent,
626 });
627
628 /// The pointer event that was being routed when the exception was raised.
629 final PointerEvent? event;
630
631 /// The hit test result entry for the object whose handleEvent method threw
632 /// the exception. May be null if no hit test entry is associated with the
633 /// event (e.g. [PointerHoverEvent]s, [PointerAddedEvent]s, and
634 /// [PointerRemovedEvent]s).
635 ///
636 /// The target object itself is given by the [HitTestEntry.target] property of
637 /// the hitTestEntry object.
638 final HitTestEntry? hitTestEntry;
639}
640