Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

1919 lines
62 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'gesture_tester.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
testGesture('Should recognize scale gestures', (GestureTester tester) {
final scale = ScaleGestureRecognizer();
final tap = TapGestureRecognizer();
var didStartScale = false;
Offset? updatedFocalPoint;
int? updatedPointerCount;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
updatedPointerCount = details.pointerCount;
};
double? updatedScale;
double? updatedHorizontalScale;
double? updatedVerticalScale;
Offset? updatedDelta;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
updatedHorizontalScale = details.horizontalScale;
updatedVerticalScale = details.verticalScale;
updatedFocalPoint = details.focalPoint;
updatedDelta = details.focalPointDelta;
updatedPointerCount = details.pointerCount;
};
var didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
var didTap = false;
tap.onTap = () {
didTap = true;
};
final pointer1 = TestPointer();
final PointerDownEvent down = pointer1.down(Offset.zero);
scale.addPointer(down);
tap.addPointer(down);
tester.closeArena(1);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isFalse);
expect(didTap, isFalse);
// One-finger panning
tester.route(down);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isFalse);
expect(didTap, isFalse);
tester.route(pointer1.move(const Offset(20.0, 30.0)));
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(updatedPointerCount, 1);
updatedPointerCount = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
// Two-finger scaling
final pointer2 = TestPointer(2);
final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0));
scale.addPointer(down2);
tap.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
expect(didEndScale, isTrue);
didEndScale = false;
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didStartScale, isFalse);
// Zoom in
tester.route(pointer2.move(const Offset(0.0, 10.0)));
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(updatedPointerCount, 2);
updatedScale = null;
updatedHorizontalScale = null;
updatedVerticalScale = null;
updatedDelta = null;
updatedPointerCount = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
// Zoom out
tester.route(pointer2.move(const Offset(15.0, 25.0)));
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(updatedPointerCount, 2);
expect(didTap, isFalse);
// Horizontal scaling
tester.route(pointer2.move(const Offset(0.0, 20.0)));
expect(updatedHorizontalScale, 2.0);
expect(updatedVerticalScale, 1.0);
expect(updatedPointerCount, 2);
// Vertical scaling
tester.route(pointer2.move(const Offset(10.0, 10.0)));
expect(updatedHorizontalScale, 1.0);
expect(updatedVerticalScale, 2.0);
expect(updatedDelta, const Offset(5.0, -5.0));
expect(updatedPointerCount, 2);
tester.route(pointer2.move(const Offset(15.0, 25.0)));
updatedFocalPoint = null;
updatedScale = null;
updatedDelta = null;
updatedPointerCount = null;
// Three-finger scaling
final pointer3 = TestPointer(3);
final PointerDownEvent down3 = pointer3.down(const Offset(25.0, 35.0));
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(updatedPointerCount, isNull);
expect(didStartScale, isFalse);
// Zoom in
tester.route(pointer3.move(const Offset(55.0, 65.0)));
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(updatedPointerCount, 3);
updatedPointerCount = 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)));
tester.route(pointer2.move(const Offset(20.0, 30.0)));
tester.route(pointer3.move(const Offset(15.0, 25.0)));
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(updatedPointerCount, 3);
updatedPointerCount = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
tester.route(pointer1.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
expect(didTap, isFalse);
// Continue scaling with two fingers
tester.route(pointer3.move(const Offset(10.0, 20.0)));
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(updatedPointerCount, 2);
updatedPointerCount = null;
// Continue rotating with two fingers
tester.route(pointer3.move(const Offset(30.0, 40.0)));
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;
tester.route(pointer3.move(const Offset(10.0, 20.0)));
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(updatedPointerCount, 2);
updatedPointerCount = null;
tester.route(pointer2.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
expect(didTap, isFalse);
// Continue panning with one finger
tester.route(pointer3.move(Offset.zero));
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(updatedPointerCount, 1);
updatedPointerCount = null;
// We are done
tester.route(pointer3.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
expect(didTap, isFalse);
scale.dispose();
tap.dispose();
});
testGesture('Rejects scale gestures from unallowed device kinds', (GestureTester tester) {
final scale = ScaleGestureRecognizer(
supportedDevices: <PointerDeviceKind>{PointerDeviceKind.touch},
);
var didStartScale = false;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
};
double? updatedScale;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
};
final mousePointer = TestPointer(1, PointerDeviceKind.mouse);
final PointerDownEvent down = mousePointer.down(Offset.zero);
scale.addPointer(down);
tester.closeArena(1);
// One-finger panning
tester.route(down);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
// Using a mouse, the scale gesture shouldn't even start.
tester.route(mousePointer.move(const Offset(20.0, 30.0)));
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
scale.dispose();
});
testGesture(
'Scale gestures starting from allowed device kinds cannot be ended from unallowed devices',
(GestureTester tester) {
final scale = ScaleGestureRecognizer(
supportedDevices: <PointerDeviceKind>{PointerDeviceKind.touch},
);
var didStartScale = false;
Offset? updatedFocalPoint;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
};
double? updatedScale;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
updatedFocalPoint = details.focalPoint;
};
var didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
final touchPointer = TestPointer();
final PointerDownEvent down = touchPointer.down(Offset.zero);
scale.addPointer(down);
tester.closeArena(1);
// One-finger panning
tester.route(down);
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedScale, isNull);
expect(updatedFocalPoint, Offset.zero);
expect(didEndScale, isFalse);
// The gesture can start using one touch finger.
tester.route(touchPointer.move(const Offset(20.0, 30.0)));
expect(updatedFocalPoint, const Offset(20.0, 30.0));
updatedFocalPoint = null;
expect(updatedScale, 1.0);
updatedScale = null;
expect(didEndScale, isFalse);
// Two-finger scaling
final mousePointer = TestPointer(2, PointerDeviceKind.mouse);
final PointerDownEvent down2 = mousePointer.down(const Offset(10.0, 20.0));
scale.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
// Mouse-generated events are ignored.
expect(didEndScale, isFalse);
expect(updatedScale, isNull);
expect(didStartScale, isFalse);
// Zoom in using a mouse doesn't work either.
tester.route(mousePointer.move(const Offset(0.0, 10.0)));
expect(updatedScale, isNull);
expect(didEndScale, isFalse);
scale.dispose();
},
);
testGesture('Scale gesture competes with drag', (GestureTester tester) {
final scale = ScaleGestureRecognizer();
final drag = HorizontalDragGestureRecognizer();
final log = <String>[];
scale.onStart = (ScaleStartDetails details) {
log.add('scale-start');
};
scale.onUpdate = (ScaleUpdateDetails details) {
log.add('scale-update');
};
scale.onEnd = (ScaleEndDetails details) {
log.add('scale-end');
};
drag.onStart = (DragStartDetails details) {
log.add('drag-start');
};
drag.onEnd = (DragEndDetails details) {
log.add('drag-end');
};
final pointer1 = TestPointer();
final PointerDownEvent down = pointer1.down(const Offset(10.0, 10.0));
scale.addPointer(down);
drag.addPointer(down);
tester.closeArena(1);
expect(log, isEmpty);
// Vertical moves are scales.
tester.route(down);
expect(log, isEmpty);
// Scale will win if focal point delta exceeds 18.0*2.
tester.route(pointer1.move(const Offset(10.0, 50.0))); // Delta of 40.0 exceeds 18.0*2.
expect(log, equals(<String>['scale-start', 'scale-update']));
log.clear();
final pointer2 = TestPointer(2);
final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0));
scale.addPointer(down2);
drag.addPointer(down2);
tester.closeArena(2);
expect(log, isEmpty);
// Second pointer joins scale even though it moves horizontally.
tester.route(down2);
expect(log, <String>['scale-end']);
log.clear();
tester.route(pointer2.move(const Offset(30.0, 20.0)));
expect(log, equals(<String>['scale-start', 'scale-update']));
log.clear();
tester.route(pointer1.up());
expect(log, equals(<String>['scale-end']));
log.clear();
tester.route(pointer2.up());
expect(log, isEmpty);
log.clear();
// Horizontal moves are either drags or scales, depending on which wins first.
// TODO(ianh): https://github.com/flutter/flutter/issues/11384
// In this case, we move fast, so that the scale wins. If we moved slowly,
// the horizontal drag would win, since it was added first.
final pointer3 = TestPointer(3);
final PointerDownEvent down3 = pointer3.down(const Offset(30.0, 30.0));
scale.addPointer(down3);
drag.addPointer(down3);
tester.closeArena(3);
tester.route(down3);
expect(log, isEmpty);
tester.route(pointer3.move(const Offset(100.0, 30.0)));
expect(log, equals(<String>['scale-start', 'scale-update']));
log.clear();
tester.route(pointer3.up());
expect(log, equals(<String>['scale-end']));
log.clear();
scale.dispose();
drag.dispose();
});
testGesture('Should recognize rotation gestures', (GestureTester tester) {
final scale = ScaleGestureRecognizer();
final tap = TapGestureRecognizer();
var didStartScale = false;
Offset? updatedFocalPoint;
int? updatedPointerCount;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
updatedPointerCount = details.pointerCount;
};
double? updatedRotation;
Offset? updatedDelta;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedRotation = details.rotation;
updatedFocalPoint = details.focalPoint;
updatedDelta = details.focalPointDelta;
updatedPointerCount = details.pointerCount;
};
var didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
var didTap = false;
tap.onTap = () {
didTap = true;
};
final pointer1 = TestPointer();
final PointerDownEvent down = pointer1.down(Offset.zero);
scale.addPointer(down);
tap.addPointer(down);
tester.closeArena(1);
expect(didStartScale, isFalse);
expect(updatedRotation, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isFalse);
expect(didTap, isFalse);
tester.route(down);
tester.route(pointer1.move(const Offset(20.0, 30.0)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, const Offset(20.0, 30.0));
updatedFocalPoint = null;
expect(updatedDelta, const Offset(20.0, 30.0));
updatedDelta = null;
expect(updatedRotation, 0.0);
updatedRotation = null;
expect(updatedPointerCount, 1);
updatedPointerCount = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
// Two-finger scaling
final pointer2 = TestPointer(2);
final PointerDownEvent down2 = pointer2.down(const Offset(30.0, 40.0));
scale.addPointer(down2);
tap.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
expect(didEndScale, isTrue);
didEndScale = false;
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedRotation, isNull);
expect(updatedPointerCount, isNull);
expect(didStartScale, isFalse);
// Zoom in
tester.route(pointer2.move(const Offset(40.0, 50.0)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, const Offset(30.0, 40.0));
updatedFocalPoint = null;
expect(updatedDelta, const Offset(5.0, 5.0));
updatedDelta = null;
expect(updatedRotation, 0.0);
updatedRotation = null;
expect(updatedPointerCount, 2);
updatedPointerCount = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
// Rotation
tester.route(pointer2.move(const Offset(0.0, 10.0)));
expect(updatedFocalPoint, const Offset(10.0, 20.0));
updatedFocalPoint = null;
expect(updatedDelta, const Offset(-20.0, -20.0));
updatedDelta = null;
expect(updatedRotation, math.pi);
updatedRotation = null;
expect(updatedPointerCount, 2);
updatedPointerCount = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
// Three-finger scaling
final pointer3 = TestPointer(3);
final PointerDownEvent down3 = pointer3.down(const Offset(25.0, 35.0));
scale.addPointer(down3);
tap.addPointer(down3);
tester.closeArena(3);
tester.route(down3);
expect(didEndScale, isTrue);
didEndScale = false;
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedRotation, isNull);
expect(updatedPointerCount, isNull);
expect(didStartScale, isFalse);
// Zoom in
tester.route(pointer3.move(const Offset(55.0, 65.0)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, const Offset(25.0, 35.0));
updatedFocalPoint = null;
expect(updatedDelta, const Offset(10.0, 10.0));
updatedDelta = null;
expect(updatedRotation, 0.0);
updatedRotation = null;
expect(updatedPointerCount, 3);
updatedPointerCount = 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)));
tester.route(pointer2.move(const Offset(20.0, 30.0)));
tester.route(pointer3.move(const Offset(15.0, 25.0)));
expect(didStartScale, isFalse);
expect(updatedFocalPoint, const Offset(20.0, 30.0));
updatedFocalPoint = null;
expect(updatedDelta!.dx, closeTo(-13.3, 0.1));
expect(updatedDelta!.dy, closeTo(-13.3, 0.1));
updatedDelta = null;
expect(updatedRotation, 0.0);
updatedRotation = null;
expect(updatedPointerCount, 3);
updatedPointerCount = null;
expect(didEndScale, isFalse);
expect(didTap, isFalse);
tester.route(pointer1.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedRotation, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
expect(didTap, isFalse);
// Continue scaling with two fingers
tester.route(pointer3.move(const Offset(10.0, 20.0)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, const Offset(15.0, 25.0));
updatedFocalPoint = null;
expect(updatedDelta, const Offset(-2.5, -2.5));
updatedDelta = null;
expect(updatedRotation, 0.0);
updatedRotation = null;
expect(updatedPointerCount, 2);
updatedPointerCount = null;
// Continue rotating with two fingers
tester.route(pointer3.move(const Offset(30.0, 40.0)));
expect(updatedFocalPoint, const Offset(25.0, 35.0));
updatedFocalPoint = null;
expect(updatedDelta, const Offset(10.0, 10.0));
updatedDelta = null;
expect(updatedRotation, -math.pi);
updatedRotation = null;
expect(updatedPointerCount, 2);
updatedPointerCount = null;
tester.route(pointer3.move(const Offset(10.0, 20.0)));
expect(updatedFocalPoint, const Offset(15.0, 25.0));
updatedFocalPoint = null;
expect(updatedDelta, const Offset(-10.0, -10.0));
updatedDelta = null;
expect(updatedRotation, 0.0);
updatedRotation = null;
expect(updatedPointerCount, 2);
updatedPointerCount = null;
tester.route(pointer2.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedRotation, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
expect(didTap, isFalse);
// We are done
tester.route(pointer3.up());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedRotation, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isFalse);
didEndScale = false;
expect(didTap, isFalse);
scale.dispose();
tap.dispose();
});
// Regressing test for https://github.com/flutter/flutter/issues/78941
testGesture('First rotation test', (GestureTester tester) {
final scale = ScaleGestureRecognizer();
addTearDown(scale.dispose);
double? updatedRotation;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedRotation = details.rotation;
};
final pointer1 = TestPointer();
final PointerDownEvent down = pointer1.down(Offset.zero);
scale.addPointer(down);
tester.closeArena(1);
tester.route(down);
final pointer2 = TestPointer(2);
final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 10.0));
scale.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
expect(updatedRotation, isNull);
// Rotation 45°.
tester.route(pointer2.move(const Offset(0.0, 10.0)));
expect(updatedRotation, math.pi / 4.0);
});
testGesture('Scale gestures pointer count test', (GestureTester tester) {
final scale = ScaleGestureRecognizer();
var pointerCountOfStart = 0;
scale.onStart = (ScaleStartDetails details) => pointerCountOfStart = details.pointerCount;
var pointerCountOfUpdate = 0;
scale.onUpdate = (ScaleUpdateDetails details) => pointerCountOfUpdate = details.pointerCount;
var pointerCountOfEnd = 0;
scale.onEnd = (ScaleEndDetails details) => pointerCountOfEnd = details.pointerCount;
final pointer1 = TestPointer();
final PointerDownEvent down = pointer1.down(Offset.zero);
scale.addPointer(down);
tester.closeArena(1);
// One-finger panning
tester.route(down);
// One pointer in contact with the screen now.
expect(pointerCountOfStart, 1);
tester.route(pointer1.move(const Offset(20.0, 30.0)));
expect(pointerCountOfUpdate, 1);
// Two-finger scaling
final pointer2 = TestPointer(2);
final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0));
scale.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
// Two pointers in contact with the screen now.
expect(pointerCountOfEnd, 2); // Additional pointer down will trigger an end event.
tester.route(pointer2.move(const Offset(0.0, 10.0)));
expect(pointerCountOfStart, 2); // The new pointer move will trigger a start event.
expect(pointerCountOfUpdate, 2);
tester.route(pointer1.up());
// One pointer in contact with the screen now.
expect(pointerCountOfEnd, 1);
tester.route(pointer2.move(const Offset(0.0, 10.0)));
expect(pointerCountOfStart, 1);
expect(pointerCountOfUpdate, 1);
tester.route(pointer2.up());
// No pointer in contact with the screen now.
expect(pointerCountOfEnd, 0);
scale.dispose();
});
testGesture('Should recognize scale gestures from pointer pan/zoom events', (
GestureTester tester,
) {
final scale = ScaleGestureRecognizer();
addTearDown(scale.dispose);
final drag = HorizontalDragGestureRecognizer();
addTearDown(drag.dispose);
var didStartScale = false;
Offset? updatedFocalPoint;
int? updatedPointerCount;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
updatedPointerCount = details.pointerCount;
};
double? updatedScale;
double? updatedHorizontalScale;
double? updatedVerticalScale;
Offset? updatedDelta;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
updatedHorizontalScale = details.horizontalScale;
updatedVerticalScale = details.verticalScale;
updatedFocalPoint = details.focalPoint;
updatedDelta = details.focalPointDelta;
updatedPointerCount = details.pointerCount;
};
var didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
final pointer1 = TestPointer(2, PointerDeviceKind.trackpad);
final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero);
scale.addPointerPanZoom(start);
drag.addPointerPanZoom(start);
tester.closeArena(2);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isFalse);
// Panning.
tester.route(start);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isFalse);
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(20.0, 30.0)));
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(updatedPointerCount, 2);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// Zoom in.
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(20.0, 30.0), scale: 2.0));
expect(updatedFocalPoint, const Offset(20.0, 30.0));
updatedFocalPoint = null;
expect(updatedScale, 2.0);
expect(updatedHorizontalScale, 2.0);
expect(updatedVerticalScale, 2.0);
expect(updatedDelta, Offset.zero);
expect(updatedPointerCount, 2);
updatedScale = null;
updatedHorizontalScale = null;
updatedVerticalScale = null;
updatedDelta = null;
updatedPointerCount = null;
expect(didEndScale, isFalse);
// Zoom out.
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(20.0, 30.0)));
expect(updatedFocalPoint, const Offset(20.0, 30.0));
updatedFocalPoint = null;
expect(updatedScale, 1.0);
expect(updatedHorizontalScale, 1.0);
expect(updatedVerticalScale, 1.0);
expect(updatedDelta, Offset.zero);
expect(updatedPointerCount, 2);
updatedScale = null;
updatedHorizontalScale = null;
updatedVerticalScale = null;
updatedDelta = null;
updatedPointerCount = null;
expect(didEndScale, isFalse);
// We are done.
tester.route(pointer1.panZoomEnd());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
scale.dispose();
});
testGesture('Pointer pan/zooms should work alongside touches', (GestureTester tester) {
final scale = ScaleGestureRecognizer();
addTearDown(scale.dispose);
final drag = HorizontalDragGestureRecognizer();
addTearDown(drag.dispose);
var didStartScale = false;
Offset? updatedFocalPoint;
int? updatedPointerCount;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
updatedPointerCount = details.pointerCount;
};
double? updatedScale;
double? updatedHorizontalScale;
double? updatedVerticalScale;
Offset? updatedDelta;
double? updatedRotation;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
updatedHorizontalScale = details.horizontalScale;
updatedVerticalScale = details.verticalScale;
updatedFocalPoint = details.focalPoint;
updatedDelta = details.focalPointDelta;
updatedRotation = details.rotation;
updatedPointerCount = details.pointerCount;
};
var didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
final touchPointer1 = TestPointer(2);
final touchPointer2 = TestPointer(3);
final panZoomPointer = TestPointer(4, PointerDeviceKind.trackpad);
final PointerPanZoomStartEvent panZoomStart = panZoomPointer.panZoomStart(Offset.zero);
scale.addPointerPanZoom(panZoomStart);
drag.addPointerPanZoom(panZoomStart);
tester.closeArena(4);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isFalse);
// Panning starting with trackpad.
tester.route(panZoomStart);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isFalse);
tester.route(panZoomPointer.panZoomUpdate(Offset.zero, pan: const Offset(40, 40)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, const Offset(40.0, 40.0));
updatedFocalPoint = null;
expect(updatedScale, 1.0);
updatedScale = null;
expect(updatedDelta, const Offset(40.0, 40.0));
updatedDelta = null;
expect(updatedPointerCount, 2);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// Add a touch pointer.
final PointerDownEvent touchStart1 = touchPointer1.down(const Offset(40, 40));
scale.addPointer(touchStart1);
drag.addPointer(touchStart1);
tester.closeArena(2);
tester.route(touchStart1);
expect(didEndScale, isTrue);
didEndScale = false;
tester.route(touchPointer1.move(const Offset(10, 10)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, const Offset(25, 25));
updatedFocalPoint = null;
// 1 down pointer + pointer pan/zoom should not scale, only pan.
expect(updatedScale, 1.0);
updatedScale = null;
expect(updatedDelta, const Offset(-15, -15));
updatedDelta = null;
expect(updatedPointerCount, 3);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// Add a second touch pointer.
final PointerDownEvent touchStart2 = touchPointer2.down(const Offset(10, 40));
scale.addPointer(touchStart2);
drag.addPointer(touchStart2);
tester.closeArena(3);
tester.route(touchStart2);
expect(didEndScale, isTrue);
didEndScale = false;
// Move the second pointer to cause pan, zoom, and rotation.
tester.route(touchPointer2.move(const Offset(40, 40)));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, const Offset(30, 30));
updatedFocalPoint = null;
expect(updatedScale, math.sqrt(2));
updatedScale = null;
expect(updatedHorizontalScale, 1.0);
updatedHorizontalScale = null;
expect(updatedVerticalScale, 1.0);
updatedVerticalScale = null;
expect(updatedDelta, const Offset(10, 0));
updatedDelta = null;
expect(updatedRotation, -math.pi / 4);
updatedRotation = null;
expect(updatedPointerCount, 4);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// Change the scale and angle of the pan/zoom to test combining.
// Scale should be multiplied together.
// Rotation angle should be added together.
tester.route(
panZoomPointer.panZoomUpdate(
Offset.zero,
pan: const Offset(40, 40),
scale: math.sqrt(2),
rotation: math.pi / 3,
),
);
expect(didStartScale, isFalse);
expect(updatedFocalPoint, const Offset(30, 30));
updatedFocalPoint = null;
expect(updatedScale, closeTo(2, 0.0001));
updatedScale = null;
expect(updatedHorizontalScale, math.sqrt(2));
updatedHorizontalScale = null;
expect(updatedVerticalScale, math.sqrt(2));
updatedVerticalScale = null;
expect(updatedDelta, Offset.zero);
updatedDelta = null;
expect(updatedRotation, closeTo(math.pi / 12, 0.0001));
updatedRotation = null;
expect(updatedPointerCount, 4);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// Move the pan/zoom origin to test combining.
tester.route(
panZoomPointer.panZoomUpdate(
const Offset(15, 15),
pan: const Offset(55, 55),
scale: math.sqrt(2),
rotation: math.pi / 3,
),
);
expect(didStartScale, isFalse);
expect(updatedFocalPoint, const Offset(40, 40));
updatedFocalPoint = null;
expect(updatedScale, closeTo(2, 0.0001));
updatedScale = null;
expect(updatedDelta, const Offset(10, 10));
updatedDelta = null;
expect(updatedRotation, closeTo(math.pi / 12, 0.0001));
updatedRotation = null;
expect(updatedPointerCount, 4);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// We are done.
tester.route(panZoomPointer.panZoomEnd());
expect(updatedFocalPoint, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didStartScale, isFalse);
tester.route(touchPointer1.up());
expect(updatedFocalPoint, isNull);
expect(didEndScale, isFalse);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didStartScale, isFalse);
tester.route(touchPointer2.up());
expect(didEndScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didStartScale, isFalse);
scale.dispose();
});
testGesture('Scale gesture competes with drag for trackpad gesture', (GestureTester tester) {
final scale = ScaleGestureRecognizer();
final drag = HorizontalDragGestureRecognizer();
final log = <String>[];
scale.onStart = (ScaleStartDetails details) {
log.add('scale-start');
};
scale.onUpdate = (ScaleUpdateDetails details) {
log.add('scale-update');
};
scale.onEnd = (ScaleEndDetails details) {
log.add('scale-end');
};
drag.onStart = (DragStartDetails details) {
log.add('drag-start');
};
drag.onEnd = (DragEndDetails details) {
log.add('drag-end');
};
final pointer1 = TestPointer(2, PointerDeviceKind.trackpad);
final PointerPanZoomStartEvent down = pointer1.panZoomStart(const Offset(10.0, 10.0));
scale.addPointerPanZoom(down);
drag.addPointerPanZoom(down);
tester.closeArena(2);
expect(log, isEmpty);
// Vertical moves are scales.
tester.route(down);
expect(log, isEmpty);
// Scale will win if focal point delta exceeds 18.0*2.
tester.route(
pointer1.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(10.0, 40.0)),
); // delta of 40.0 exceeds 18.0*2.
expect(log, equals(<String>['scale-start', 'scale-update']));
log.clear();
final pointer2 = TestPointer(3, PointerDeviceKind.trackpad);
final PointerPanZoomStartEvent down2 = pointer2.panZoomStart(const Offset(10.0, 20.0));
scale.addPointerPanZoom(down2);
drag.addPointerPanZoom(down2);
tester.closeArena(3);
expect(log, isEmpty);
// Second pointer joins scale even though it moves horizontally.
tester.route(down2);
expect(log, <String>['scale-end']);
log.clear();
tester.route(pointer2.panZoomUpdate(const Offset(10.0, 20.0), pan: const Offset(20.0, 0.0)));
expect(log, equals(<String>['scale-start', 'scale-update']));
log.clear();
tester.route(pointer1.panZoomEnd());
expect(log, equals(<String>['scale-end']));
log.clear();
tester.route(pointer2.panZoomEnd());
expect(log, isEmpty);
log.clear();
// Horizontal moves are either drags or scales, depending on which wins first.
// TODO(ianh): https://github.com/flutter/flutter/issues/11384
// In this case, we move fast, so that the scale wins. If we moved slowly,
// the horizontal drag would win, since it was added first.
final pointer3 = TestPointer(4, PointerDeviceKind.trackpad);
final PointerPanZoomStartEvent down3 = pointer3.panZoomStart(const Offset(30.0, 30.0));
scale.addPointerPanZoom(down3);
drag.addPointerPanZoom(down3);
tester.closeArena(4);
tester.route(down3);
expect(log, isEmpty);
tester.route(pointer3.panZoomUpdate(const Offset(30.0, 30.0), pan: const Offset(70.0, 0.0)));
expect(log, equals(<String>['scale-start', 'scale-update']));
log.clear();
tester.route(pointer3.panZoomEnd());
expect(log, equals(<String>['scale-end']));
log.clear();
scale.dispose();
drag.dispose();
});
testGesture('Scale gesture from pan/zoom events properly handles DragStartBehavior.start', (
GestureTester tester,
) {
final scale = ScaleGestureRecognizer(dragStartBehavior: DragStartBehavior.start);
addTearDown(scale.dispose);
final drag = HorizontalDragGestureRecognizer();
addTearDown(drag.dispose);
var didStartScale = false;
Offset? updatedFocalPoint;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
};
double? updatedScale;
double? updatedHorizontalScale;
double? updatedVerticalScale;
double? updatedRotation;
Offset? updatedDelta;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
updatedHorizontalScale = details.horizontalScale;
updatedVerticalScale = details.verticalScale;
updatedFocalPoint = details.focalPoint;
updatedRotation = details.rotation;
updatedDelta = details.focalPointDelta;
};
var didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
final pointer1 = TestPointer(2, PointerDeviceKind.trackpad);
final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero);
scale.addPointerPanZoom(start);
drag.addPointerPanZoom(start);
tester.closeArena(2);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isFalse);
tester.route(start);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isFalse);
// Zoom enough to win the gesture.
tester.route(pointer1.panZoomUpdate(Offset.zero, scale: 1.1, rotation: 1));
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, 1.0);
updatedScale = null;
expect(updatedDelta, Offset.zero);
updatedDelta = null;
expect(didEndScale, isFalse);
// Zoom in - should be relative to 1.1.
tester.route(pointer1.panZoomUpdate(Offset.zero, scale: 1.21, rotation: 1.5));
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, closeTo(1.1, 0.0001));
expect(updatedHorizontalScale, closeTo(1.1, 0.0001));
expect(updatedVerticalScale, closeTo(1.1, 0.0001));
expect(updatedRotation, 0.5);
expect(updatedDelta, Offset.zero);
updatedScale = null;
updatedHorizontalScale = null;
updatedVerticalScale = null;
updatedRotation = null;
updatedDelta = null;
expect(didEndScale, isFalse);
// Zoom out - should be relative to 1.1.
tester.route(pointer1.panZoomUpdate(Offset.zero, scale: 0.99, rotation: 1.0));
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, closeTo(0.9, 0.0001));
expect(updatedHorizontalScale, closeTo(0.9, 0.0001));
expect(updatedVerticalScale, closeTo(0.9, 0.0001));
expect(updatedRotation, 0.0);
expect(updatedDelta, Offset.zero);
updatedScale = null;
updatedHorizontalScale = null;
updatedVerticalScale = null;
updatedDelta = null;
expect(didEndScale, isFalse);
// We are done.
tester.route(pointer1.panZoomEnd());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
});
testGesture('scale trackpadScrollCausesScale', (GestureTester tester) {
final scale = ScaleGestureRecognizer(
dragStartBehavior: DragStartBehavior.start,
trackpadScrollCausesScale: true,
);
var didStartScale = false;
Offset? updatedFocalPoint;
int? updatedPointerCount;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
updatedPointerCount = details.pointerCount;
};
double? updatedScale;
Offset? updatedDelta;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
updatedFocalPoint = details.focalPoint;
updatedDelta = details.focalPointDelta;
updatedPointerCount = details.pointerCount;
};
var didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
final pointer1 = TestPointer(2, PointerDeviceKind.trackpad);
final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero);
scale.addPointerPanZoom(start);
tester.closeArena(2);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isFalse);
tester.route(start);
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedScale, isNull);
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedDelta, isNull);
expect(updatedPointerCount, 2);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// Zoom in by scrolling up.
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(0, -200)));
expect(didStartScale, isFalse);
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, math.e);
updatedScale = null;
expect(updatedDelta, Offset.zero);
updatedDelta = null;
expect(updatedPointerCount, 2);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// A horizontal scroll should do nothing.
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(200, -200)));
expect(didStartScale, isFalse);
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, math.e);
updatedScale = null;
expect(updatedDelta, Offset.zero);
updatedDelta = null;
expect(updatedPointerCount, 2);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// End.
tester.route(pointer1.panZoomEnd());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
// Try with a different trackpadScrollToScaleFactor
scale.trackpadScrollToScaleFactor = const Offset(1 / 125, 0);
final PointerPanZoomStartEvent start2 = pointer1.panZoomStart(Offset.zero);
scale.addPointerPanZoom(start2);
tester.closeArena(2);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
expect(updatedFocalPoint, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isFalse);
tester.route(start2);
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedScale, isNull);
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedDelta, isNull);
expect(updatedPointerCount, 2);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// Zoom in by scrolling left.
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(125, 0)));
expect(didStartScale, isFalse);
didStartScale = false;
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, math.e);
updatedScale = null;
expect(updatedDelta, Offset.zero);
updatedDelta = null;
expect(updatedPointerCount, 2);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// A vertical scroll should do nothing.
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(125, 125)));
expect(didStartScale, isFalse);
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(updatedScale, math.e);
updatedScale = null;
expect(updatedDelta, Offset.zero);
updatedDelta = null;
expect(updatedPointerCount, 2);
updatedPointerCount = null;
expect(didEndScale, isFalse);
// End.
tester.route(pointer1.panZoomEnd());
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(updatedScale, isNull);
expect(updatedDelta, isNull);
expect(updatedPointerCount, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
scale.dispose();
});
testGesture('scale ending velocity', (GestureTester tester) {
final scale = ScaleGestureRecognizer(
dragStartBehavior: DragStartBehavior.start,
trackpadScrollCausesScale: true,
);
var didStartScale = false;
Offset? updatedFocalPoint;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
};
var didEndScale = false;
double? scaleEndVelocity;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
scaleEndVelocity = details.scaleVelocity;
};
final pointer1 = TestPointer(2, PointerDeviceKind.trackpad);
final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero);
scale.addPointerPanZoom(start);
tester.closeArena(2);
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(didEndScale, isFalse);
tester.route(start);
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedFocalPoint, Offset.zero);
updatedFocalPoint = null;
expect(didEndScale, isFalse);
// Zoom in by scrolling up.
for (var i = 0; i < 100; i++) {
tester.route(
pointer1.panZoomUpdate(
Offset.zero,
pan: Offset(0, i * -10),
timeStamp: Duration(milliseconds: i * 25),
),
);
}
// End.
tester.route(pointer1.panZoomEnd(timeStamp: const Duration(milliseconds: 2500)));
expect(didStartScale, isFalse);
expect(updatedFocalPoint, isNull);
expect(didEndScale, isTrue);
didEndScale = false;
expect(scaleEndVelocity, moreOrLessEquals(281.41454098027765));
scale.dispose();
});
testGesture(
'ScaleStartDetails and ScaleUpdateDetails callbacks should contain their event.timestamp',
(GestureTester tester) {
final scale = ScaleGestureRecognizer();
final tap = TapGestureRecognizer();
var 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;
};
var didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
var didTap = false;
tap.onTap = () {
didTap = true;
};
final 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 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 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();
},
);
testGesture('ScaleStartDetails should contain the correct PointerDeviceKind', (
GestureTester tester,
) {
final scale = ScaleGestureRecognizer();
var didStartScale = false;
PointerDeviceKind? updatedKind;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedKind = details.kind;
};
scale.onEnd = (ScaleEndDetails details) {
didStartScale = false;
};
// The default kind is touch.
// ignore: avoid_redundant_argument_values
final pointer1 = TestPointer(1, PointerDeviceKind.touch);
final PointerDownEvent down = pointer1.down(Offset.zero);
scale.addPointer(down);
tester.closeArena(1);
// One-finger panning
tester.route(down);
tester.route(pointer1.move(const Offset(20.0, 30.0)));
expect(didStartScale, isTrue);
expect(updatedKind, PointerDeviceKind.touch);
tester.route(pointer1.up());
expect(didStartScale, isFalse);
final pointer2 = TestPointer(2, PointerDeviceKind.mouse);
final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0));
scale.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
tester.route(pointer2.move(const Offset(20.0, 30.0)));
expect(didStartScale, isTrue);
expect(updatedKind, PointerDeviceKind.mouse);
tester.route(pointer2.up());
expect(didStartScale, isFalse);
final pointer3 = TestPointer(3, PointerDeviceKind.stylus);
final PointerDownEvent down3 = pointer3.down(const Offset(10.0, 20.0));
scale.addPointer(down3);
tester.closeArena(3);
tester.route(down3);
tester.route(pointer3.move(const Offset(20.0, 30.0)));
expect(didStartScale, isTrue);
expect(updatedKind, PointerDeviceKind.stylus);
tester.route(pointer3.up());
expect(didStartScale, isFalse);
final pointer4 = TestPointer(4, PointerDeviceKind.invertedStylus);
final PointerDownEvent down4 = pointer4.down(const Offset(10.0, 20.0));
scale.addPointer(down4);
tester.closeArena(4);
tester.route(down4);
tester.route(pointer4.move(const Offset(20.0, 30.0)));
expect(didStartScale, isTrue);
expect(updatedKind, PointerDeviceKind.invertedStylus);
tester.route(pointer4.up());
expect(didStartScale, isFalse);
final pointer5 = TestPointer(5, PointerDeviceKind.unknown);
final PointerDownEvent down5 = pointer5.down(const Offset(10.0, 20.0));
scale.addPointer(down5);
tester.closeArena(5);
tester.route(down5);
tester.route(pointer5.move(const Offset(20.0, 30.0)));
expect(didStartScale, isTrue);
expect(updatedKind, PointerDeviceKind.unknown);
tester.route(pointer5.up());
expect(didStartScale, isFalse);
final pointer6 = TestPointer(6, PointerDeviceKind.trackpad);
final PointerPanZoomStartEvent down6 = pointer6.panZoomStart(const Offset(10.0, 20.0));
scale.addPointerPanZoom(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(pointer6.panZoomUpdate(const Offset(20.0, 30.0)));
expect(didStartScale, isTrue);
expect(updatedKind, PointerDeviceKind.trackpad);
tester.route(pointer6.panZoomEnd());
expect(didStartScale, isFalse);
scale.dispose();
});
}