Unverified Commit 443d8617 authored by yakagami's avatar yakagami Committed by GitHub

add sourceTimeStamp to ScaleUpdateDetails (#135936)

This PR adds the ability to get the `sourceTimeStamp` from `ScaleUpdateDetails` in a `GestureScaleUpdateCallback` like so:

```dart
onScaleUpdate: (ScaleUpdateDetails details){
  print(details.sourceTimeStamp);
}
```

`sourceTimeStamp` is necessary when tracking velocity eg.

```dart
VelocityTracker tracker = VelocityTracker.withKind(PointerDeviceKind.touch);
///...
onScaleUpdate: (ScaleUpdateDetails details){
  tracker.addPosition(details.sourceTimeStamp!, details.focalPoint);
}
```

The docs say:

>Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan. Just use the scale gesture recognizer.

Currently this is not entirely accurate, and should be fixed, as noted in https://github.com/flutter/flutter/issues/43833#issuecomment-548133779. This PR does not add `sourceTimeStamp` to `ScaleStartDetails` because it is more involved. Specifically, `ScaleStartDetails` can be created in `acceptGesture` which does not have access to the `PointerEvent` to get the `event.timeStamp` (https://github.com/flutter/flutter/blob/54fa25543243e3bf31af6af0c1fef6adabc1d5c1/packages/flutter/lib/src/gestures/scale.dart#L730C5-L730C5).

fixes https://github.com/flutter/flutter/issues/135873. See also https://github.com/flutter/flutter/issues/43833 which added delta and https://github.com/flutter/flutter/issues/49025 which added `numPointers` to `ScaleUpdateDetails` for the reason given above. `sourceTimeStamp` should probably be added to `ScaleStartDetails` as well because it exists in `DragStartDetails` and therefore in `onPanStart`.

I am not sure how to add tests for this, any input about this PR would be appreciated.

- [] All existing and new tests are passing.
parent c864a556
......@@ -100,6 +100,7 @@ class ScaleStartDetails {
this.focalPoint = Offset.zero,
Offset? localFocalPoint,
this.pointerCount = 0,
this.sourceTimeStamp,
}) : localFocalPoint = localFocalPoint ?? focalPoint;
/// The initial focal point of the pointers in contact with the screen.
......@@ -129,6 +130,12 @@ class ScaleStartDetails {
/// recognizer.
final int pointerCount;
/// Recorded timestamp of the source pointer event that triggered the scale
/// event.
///
/// Could be null if triggered from proxied events such as accessibility.
final Duration? sourceTimeStamp;
@override
String toString() => 'ScaleStartDetails(focalPoint: $focalPoint, localFocalPoint: $localFocalPoint, pointersCount: $pointerCount)';
}
......@@ -148,6 +155,7 @@ class ScaleUpdateDetails {
this.rotation = 0.0,
this.pointerCount = 0,
this.focalPointDelta = Offset.zero,
this.sourceTimeStamp,
}) : assert(scale >= 0.0),
assert(horizontalScale >= 0.0),
assert(verticalScale >= 0.0),
......@@ -225,6 +233,12 @@ class ScaleUpdateDetails {
/// recognizer.
final int pointerCount;
/// Recorded timestamp of the source pointer event that triggered the scale
/// event.
///
/// Could be null if triggered from proxied events such as accessibility.
final Duration? sourceTimeStamp;
@override
String toString() => 'ScaleUpdateDetails('
'focalPoint: $focalPoint,'
......@@ -234,7 +248,8 @@ class ScaleUpdateDetails {
' verticalScale: $verticalScale,'
' rotation: $rotation,'
' pointerCount: $pointerCount,'
' focalPointDelta: $focalPointDelta)';
' focalPointDelta: $focalPointDelta,'
' sourceTimeStamp: $sourceTimeStamp)';
}
/// Details for [GestureScaleEndCallback].
......@@ -415,6 +430,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
final Map<int, _PointerPanZoomData> _pointerPanZooms = <int, _PointerPanZoomData>{};
double _initialPanZoomScaleFactor = 1;
double _initialPanZoomRotationFactor = 0;
Duration? _initialEventTimestamp;
double get _pointerScaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
......@@ -475,6 +491,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
_velocityTrackers[event.pointer] = VelocityTracker.withKind(event.kind);
_initialEventTimestamp = event.timeStamp;
if (_state == _ScaleState.ready) {
_state = _ScaleState.possible;
_initialSpan = 0.0;
......@@ -494,6 +511,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
super.addAllowedPointerPanZoom(event);
startTrackingPointer(event.pointer, event.transform);
_velocityTrackers[event.pointer] = VelocityTracker.withKind(event.kind);
_initialEventTimestamp = event.timeStamp;
if (_state == _ScaleState.ready) {
_state = _ScaleState.possible;
_initialPanZoomScaleFactor = 1.0;
......@@ -690,6 +708,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
}
if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
_initialEventTimestamp = event.timeStamp;
_state = _ScaleState.started;
_dispatchOnStartCallbackIfNeeded();
}
......@@ -707,6 +726,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
rotation: _computeRotationFactor(),
pointerCount: pointerCount,
focalPointDelta: _delta,
sourceTimeStamp: event.timeStamp
));
});
}
......@@ -721,9 +741,11 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
focalPoint: _currentFocalPoint!,
localFocalPoint: _localFocalPoint,
pointerCount: pointerCount,
sourceTimeStamp: _initialEventTimestamp,
));
});
}
_initialEventTimestamp = null;
}
@override
......
......@@ -1356,4 +1356,296 @@ void main() {
scale.dispose();
});
testGesture('ScaleStartDetails and ScaleUpdateDetails callbacks should contain their event.timestamp', (GestureTester tester) {
final ScaleGestureRecognizer scale = ScaleGestureRecognizer();
final TapGestureRecognizer tap = TapGestureRecognizer();
bool didStartScale = false;
Offset? updatedFocalPoint;
Duration? initialSourceTimestamp;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
initialSourceTimestamp = details.sourceTimeStamp;
};
double? updatedScale;
double? updatedHorizontalScale;
double? updatedVerticalScale;
Offset? updatedDelta;
Duration? updatedSourceTimestamp;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
updatedHorizontalScale = details.horizontalScale;
updatedVerticalScale = details.verticalScale;
updatedFocalPoint = details.focalPoint;
updatedDelta = details.focalPointDelta;
updatedSourceTimestamp = details.sourceTimeStamp;
};
bool didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
bool didTap = false;
tap.onTap = () {
didTap = true;
};
final TestPointer pointer1 = TestPointer();
final PointerDownEvent down = pointer1.down(Offset.zero, timeStamp: const Duration(milliseconds: 10));
scale.addPointer(down);
tap.addPointer(down);
tester.closeArena(1);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedSourceTimestamp, isNull);
expect(didEndScale, isFalse);
expect(didTap, isFalse);
expect(initialSourceTimestamp, isNull);
// One-finger panning.
tester.route(down);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isFalse);
expect(didTap, isFalse);
expect(initialSourceTimestamp, isNull);
tester.route(pointer1.move(const Offset(20.0, 30.0), timeStamp: const Duration(milliseconds: 20)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, const Offset(20.0, 30.0));
updatedFocalPoint = null;
expect(updatedScale, 1.0);
updatedScale = null;
expect(updatedDelta, const Offset(20.0, 30.0));
updatedDelta = null;
expect(updatedSourceTimestamp, const Duration(milliseconds: 20));
updatedSourceTimestamp = null;
expect(initialSourceTimestamp, const Duration(milliseconds: 10));
initialSourceTimestamp = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
expect(scale.pointerCount, 1);
// Two-finger scaling.
final TestPointer pointer2 = TestPointer(2);
final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0, ), timeStamp: const Duration(milliseconds: 30));
scale.addPointer(down2);
tap.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
expect(scale.pointerCount, 2);
expect(didEndScale, isTrue);
didEndScale = false;
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedSourceTimestamp, isNull);
expect(didStartScale, isFalse);
expect(initialSourceTimestamp, isNull);
// Zoom in.
tester.route(pointer2.move(const Offset(0.0, 10.0), timeStamp: const Duration(milliseconds: 40)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, const Offset(10.0, 20.0));
updatedFocalPoint = null;
expect(updatedScale, 2.0);
expect(updatedHorizontalScale, 2.0);
expect(updatedVerticalScale, 2.0);
expect(updatedDelta, const Offset(-5.0, -5.0));
expect(updatedSourceTimestamp, const Duration(milliseconds: 40));
expect(initialSourceTimestamp, const Duration(milliseconds: 40));
updatedScale = null;
updatedHorizontalScale = null;
updatedVerticalScale = null;
updatedDelta = null;
updatedSourceTimestamp = null;
initialSourceTimestamp = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
// Zoom out.
tester.route(pointer2.move(const Offset(15.0, 25.0), timeStamp: const Duration(milliseconds: 50)));
expect(updatedFocalPoint, const Offset(17.5, 27.5));
expect(updatedScale, 0.5);
expect(updatedHorizontalScale, 0.5);
expect(updatedVerticalScale, 0.5);
expect(updatedDelta, const Offset(7.5, 7.5));
expect(updatedSourceTimestamp, const Duration(milliseconds: 50));
expect(didTap, isFalse);
expect(initialSourceTimestamp, isNull);
// Horizontal scaling.
tester.route(pointer2.move(const Offset(0.0, 20.0), timeStamp: const Duration(milliseconds: 60)));
expect(updatedHorizontalScale, 2.0);
expect(updatedVerticalScale, 1.0);
expect(updatedSourceTimestamp, const Duration(milliseconds: 60));
expect(initialSourceTimestamp, isNull);
// Vertical scaling.
tester.route(pointer2.move(const Offset(10.0, 10.0), timeStamp: const Duration(milliseconds: 70)));
expect(updatedHorizontalScale, 1.0);
expect(updatedVerticalScale, 2.0);
expect(updatedDelta, const Offset(5.0, -5.0));
expect(updatedSourceTimestamp, const Duration(milliseconds: 70));
expect(initialSourceTimestamp, isNull);
tester.route(pointer2.move(const Offset(15.0, 25.0)));
updatedFocalPoint = null;
updatedScale = null;
updatedDelta = null;
updatedSourceTimestamp = null;
// Three-finger scaling.
final TestPointer pointer3 = TestPointer(3);
final PointerDownEvent down3 = pointer3.down(const Offset(25.0, 35.0), timeStamp: const Duration(milliseconds: 80));
scale.addPointer(down3);
tap.addPointer(down3);
tester.closeArena(3);
tester.route(down3);
expect(didEndScale, isTrue);
didEndScale = false;
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(didStartScale, isFalse);
expect(initialSourceTimestamp, isNull);
// Zoom in.
tester.route(pointer3.move(const Offset(55.0, 65.0), timeStamp: const Duration(milliseconds: 90)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, const Offset(30.0, 40.0));
updatedFocalPoint = null;
expect(updatedScale, 5.0);
updatedScale = null;
expect(updatedDelta, const Offset(10.0, 10.0));
updatedDelta = null;
expect(updatedSourceTimestamp, const Duration(milliseconds: 90));
updatedSourceTimestamp = null;
expect(initialSourceTimestamp, const Duration(milliseconds: 90));
initialSourceTimestamp = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
// Return to original positions but with different fingers.
tester.route(pointer1.move(const Offset(25.0, 35.0), timeStamp: const Duration(milliseconds: 100)));
tester.route(pointer2.move(const Offset(20.0, 30.0), timeStamp: const Duration(milliseconds: 110)));
tester.route(pointer3.move(const Offset(15.0, 25.0), timeStamp: const Duration(milliseconds: 120)));
expect(didStartScale, isFalse);
expect(updatedFocalPoint, const Offset(20.0, 30.0));
updatedFocalPoint = null;
expect(updatedScale, 1.0);
updatedScale = null;
expect(updatedDelta!.dx, closeTo(-13.3, 0.1));
expect(updatedDelta!.dy, closeTo(-13.3, 0.1));
updatedDelta = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
expect(updatedSourceTimestamp, const Duration(milliseconds: 120));
updatedSourceTimestamp = null;
expect(initialSourceTimestamp, isNull);
tester.route(pointer1.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isTrue);
expect(updatedSourceTimestamp, isNull);
expect(initialSourceTimestamp, isNull);
didEndScale = false;
expect(didTap, isFalse);
// Continue scaling with two fingers.
tester.route(pointer3.move(const Offset(10.0, 20.0), timeStamp: const Duration(milliseconds: 130)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, const Offset(15.0, 25.0));
updatedFocalPoint = null;
expect(updatedScale, 2.0);
updatedScale = null;
expect(updatedDelta, const Offset(-2.5, -2.5));
updatedDelta = null;
expect(updatedSourceTimestamp, const Duration(milliseconds: 130));
updatedSourceTimestamp = null;
expect(initialSourceTimestamp, const Duration(milliseconds: 130));
initialSourceTimestamp = null;
// Continue rotating with two fingers.
tester.route(pointer3.move(const Offset(30.0, 40.0), timeStamp: const Duration(milliseconds: 140)));
expect(updatedFocalPoint, const Offset(25.0, 35.0));
updatedFocalPoint = null;
expect(updatedScale, 2.0);
updatedScale = null;
expect(updatedDelta, const Offset(10.0, 10.0));
updatedDelta = null;
expect(updatedSourceTimestamp, const Duration(milliseconds: 140));
updatedSourceTimestamp = null;
expect(initialSourceTimestamp, isNull);
tester.route(pointer3.move(const Offset(10.0, 20.0), timeStamp: const Duration(milliseconds: 140)));
expect(updatedFocalPoint, const Offset(15.0, 25.0));
updatedFocalPoint = null;
expect(updatedScale, 2.0);
updatedScale = null;
expect(updatedDelta, const Offset(-10.0, -10.0));
updatedDelta = null;
expect(updatedSourceTimestamp, const Duration(milliseconds: 140));
updatedSourceTimestamp = null;
expect(initialSourceTimestamp, isNull);
tester.route(pointer2.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(updatedSourceTimestamp, isNull);
expect(initialSourceTimestamp, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
expect(didTap, isFalse);
// Continue panning with one finger.
tester.route(pointer3.move(Offset.zero, timeStamp: const Duration(milliseconds: 150)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, 1.0);
updatedScale = null;
expect(updatedDelta, const Offset(-10.0, -20.0));
updatedDelta = null;
expect(updatedSourceTimestamp, const Duration(milliseconds: 150));
updatedSourceTimestamp = null;
expect(initialSourceTimestamp, const Duration(milliseconds: 150));
initialSourceTimestamp = null;
// We are done.
tester.route(pointer3.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isTrue);
expect(updatedSourceTimestamp, isNull);
expect(initialSourceTimestamp, isNull);
didEndScale = false;
expect(didTap, isFalse);
scale.dispose();
tap.dispose();
});
}
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