Unverified Commit 29898812 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[framework] make hit slop based on device pointer kind for drag/pan/scale gestures (#64267)

Currently the framework uses fairly large "hit slop" values to disambiguate taps from drags/pans. This makes sense on touch devices where the interaction is not very precise, on mice however it can feel as if the UI is lagging. This is immediately noticeable on our infra dashboard, where it takes almost half of a grid square of drag before the actual drag kicks in.

One potential solution is to always use smaller constants depending on whether the interaction is mouse or touch based. The only reasonable choice is to use the pointer device kind and not target platform - same platform can have different input sources. This requires exposing the pointer device kind in a few new places in several of the gesture detectors, and using the enum to compute the correct hit slop from an expanded set of constants.

This almost works, however there are a few places (notably ListViews) which uses the touch hit slop as a default value in scroll physics. It does not seem like it will be easy to disambiguate a user provided scroll physics constant from the default and/or adjust it somehow - this might require significant changes to scroll physics which I have left out of this PR.

This PR does not adjust:

kTouchSlop used in scroll_physics.dart's minFlingDistance
kTouchSlop used in PrimaryPointerGestureRecognizer/LongPressGestureRecognizer
parent c33d8f4e
...@@ -20,8 +20,8 @@ void main() { ...@@ -20,8 +20,8 @@ void main() {
assert(false, "Don't run benchmarks in checked mode! Use 'flutter run --release'."); assert(false, "Don't run benchmarks in checked mode! Use 'flutter run --release'.");
final BenchmarkResultPrinter printer = BenchmarkResultPrinter(); final BenchmarkResultPrinter printer = BenchmarkResultPrinter();
final List<TrackerBenchmark> benchmarks = <TrackerBenchmark>[ final List<TrackerBenchmark> benchmarks = <TrackerBenchmark>[
TrackerBenchmark(name: 'velocity_tracker_iteration', tracker: VelocityTracker()), TrackerBenchmark(name: 'velocity_tracker_iteration', tracker: VelocityTracker(PointerDeviceKind.touch)),
TrackerBenchmark(name: 'velocity_tracker_iteration_ios_fling', tracker: IOSScrollViewFlingVelocityTracker()), TrackerBenchmark(name: 'velocity_tracker_iteration_ios_fling', tracker: IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch)),
]; ];
final Stopwatch watch = Stopwatch(); final Stopwatch watch = Stopwatch();
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// Modeled after Android's ViewConfiguration: // Modeled after Android's ViewConfiguration:
// https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/ViewConfiguration.java // https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/ViewConfiguration.java
...@@ -95,3 +94,12 @@ const double kMaxFlingVelocity = 8000.0; // Logical pixels / second ...@@ -95,3 +94,12 @@ const double kMaxFlingVelocity = 8000.0; // Logical pixels / second
/// tap in a jump-tap gesture. /// tap in a jump-tap gesture.
// TODO(ianh): Implement jump-tap gestures. // TODO(ianh): Implement jump-tap gestures.
const Duration kJumpTapTimeout = Duration(milliseconds: 500); const Duration kJumpTapTimeout = Duration(milliseconds: 500);
/// Like [kTouchSlop], but for more precise pointers like mice and trackpads.
const double kPrecisePointerHitSlop = 1.0; // Logical pixels;
/// Like [kPanSlop], but for more precise pointers like mice and trackpads.
const double kPrecisePointerPanSlop = kPrecisePointerHitSlop * 2.0; // Logical pixels
/// Like [kScaleSlop], but for more precise pointers like mice and trackpads.
const double kPrecisePointerScaleSlop = kPrecisePointerHitSlop; // Logical pixels
...@@ -8,6 +8,8 @@ import 'dart:ui' show Offset, PointerDeviceKind; ...@@ -8,6 +8,8 @@ import 'dart:ui' show Offset, PointerDeviceKind;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
import 'constants.dart';
export 'dart:ui' show Offset, PointerDeviceKind; export 'dart:ui' show Offset, PointerDeviceKind;
/// The bit of [PointerEvent.buttons] that corresponds to a cross-device /// The bit of [PointerEvent.buttons] that corresponds to a cross-device
...@@ -2221,3 +2223,42 @@ class PointerCancelEvent extends PointerEvent { ...@@ -2221,3 +2223,42 @@ class PointerCancelEvent extends PointerEvent {
); );
} }
} }
/// Determine the approriate hit slop pixels based on the [kind] of pointer.
double computeHitSlop(PointerDeviceKind kind) {
switch (kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
return kPrecisePointerHitSlop;
case PointerDeviceKind.unknown:
case PointerDeviceKind.touch:
return kTouchSlop;
}
}
/// Determine the approriate pan slop pixels based on the [kind] of pointer.
double computePanSlop(PointerDeviceKind kind) {
switch (kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
return kPrecisePointerPanSlop;
case PointerDeviceKind.unknown:
case PointerDeviceKind.touch:
return kPanSlop;
}
}
/// Determine the approriate scale slop pixels based on the [kind] of pointer.
double computeScaleSlop(PointerDeviceKind kind) {
switch (kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
return kPrecisePointerScaleSlop;
case PointerDeviceKind.unknown:
case PointerDeviceKind.touch:
return kScaleSlop;
}
}
...@@ -2,13 +2,11 @@ ...@@ -2,13 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show Offset; import 'dart:ui' show Offset;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'arena.dart'; import 'arena.dart';
import 'constants.dart';
import 'events.dart'; import 'events.dart';
import 'recognizer.dart'; import 'recognizer.dart';
...@@ -254,7 +252,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -254,7 +252,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
if (pressure > startPressure) { if (pressure > startPressure) {
_state = _ForceState.started; _state = _ForceState.started;
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
} else if (event.delta.distanceSquared > kTouchSlop) { } else if (event.delta.distanceSquared > computeHitSlop(event.kind)) {
resolve(GestureDisposition.rejected); resolve(GestureDisposition.rejected);
} }
} }
......
...@@ -376,7 +376,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { ...@@ -376,7 +376,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
void handlePrimaryPointer(PointerEvent event) { void handlePrimaryPointer(PointerEvent event) {
if (!event.synthesized) { if (!event.synthesized) {
if (event is PointerDownEvent) { if (event is PointerDownEvent) {
_velocityTracker = VelocityTracker(); _velocityTracker = VelocityTracker(event.kind);
_velocityTracker!.addPosition(event.timeStamp, event.localPosition); _velocityTracker!.addPosition(event.timeStamp, event.localPosition);
} }
if (event is PointerMoveEvent) { if (event is PointerMoveEvent) {
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
...@@ -70,7 +69,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -70,7 +69,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
}) : assert(dragStartBehavior != null), }) : assert(dragStartBehavior != null),
super(debugOwner: debugOwner, kind: kind); super(debugOwner: debugOwner, kind: kind);
static VelocityTracker _defaultBuilder(PointerEvent ev) => VelocityTracker(); static VelocityTracker _defaultBuilder(PointerEvent event) => VelocityTracker(event.kind);
/// Configure the behavior of offsets sent to [onStart]. /// Configure the behavior of offsets sent to [onStart].
/// ///
/// If set to [DragStartBehavior.start], the [onStart] callback will be called /// If set to [DragStartBehavior.start], the [onStart] callback will be called
...@@ -218,11 +217,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -218,11 +217,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// A fling calls its gesture end callback with a velocity, allowing the /// A fling calls its gesture end callback with a velocity, allowing the
/// provider of the callback to respond by carrying the gesture forward with /// provider of the callback to respond by carrying the gesture forward with
/// inertia, for example. /// inertia, for example.
bool isFlingGesture(VelocityEstimate estimate); bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind);
Offset _getDeltaForDetails(Offset delta); Offset _getDeltaForDetails(Offset delta);
double? _getPrimaryValueFromOffset(Offset value); double? _getPrimaryValueFromOffset(Offset value);
bool get _hasSufficientGlobalDistanceToAccept; bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind);
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{}; final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
...@@ -302,7 +301,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -302,7 +301,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
untransformedDelta: movedLocally, untransformedDelta: movedLocally,
untransformedEndPosition: event.localPosition, untransformedEndPosition: event.localPosition,
).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign; ).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
if (_hasSufficientGlobalDistanceToAccept) if (_hasSufficientGlobalDistanceToAccept(event.kind))
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
} }
} }
...@@ -444,7 +443,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -444,7 +443,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
String Function() debugReport; String Function() debugReport;
final VelocityEstimate? estimate = tracker.getVelocityEstimate(); final VelocityEstimate? estimate = tracker.getVelocityEstimate();
if (estimate != null && isFlingGesture(estimate)) { if (estimate != null && isFlingGesture(estimate, tracker.kind)) {
final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond) final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
.clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity); .clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
details = DragEndDetails( details = DragEndDetails(
...@@ -506,14 +505,16 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer { ...@@ -506,14 +505,16 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
}) : super(debugOwner: debugOwner, kind: kind); }) : super(debugOwner: debugOwner, kind: kind);
@override @override
bool isFlingGesture(VelocityEstimate estimate) { bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity; final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
final double minDistance = minFlingDistance ?? kTouchSlop; final double minDistance = minFlingDistance ?? computeHitSlop(kind);
return estimate.pixelsPerSecond.dy.abs() > minVelocity && estimate.offset.dy.abs() > minDistance; return estimate.pixelsPerSecond.dy.abs() > minVelocity && estimate.offset.dy.abs() > minDistance;
} }
@override @override
bool get _hasSufficientGlobalDistanceToAccept => _globalDistanceMoved.abs() > kTouchSlop; bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind) {
return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind);
}
@override @override
Offset _getDeltaForDetails(Offset delta) => Offset(0.0, delta.dy); Offset _getDeltaForDetails(Offset delta) => Offset(0.0, delta.dy);
...@@ -545,14 +546,16 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer { ...@@ -545,14 +546,16 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
}) : super(debugOwner: debugOwner, kind: kind); }) : super(debugOwner: debugOwner, kind: kind);
@override @override
bool isFlingGesture(VelocityEstimate estimate) { bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity; final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
final double minDistance = minFlingDistance ?? kTouchSlop; final double minDistance = minFlingDistance ?? computeHitSlop(kind);
return estimate.pixelsPerSecond.dx.abs() > minVelocity && estimate.offset.dx.abs() > minDistance; return estimate.pixelsPerSecond.dx.abs() > minVelocity && estimate.offset.dx.abs() > minDistance;
} }
@override @override
bool get _hasSufficientGlobalDistanceToAccept => _globalDistanceMoved.abs() > kTouchSlop; bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind) {
return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind);
}
@override @override
Offset _getDeltaForDetails(Offset delta) => Offset(delta.dx, 0.0); Offset _getDeltaForDetails(Offset delta) => Offset(delta.dx, 0.0);
...@@ -578,16 +581,16 @@ class PanGestureRecognizer extends DragGestureRecognizer { ...@@ -578,16 +581,16 @@ class PanGestureRecognizer extends DragGestureRecognizer {
PanGestureRecognizer({ Object? debugOwner }) : super(debugOwner: debugOwner); PanGestureRecognizer({ Object? debugOwner }) : super(debugOwner: debugOwner);
@override @override
bool isFlingGesture(VelocityEstimate estimate) { bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity; final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
final double minDistance = minFlingDistance ?? kTouchSlop; final double minDistance = minFlingDistance ?? computeHitSlop(kind);
return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity
&& estimate.offset.distanceSquared > minDistance * minDistance; && estimate.offset.distanceSquared > minDistance * minDistance;
} }
@override @override
bool get _hasSufficientGlobalDistanceToAccept { bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind) {
return _globalDistanceMoved.abs() > kPanSlop; return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind);
} }
@override @override
......
...@@ -28,13 +28,20 @@ abstract class MultiDragPointerState { ...@@ -28,13 +28,20 @@ abstract class MultiDragPointerState {
/// Creates per-pointer state for a [MultiDragGestureRecognizer]. /// Creates per-pointer state for a [MultiDragGestureRecognizer].
/// ///
/// The [initialPosition] argument must not be null. /// The [initialPosition] argument must not be null.
MultiDragPointerState(this.initialPosition) MultiDragPointerState(this.initialPosition, this.kind)
: assert(initialPosition != null); : assert(initialPosition != null),
_velocityTracker = VelocityTracker(kind);
/// The global coordinates of the pointer when the pointer contacted the screen. /// The global coordinates of the pointer when the pointer contacted the screen.
final Offset initialPosition; final Offset initialPosition;
final VelocityTracker _velocityTracker = VelocityTracker(); final VelocityTracker _velocityTracker;
/// The kind of pointer performing the multi-drag gesture.
///
/// Used by subclasses to determine the appropriate hit slop, for example.
final PointerDeviceKind kind;
Drag? _client; Drag? _client;
/// The offset of the pointer from the last position that was reported to the client. /// The offset of the pointer from the last position that was reported to the client.
...@@ -309,12 +316,12 @@ abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> exten ...@@ -309,12 +316,12 @@ abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> exten
} }
class _ImmediatePointerState extends MultiDragPointerState { class _ImmediatePointerState extends MultiDragPointerState {
_ImmediatePointerState(Offset initialPosition) : super(initialPosition); _ImmediatePointerState(Offset initialPosition, PointerDeviceKind kind) : super(initialPosition, kind);
@override @override
void checkForResolutionAfterMove() { void checkForResolutionAfterMove() {
assert(pendingDelta != null); assert(pendingDelta != null);
if (pendingDelta!.distance > kTouchSlop) if (pendingDelta!.distance > computeHitSlop(kind))
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
} }
...@@ -349,7 +356,7 @@ class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Im ...@@ -349,7 +356,7 @@ class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Im
@override @override
_ImmediatePointerState createNewPointerState(PointerDownEvent event) { _ImmediatePointerState createNewPointerState(PointerDownEvent event) {
return _ImmediatePointerState(event.position); return _ImmediatePointerState(event.position, event.kind);
} }
@override @override
...@@ -358,12 +365,12 @@ class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Im ...@@ -358,12 +365,12 @@ class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Im
class _HorizontalPointerState extends MultiDragPointerState { class _HorizontalPointerState extends MultiDragPointerState {
_HorizontalPointerState(Offset initialPosition) : super(initialPosition); _HorizontalPointerState(Offset initialPosition, PointerDeviceKind kind) : super(initialPosition, kind);
@override @override
void checkForResolutionAfterMove() { void checkForResolutionAfterMove() {
assert(pendingDelta != null); assert(pendingDelta != null);
if (pendingDelta!.dx.abs() > kTouchSlop) if (pendingDelta!.dx.abs() > computeHitSlop(kind))
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
} }
...@@ -398,7 +405,7 @@ class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_H ...@@ -398,7 +405,7 @@ class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_H
@override @override
_HorizontalPointerState createNewPointerState(PointerDownEvent event) { _HorizontalPointerState createNewPointerState(PointerDownEvent event) {
return _HorizontalPointerState(event.position); return _HorizontalPointerState(event.position, event.kind);
} }
@override @override
...@@ -407,12 +414,12 @@ class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_H ...@@ -407,12 +414,12 @@ class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_H
class _VerticalPointerState extends MultiDragPointerState { class _VerticalPointerState extends MultiDragPointerState {
_VerticalPointerState(Offset initialPosition) : super(initialPosition); _VerticalPointerState(Offset initialPosition, PointerDeviceKind kind) : super(initialPosition, kind);
@override @override
void checkForResolutionAfterMove() { void checkForResolutionAfterMove() {
assert(pendingDelta != null); assert(pendingDelta != null);
if (pendingDelta!.dy.abs() > kTouchSlop) if (pendingDelta!.dy.abs() > computeHitSlop(kind))
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
} }
...@@ -447,7 +454,7 @@ class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Ver ...@@ -447,7 +454,7 @@ class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Ver
@override @override
_VerticalPointerState createNewPointerState(PointerDownEvent event) { _VerticalPointerState createNewPointerState(PointerDownEvent event) {
return _VerticalPointerState(event.position); return _VerticalPointerState(event.position, event.kind);
} }
@override @override
...@@ -455,9 +462,9 @@ class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Ver ...@@ -455,9 +462,9 @@ class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Ver
} }
class _DelayedPointerState extends MultiDragPointerState { class _DelayedPointerState extends MultiDragPointerState {
_DelayedPointerState(Offset initialPosition, Duration delay) _DelayedPointerState(Offset initialPosition, Duration delay, PointerDeviceKind kind)
: assert(delay != null), : assert(delay != null),
super(initialPosition) { super(initialPosition, kind) {
_timer = Timer(delay, _delayPassed); _timer = Timer(delay, _delayPassed);
} }
...@@ -467,7 +474,7 @@ class _DelayedPointerState extends MultiDragPointerState { ...@@ -467,7 +474,7 @@ class _DelayedPointerState extends MultiDragPointerState {
void _delayPassed() { void _delayPassed() {
assert(_timer != null); assert(_timer != null);
assert(pendingDelta != null); assert(pendingDelta != null);
assert(pendingDelta!.distance <= kTouchSlop); assert(pendingDelta!.distance <= computeHitSlop(kind));
_timer = null; _timer = null;
if (_starter != null) { if (_starter != null) {
_starter!(initialPosition); _starter!(initialPosition);
...@@ -504,7 +511,7 @@ class _DelayedPointerState extends MultiDragPointerState { ...@@ -504,7 +511,7 @@ class _DelayedPointerState extends MultiDragPointerState {
return; return;
} }
assert(pendingDelta != null); assert(pendingDelta != null);
if (pendingDelta!.distance > kTouchSlop) { if (pendingDelta!.distance > computeHitSlop(kind)) {
resolve(GestureDisposition.rejected); resolve(GestureDisposition.rejected);
_ensureTimerStopped(); _ensureTimerStopped();
} }
...@@ -555,7 +562,7 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Dela ...@@ -555,7 +562,7 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Dela
@override @override
_DelayedPointerState createNewPointerState(PointerDownEvent event) { _DelayedPointerState createNewPointerState(PointerDownEvent event) {
return _DelayedPointerState(event.position, delay); return _DelayedPointerState(event.position, delay, event.kind);
} }
@override @override
......
...@@ -398,7 +398,7 @@ class _TapGesture extends _TapTracker { ...@@ -398,7 +398,7 @@ class _TapGesture extends _TapTracker {
void handleEvent(PointerEvent event) { void handleEvent(PointerEvent event) {
assert(event.pointer == pointer); assert(event.pointer == pointer);
if (event is PointerMoveEvent) { if (event is PointerMoveEvent) {
if (!isWithinGlobalTolerance(event, kTouchSlop)) if (!isWithinGlobalTolerance(event, computeHitSlop(event.kind)))
cancel(); cancel();
else else
_lastPosition = OffsetPair.fromEventPosition(event); _lastPosition = OffsetPair.fromEventPosition(event);
......
...@@ -286,7 +286,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -286,7 +286,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
@override @override
void addAllowedPointer(PointerEvent event) { void addAllowedPointer(PointerEvent event) {
startTrackingPointer(event.pointer, event.transform); startTrackingPointer(event.pointer, event.transform);
_velocityTrackers[event.pointer] = VelocityTracker(); _velocityTrackers[event.pointer] = VelocityTracker(event.kind);
if (_state == _ScaleState.ready) { if (_state == _ScaleState.ready) {
_state = _ScaleState.possible; _state = _ScaleState.possible;
_initialSpan = 0.0; _initialSpan = 0.0;
...@@ -329,7 +329,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -329,7 +329,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_update(); _update();
if (!didChangeConfiguration || _reconfigure(event.pointer)) if (!didChangeConfiguration || _reconfigure(event.pointer))
_advanceStateMachine(shouldStartIfAccepted); _advanceStateMachine(shouldStartIfAccepted, event.kind);
stopTrackingIfPointerNoLongerDown(event); stopTrackingIfPointerNoLongerDown(event);
} }
...@@ -414,14 +414,14 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -414,14 +414,14 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
return true; return true;
} }
void _advanceStateMachine(bool shouldStartIfAccepted) { void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) {
if (_state == _ScaleState.ready) if (_state == _ScaleState.ready)
_state = _ScaleState.possible; _state = _ScaleState.possible;
if (_state == _ScaleState.possible) { if (_state == _ScaleState.possible) {
final double spanDelta = (_currentSpan - _initialSpan).abs(); final double spanDelta = (_currentSpan - _initialSpan).abs();
final double focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance; final double focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance;
if (spanDelta > kScaleSlop || focalPointDelta > kPanSlop) if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind))
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
} else if (_state.index >= _ScaleState.accepted.index) { } else if (_state.index >= _ScaleState.accepted.index) {
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
......
...@@ -7,6 +7,7 @@ import 'dart:ui' show Offset; ...@@ -7,6 +7,7 @@ import 'dart:ui' show Offset;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'events.dart';
import 'lsq_solver.dart'; import 'lsq_solver.dart';
export 'dart:ui' show Offset; export 'dart:ui' show Offset;
...@@ -147,11 +148,17 @@ class _PointAtTime { ...@@ -147,11 +148,17 @@ class _PointAtTime {
/// The quality of the velocity estimation will be better if more data points /// The quality of the velocity estimation will be better if more data points
/// have been received. /// have been received.
class VelocityTracker { class VelocityTracker {
/// Create a new velocity tracker for a pointer [kind].
VelocityTracker(this.kind);
static const int _assumePointerMoveStoppedMilliseconds = 40; static const int _assumePointerMoveStoppedMilliseconds = 40;
static const int _historySize = 20; static const int _historySize = 20;
static const int _horizonMilliseconds = 100; static const int _horizonMilliseconds = 100;
static const int _minSampleSize = 3; static const int _minSampleSize = 3;
/// The kind of pointer this tracker is for.
final PointerDeviceKind kind;
// Circular buffer; current sample at _index. // Circular buffer; current sample at _index.
final List<_PointAtTime?> _samples = List<_PointAtTime?>.filled(_historySize, null, growable: false); final List<_PointAtTime?> _samples = List<_PointAtTime?>.filled(_historySize, null, growable: false);
int _index = 0; int _index = 0;
...@@ -273,6 +280,9 @@ class VelocityTracker { ...@@ -273,6 +280,9 @@ class VelocityTracker {
/// * [scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)](https://developer.apple.com/documentation/uikit/uiscrollviewdelegate/1619385-scrollviewwillenddragging), /// * [scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)](https://developer.apple.com/documentation/uikit/uiscrollviewdelegate/1619385-scrollviewwillenddragging),
/// the iOS method that reports the fling velocity when the touch is released. /// the iOS method that reports the fling velocity when the touch is released.
class IOSScrollViewFlingVelocityTracker extends VelocityTracker { class IOSScrollViewFlingVelocityTracker extends VelocityTracker {
/// Create a new IOSScrollViewFlingVelocityTracker.
IOSScrollViewFlingVelocityTracker(PointerDeviceKind kind) : super(kind);
/// The velocity estimation uses at most 4 `_PointAtTime` samples. The extra /// The velocity estimation uses at most 4 `_PointAtTime` samples. The extra
/// samples are there to make the `VelocityEstimate.offset` sufficiently large /// samples are there to make the `VelocityEstimate.offset` sufficiently large
/// to be recognized as a fling. See /// to be recognized as a fling. See
......
...@@ -73,15 +73,15 @@ class ScrollBehavior { ...@@ -73,15 +73,15 @@ class ScrollBehavior {
switch (getPlatform(context)) { switch (getPlatform(context)) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS: case TargetPlatform.macOS:
return (PointerEvent ev) => IOSScrollViewFlingVelocityTracker(); return (PointerEvent event) => IOSScrollViewFlingVelocityTracker(event.kind);
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.linux: case TargetPlatform.linux:
case TargetPlatform.windows: case TargetPlatform.windows:
return (PointerEvent ev) => VelocityTracker(); return (PointerEvent event) => VelocityTracker(event.kind);
} }
assert(false); assert(false);
return (PointerEvent ev) => VelocityTracker(); return (PointerEvent event) => VelocityTracker(event.kind);
} }
static const ScrollPhysics _bouncingPhysics = BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics()); static const ScrollPhysics _bouncingPhysics = BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics());
......
...@@ -38,6 +38,26 @@ void main() { ...@@ -38,6 +38,26 @@ void main() {
expect(isSingleButton(0x220), isFalse); expect(isSingleButton(0x220), isFalse);
}); });
test('computed hit slop values are based on pointer device kind', () {
expect(computeHitSlop(PointerDeviceKind.mouse), kPrecisePointerHitSlop);
expect(computeHitSlop(PointerDeviceKind.stylus), kPrecisePointerHitSlop);
expect(computeHitSlop(PointerDeviceKind.invertedStylus), kPrecisePointerHitSlop);
expect(computeHitSlop(PointerDeviceKind.touch), kTouchSlop);
expect(computeHitSlop(PointerDeviceKind.unknown), kTouchSlop);
expect(computePanSlop(PointerDeviceKind.mouse), kPrecisePointerPanSlop);
expect(computePanSlop(PointerDeviceKind.stylus), kPrecisePointerPanSlop);
expect(computePanSlop(PointerDeviceKind.invertedStylus), kPrecisePointerPanSlop);
expect(computePanSlop(PointerDeviceKind.touch), kPanSlop);
expect(computePanSlop(PointerDeviceKind.unknown), kPanSlop);
expect(computeScaleSlop(PointerDeviceKind.mouse), kPrecisePointerScaleSlop);
expect(computeScaleSlop(PointerDeviceKind.stylus), kPrecisePointerScaleSlop);
expect(computeScaleSlop(PointerDeviceKind.invertedStylus), kPrecisePointerScaleSlop);
expect(computeScaleSlop(PointerDeviceKind.touch), kScaleSlop);
expect(computeScaleSlop(PointerDeviceKind.unknown), kScaleSlop);
});
group('fromMouseEvent', () { group('fromMouseEvent', () {
const PointerEvent hover = PointerHoverEvent( const PointerEvent hover = PointerHoverEvent(
timeStamp: Duration(days: 1), timeStamp: Duration(days: 1),
......
...@@ -38,7 +38,7 @@ void main() { ...@@ -38,7 +38,7 @@ void main() {
]; ];
test('Velocity tracker gives expected results', () { test('Velocity tracker gives expected results', () {
final VelocityTracker tracker = VelocityTracker(); final VelocityTracker tracker = VelocityTracker(PointerDeviceKind.touch);
int i = 0; int i = 0;
for (final PointerEvent event in velocityEventData) { for (final PointerEvent event in velocityEventData) {
if (event is PointerDownEvent || event is PointerMoveEvent) if (event is PointerDownEvent || event is PointerMoveEvent)
...@@ -64,7 +64,7 @@ void main() { ...@@ -64,7 +64,7 @@ void main() {
test('Interrupted velocity estimation', () { test('Interrupted velocity estimation', () {
// Regression test for https://github.com/flutter/flutter/pull/7510 // Regression test for https://github.com/flutter/flutter/pull/7510
final VelocityTracker tracker = VelocityTracker(); final VelocityTracker tracker = VelocityTracker(PointerDeviceKind.touch);
for (final PointerEvent event in interruptedVelocityEventData) { for (final PointerEvent event in interruptedVelocityEventData) {
if (event is PointerDownEvent || event is PointerMoveEvent) if (event is PointerDownEvent || event is PointerMoveEvent)
tracker.addPosition(event.timeStamp, event.position); tracker.addPosition(event.timeStamp, event.position);
...@@ -75,12 +75,12 @@ void main() { ...@@ -75,12 +75,12 @@ void main() {
}); });
test('No data velocity estimation', () { test('No data velocity estimation', () {
final VelocityTracker tracker = VelocityTracker(); final VelocityTracker tracker = VelocityTracker(PointerDeviceKind.touch);
expect(tracker.getVelocity(), Velocity.zero); expect(tracker.getVelocity(), Velocity.zero);
}); });
test('FreeScrollStartVelocityTracker.getVelocity throws when no points', () { test('FreeScrollStartVelocityTracker.getVelocity throws when no points', () {
final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(); final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
AssertionError exception; AssertionError exception;
try { try {
tracker.getVelocity(); tracker.getVelocity();
...@@ -92,7 +92,7 @@ void main() { ...@@ -92,7 +92,7 @@ void main() {
}); });
test('FreeScrollStartVelocityTracker.getVelocity throws when the new point precedes the previous point', () { test('FreeScrollStartVelocityTracker.getVelocity throws when the new point precedes the previous point', () {
final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(); final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
AssertionError exception; AssertionError exception;
tracker.addPosition(const Duration(hours: 1), Offset.zero); tracker.addPosition(const Duration(hours: 1), Offset.zero);
...@@ -107,7 +107,7 @@ void main() { ...@@ -107,7 +107,7 @@ void main() {
}); });
test('Estimate does not throw when there are more than 1 point', () { test('Estimate does not throw when there are more than 1 point', () {
final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(); final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
Offset position = Offset.zero; Offset position = Offset.zero;
Duration time = Duration.zero; Duration time = Duration.zero;
const Offset positionDelta = Offset(0, -1); const Offset positionDelta = Offset(0, -1);
...@@ -129,7 +129,7 @@ void main() { ...@@ -129,7 +129,7 @@ void main() {
}); });
test('Makes consistent velocity estimates with consistent velocity', () { test('Makes consistent velocity estimates with consistent velocity', () {
final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(); final IOSScrollViewFlingVelocityTracker tracker = IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch);
Offset position = Offset.zero; Offset position = Offset.zero;
Duration time = Duration.zero; Duration time = Duration.zero;
const Offset positionDelta = Offset(0, -1); const Offset positionDelta = Offset(0, -1);
......
...@@ -310,7 +310,7 @@ class RangeMaintainingTestScrollBehavior extends ScrollBehavior { ...@@ -310,7 +310,7 @@ class RangeMaintainingTestScrollBehavior extends ScrollBehavior {
@override @override
GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) { GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) {
return (PointerEvent ev) => VelocityTracker(); return (PointerEvent event) => VelocityTracker(event.kind);
} }
@override @override
......
...@@ -27,8 +27,8 @@ class TestScrollBehavior extends ScrollBehavior { ...@@ -27,8 +27,8 @@ class TestScrollBehavior extends ScrollBehavior {
@override @override
GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) { GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) {
lastCreatedBuilder = flag lastCreatedBuilder = flag
? (PointerEvent ev) => VelocityTracker() ? (PointerEvent ev) => VelocityTracker(ev.kind)
: (PointerEvent ev) => IOSScrollViewFlingVelocityTracker(); : (PointerEvent ev) => IOSScrollViewFlingVelocityTracker(ev.kind);
return lastCreatedBuilder; return lastCreatedBuilder;
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment