mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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
1130 lines
32 KiB
Dart
1130 lines
32 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 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'gesture_tester.dart';
|
|
|
|
class TestGestureArenaMember extends GestureArenaMember {
|
|
@override
|
|
void acceptGesture(int key) {}
|
|
|
|
@override
|
|
void rejectGesture(int key) {}
|
|
}
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
// Down/up pair 1: normal tap sequence
|
|
const down1 = PointerDownEvent(pointer: 1, position: Offset(10.0, 10.0));
|
|
|
|
const up1 = PointerUpEvent(pointer: 1, position: Offset(11.0, 9.0));
|
|
|
|
// Down/up pair 2: normal tap sequence far away from pair 1
|
|
const down2 = PointerDownEvent(pointer: 2, position: Offset(30.0, 30.0));
|
|
|
|
const up2 = PointerUpEvent(pointer: 2, position: Offset(31.0, 29.0));
|
|
|
|
// Down/move/up sequence 3: intervening motion, more than kTouchSlop. (~21px)
|
|
const down3 = PointerDownEvent(pointer: 3, position: Offset(10.0, 10.0));
|
|
|
|
const move3 = PointerMoveEvent(pointer: 3, position: Offset(25.0, 25.0));
|
|
|
|
const up3 = PointerUpEvent(pointer: 3, position: Offset(25.0, 25.0));
|
|
|
|
// Down/move/up sequence 4: intervening motion, less than kTouchSlop. (~17px)
|
|
const down4 = PointerDownEvent(pointer: 4, position: Offset(10.0, 10.0));
|
|
|
|
const move4 = PointerMoveEvent(pointer: 4, position: Offset(22.0, 22.0));
|
|
|
|
const up4 = PointerUpEvent(pointer: 4, position: Offset(22.0, 22.0));
|
|
|
|
// Down/up sequence 5: tap sequence with secondary button
|
|
const down5 = PointerDownEvent(
|
|
pointer: 5,
|
|
position: Offset(20.0, 20.0),
|
|
buttons: kSecondaryButton,
|
|
);
|
|
|
|
const up5 = PointerUpEvent(pointer: 5, position: Offset(20.0, 20.0));
|
|
|
|
// Down/up sequence 6: tap sequence with tertiary button
|
|
const down6 = PointerDownEvent(
|
|
pointer: 6,
|
|
position: Offset(20.0, 20.0),
|
|
buttons: kTertiaryButton,
|
|
);
|
|
|
|
const up6 = PointerUpEvent(pointer: 6, position: Offset(20.0, 20.0));
|
|
|
|
testGesture('Should recognize tap', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
addTearDown(tap.dispose);
|
|
|
|
var tapRecognized = false;
|
|
tap.onTap = () {
|
|
tapRecognized = true;
|
|
};
|
|
|
|
tap.addPointer(down1);
|
|
tester.closeArena(1);
|
|
expect(tapRecognized, isFalse);
|
|
tester.route(down1);
|
|
expect(tapRecognized, isFalse);
|
|
|
|
tester.route(up1);
|
|
expect(tapRecognized, isTrue);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(tapRecognized, isTrue);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('Should recognize tap for supported devices only', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer(
|
|
supportedDevices: <PointerDeviceKind>{PointerDeviceKind.mouse, PointerDeviceKind.stylus},
|
|
);
|
|
|
|
var tapRecognized = false;
|
|
tap.onTap = () {
|
|
tapRecognized = true;
|
|
};
|
|
const touchDown = PointerDownEvent(pointer: 1, position: Offset(10.0, 10.0));
|
|
const touchUp = PointerUpEvent(pointer: 1, position: Offset(11.0, 9.0));
|
|
|
|
tap.addPointer(touchDown);
|
|
tester.closeArena(1);
|
|
expect(tapRecognized, isFalse);
|
|
tester.route(touchDown);
|
|
expect(tapRecognized, isFalse);
|
|
|
|
tester.route(touchUp);
|
|
expect(tapRecognized, isFalse);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(tapRecognized, isFalse);
|
|
|
|
const mouseDown = PointerDownEvent(
|
|
kind: PointerDeviceKind.mouse,
|
|
pointer: 1,
|
|
position: Offset(10.0, 10.0),
|
|
);
|
|
const mouseUp = PointerUpEvent(
|
|
kind: PointerDeviceKind.mouse,
|
|
pointer: 1,
|
|
position: Offset(11.0, 9.0),
|
|
);
|
|
|
|
tap.addPointer(mouseDown);
|
|
tester.closeArena(1);
|
|
expect(tapRecognized, isFalse);
|
|
tester.route(mouseDown);
|
|
expect(tapRecognized, isFalse);
|
|
|
|
tester.route(mouseUp);
|
|
expect(tapRecognized, isTrue);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(tapRecognized, isTrue);
|
|
|
|
tapRecognized = false;
|
|
|
|
const stylusDown = PointerDownEvent(
|
|
kind: PointerDeviceKind.stylus,
|
|
pointer: 1,
|
|
position: Offset(10.0, 10.0),
|
|
);
|
|
const stylusUp = PointerUpEvent(
|
|
kind: PointerDeviceKind.stylus,
|
|
pointer: 1,
|
|
position: Offset(11.0, 9.0),
|
|
);
|
|
|
|
tap.addPointer(stylusDown);
|
|
tester.closeArena(1);
|
|
expect(tapRecognized, isFalse);
|
|
tester.route(stylusDown);
|
|
expect(tapRecognized, isFalse);
|
|
|
|
tester.route(stylusUp);
|
|
expect(tapRecognized, isTrue);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(tapRecognized, isTrue);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('Details contain the correct device kind', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
|
|
TapDownDetails? lastDownDetails;
|
|
TapUpDetails? lastUpDetails;
|
|
|
|
tap.onTapDown = (TapDownDetails details) {
|
|
lastDownDetails = details;
|
|
};
|
|
tap.onTapUp = (TapUpDetails details) {
|
|
lastUpDetails = details;
|
|
};
|
|
|
|
const mouseDown = PointerDownEvent(pointer: 1, kind: PointerDeviceKind.mouse);
|
|
const mouseUp = PointerUpEvent(pointer: 1, kind: PointerDeviceKind.mouse);
|
|
|
|
tap.addPointer(mouseDown);
|
|
tester.closeArena(1);
|
|
tester.route(mouseDown);
|
|
expect(lastDownDetails?.kind, PointerDeviceKind.mouse);
|
|
|
|
tester.route(mouseUp);
|
|
expect(lastUpDetails?.kind, PointerDeviceKind.mouse);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('No duplicate tap events', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
|
|
var tapsRecognized = 0;
|
|
tap.onTap = () {
|
|
tapsRecognized++;
|
|
};
|
|
|
|
tap.addPointer(down1);
|
|
tester.closeArena(1);
|
|
expect(tapsRecognized, 0);
|
|
tester.route(down1);
|
|
expect(tapsRecognized, 0);
|
|
|
|
tester.route(up1);
|
|
expect(tapsRecognized, 1);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(tapsRecognized, 1);
|
|
|
|
tap.addPointer(down1);
|
|
tester.closeArena(1);
|
|
expect(tapsRecognized, 1);
|
|
tester.route(down1);
|
|
expect(tapsRecognized, 1);
|
|
|
|
tester.route(up1);
|
|
expect(tapsRecognized, 2);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(tapsRecognized, 2);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('Should not recognize two overlapping taps (FIFO)', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
|
|
var tapsRecognized = 0;
|
|
tap.onTap = () {
|
|
tapsRecognized++;
|
|
};
|
|
|
|
tap.addPointer(down1);
|
|
tester.closeArena(1);
|
|
expect(tapsRecognized, 0);
|
|
tester.route(down1);
|
|
expect(tapsRecognized, 0);
|
|
|
|
tap.addPointer(down2);
|
|
tester.closeArena(2);
|
|
expect(tapsRecognized, 0);
|
|
tester.route(down1);
|
|
expect(tapsRecognized, 0);
|
|
|
|
tester.route(up1);
|
|
expect(tapsRecognized, 1);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(tapsRecognized, 1);
|
|
|
|
tester.route(up2);
|
|
expect(tapsRecognized, 1);
|
|
GestureBinding.instance.gestureArena.sweep(2);
|
|
expect(tapsRecognized, 1);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('Should not recognize two overlapping taps (FILO)', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
|
|
var tapsRecognized = 0;
|
|
tap.onTap = () {
|
|
tapsRecognized++;
|
|
};
|
|
|
|
tap.addPointer(down1);
|
|
tester.closeArena(1);
|
|
expect(tapsRecognized, 0);
|
|
tester.route(down1);
|
|
expect(tapsRecognized, 0);
|
|
|
|
tap.addPointer(down2);
|
|
tester.closeArena(2);
|
|
expect(tapsRecognized, 0);
|
|
tester.route(down1);
|
|
expect(tapsRecognized, 0);
|
|
|
|
tester.route(up2);
|
|
expect(tapsRecognized, 0);
|
|
GestureBinding.instance.gestureArena.sweep(2);
|
|
expect(tapsRecognized, 0);
|
|
|
|
tester.route(up1);
|
|
expect(tapsRecognized, 1);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(tapsRecognized, 1);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('Distance cancels tap', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
|
|
var tapRecognized = false;
|
|
tap.onTap = () {
|
|
tapRecognized = true;
|
|
};
|
|
var tapCanceled = false;
|
|
tap.onTapCancel = () {
|
|
tapCanceled = true;
|
|
};
|
|
|
|
tap.addPointer(down3);
|
|
tester.closeArena(3);
|
|
expect(tapRecognized, isFalse);
|
|
expect(tapCanceled, isFalse);
|
|
tester.route(down3);
|
|
expect(tapRecognized, isFalse);
|
|
expect(tapCanceled, isFalse);
|
|
|
|
tester.route(move3);
|
|
expect(tapRecognized, isFalse);
|
|
expect(tapCanceled, isTrue);
|
|
tester.route(up3);
|
|
expect(tapRecognized, isFalse);
|
|
expect(tapCanceled, isTrue);
|
|
GestureBinding.instance.gestureArena.sweep(3);
|
|
expect(tapRecognized, isFalse);
|
|
expect(tapCanceled, isTrue);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('Short distance does not cancel tap', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
|
|
var tapRecognized = false;
|
|
tap.onTap = () {
|
|
tapRecognized = true;
|
|
};
|
|
var tapCanceled = false;
|
|
tap.onTapCancel = () {
|
|
tapCanceled = true;
|
|
};
|
|
|
|
tap.addPointer(down4);
|
|
tester.closeArena(4);
|
|
expect(tapRecognized, isFalse);
|
|
expect(tapCanceled, isFalse);
|
|
tester.route(down4);
|
|
expect(tapRecognized, isFalse);
|
|
expect(tapCanceled, isFalse);
|
|
|
|
tester.route(move4);
|
|
expect(tapRecognized, isFalse);
|
|
expect(tapCanceled, isFalse);
|
|
tester.route(up4);
|
|
expect(tapRecognized, isTrue);
|
|
expect(tapCanceled, isFalse);
|
|
GestureBinding.instance.gestureArena.sweep(4);
|
|
expect(tapRecognized, isTrue);
|
|
expect(tapCanceled, isFalse);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('Timeout does not cancel tap', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
|
|
var tapRecognized = false;
|
|
tap.onTap = () {
|
|
tapRecognized = true;
|
|
};
|
|
|
|
tap.addPointer(down1);
|
|
tester.closeArena(1);
|
|
expect(tapRecognized, isFalse);
|
|
tester.route(down1);
|
|
expect(tapRecognized, isFalse);
|
|
|
|
tester.async.elapse(const Duration(milliseconds: 500));
|
|
expect(tapRecognized, isFalse);
|
|
tester.route(up1);
|
|
expect(tapRecognized, isTrue);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(tapRecognized, isTrue);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('Should yield to other arena members', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
|
|
var tapRecognized = false;
|
|
tap.onTap = () {
|
|
tapRecognized = true;
|
|
};
|
|
|
|
tap.addPointer(down1);
|
|
final member = TestGestureArenaMember();
|
|
final GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(1, member);
|
|
GestureBinding.instance.gestureArena.hold(1);
|
|
tester.closeArena(1);
|
|
expect(tapRecognized, isFalse);
|
|
tester.route(down1);
|
|
expect(tapRecognized, isFalse);
|
|
|
|
tester.route(up1);
|
|
expect(tapRecognized, isFalse);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(tapRecognized, isFalse);
|
|
|
|
entry.resolve(GestureDisposition.accepted);
|
|
expect(tapRecognized, isFalse);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('Should trigger on release of held arena', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
|
|
var tapRecognized = false;
|
|
tap.onTap = () {
|
|
tapRecognized = true;
|
|
};
|
|
|
|
tap.addPointer(down1);
|
|
final member = TestGestureArenaMember();
|
|
final GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(1, member);
|
|
GestureBinding.instance.gestureArena.hold(1);
|
|
tester.closeArena(1);
|
|
expect(tapRecognized, isFalse);
|
|
tester.route(down1);
|
|
expect(tapRecognized, isFalse);
|
|
|
|
tester.route(up1);
|
|
expect(tapRecognized, isFalse);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(tapRecognized, isFalse);
|
|
|
|
entry.resolve(GestureDisposition.rejected);
|
|
tester.async.flushMicrotasks();
|
|
expect(tapRecognized, isTrue);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('Should log exceptions from callbacks', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
|
|
tap.onTap = () {
|
|
throw Exception(test);
|
|
};
|
|
|
|
final FlutterExceptionHandler? previousErrorHandler = FlutterError.onError;
|
|
var gotError = false;
|
|
FlutterError.onError = (FlutterErrorDetails details) {
|
|
gotError = true;
|
|
};
|
|
|
|
tap.addPointer(down1);
|
|
tester.closeArena(1);
|
|
tester.route(down1);
|
|
expect(gotError, isFalse);
|
|
|
|
tester.route(up1);
|
|
expect(gotError, isTrue);
|
|
|
|
FlutterError.onError = previousErrorHandler;
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('onTapCancel should show reason in the proper format', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
|
|
tap.onTapCancel = () {
|
|
throw Exception(test);
|
|
};
|
|
|
|
final FlutterExceptionHandler? previousErrorHandler = FlutterError.onError;
|
|
var gotError = false;
|
|
FlutterError.onError = (FlutterErrorDetails details) {
|
|
expect(details.toString().contains('"spontaneous onTapCancel"'), isTrue);
|
|
gotError = true;
|
|
};
|
|
|
|
const pointer = 1;
|
|
tap.addPointer(const PointerDownEvent(pointer: pointer));
|
|
tester.closeArena(pointer);
|
|
tester.async.elapse(const Duration(milliseconds: 500));
|
|
tester.route(const PointerCancelEvent(pointer: pointer));
|
|
|
|
expect(gotError, isTrue);
|
|
|
|
FlutterError.onError = previousErrorHandler;
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('No duplicate tap events', (GestureTester tester) {
|
|
final tapA = TapGestureRecognizer();
|
|
final tapB = TapGestureRecognizer();
|
|
|
|
final log = <String>[];
|
|
tapA.onTapDown = (TapDownDetails details) {
|
|
log.add('tapA onTapDown');
|
|
};
|
|
tapA.onTapUp = (TapUpDetails details) {
|
|
log.add('tapA onTapUp');
|
|
};
|
|
tapA.onTap = () {
|
|
log.add('tapA onTap');
|
|
};
|
|
tapA.onTapCancel = () {
|
|
log.add('tapA onTapCancel');
|
|
};
|
|
tapB.onTapDown = (TapDownDetails details) {
|
|
log.add('tapB onTapDown');
|
|
};
|
|
tapB.onTapUp = (TapUpDetails details) {
|
|
log.add('tapB onTapUp');
|
|
};
|
|
tapB.onTap = () {
|
|
log.add('tapB onTap');
|
|
};
|
|
tapB.onTapCancel = () {
|
|
log.add('tapB onTapCancel');
|
|
};
|
|
|
|
log.add('start');
|
|
tapA.addPointer(down1);
|
|
log.add('added 1 to A');
|
|
tapB.addPointer(down1);
|
|
log.add('added 1 to B');
|
|
tester.closeArena(1);
|
|
log.add('closed 1');
|
|
tester.route(down1);
|
|
log.add('routed 1 down');
|
|
tester.route(up1);
|
|
log.add('routed 1 up');
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
log.add('swept 1');
|
|
tapA.addPointer(down2);
|
|
log.add('down 2 to A');
|
|
tapB.addPointer(down2);
|
|
log.add('down 2 to B');
|
|
tester.closeArena(2);
|
|
log.add('closed 2');
|
|
tester.route(down2);
|
|
log.add('routed 2 down');
|
|
tester.route(up2);
|
|
log.add('routed 2 up');
|
|
GestureBinding.instance.gestureArena.sweep(2);
|
|
log.add('swept 2');
|
|
tapA.dispose();
|
|
log.add('disposed A');
|
|
tapB.dispose();
|
|
log.add('disposed B');
|
|
|
|
expect(log, <String>[
|
|
'start',
|
|
'added 1 to A',
|
|
'added 1 to B',
|
|
'closed 1',
|
|
'routed 1 down',
|
|
'routed 1 up',
|
|
'tapA onTapDown',
|
|
'tapA onTapUp',
|
|
'tapA onTap',
|
|
'swept 1',
|
|
'down 2 to A',
|
|
'down 2 to B',
|
|
'closed 2',
|
|
'routed 2 down',
|
|
'routed 2 up',
|
|
'tapA onTapDown',
|
|
'tapA onTapUp',
|
|
'tapA onTap',
|
|
'swept 2',
|
|
'disposed A',
|
|
'disposed B',
|
|
]);
|
|
});
|
|
|
|
testGesture('PointerCancelEvent cancels tap', (GestureTester tester) {
|
|
const down = PointerDownEvent(pointer: 5, position: Offset(10.0, 10.0));
|
|
const cancel = PointerCancelEvent(pointer: 5, position: Offset(10.0, 10.0));
|
|
|
|
final tap = TapGestureRecognizer();
|
|
|
|
final recognized = <String>[];
|
|
tap.onTapDown = (_) {
|
|
recognized.add('down');
|
|
};
|
|
tap.onTapUp = (_) {
|
|
recognized.add('up');
|
|
};
|
|
tap.onTap = () {
|
|
recognized.add('tap');
|
|
};
|
|
tap.onTapCancel = () {
|
|
recognized.add('cancel');
|
|
};
|
|
|
|
tap.addPointer(down);
|
|
tester.closeArena(5);
|
|
tester.async.elapse(const Duration(milliseconds: 5000));
|
|
expect(recognized, <String>['down']);
|
|
tester.route(cancel);
|
|
expect(recognized, <String>['down', 'cancel']);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('PointerCancelEvent after exceeding deadline cancels tap', (GestureTester tester) {
|
|
const down = PointerDownEvent(pointer: 5, position: Offset(10.0, 10.0));
|
|
const cancel = PointerCancelEvent(pointer: 5, position: Offset(10.0, 10.0));
|
|
|
|
final tap = TapGestureRecognizer();
|
|
final drag = HorizontalDragGestureRecognizer()..onStart = (_) {}; // Need a callback to compete
|
|
addTearDown(drag.dispose);
|
|
|
|
final recognized = <String>[];
|
|
tap.onTapDown = (_) {
|
|
recognized.add('down');
|
|
};
|
|
tap.onTapUp = (_) {
|
|
recognized.add('up');
|
|
};
|
|
tap.onTap = () {
|
|
recognized.add('tap');
|
|
};
|
|
tap.onTapCancel = () {
|
|
recognized.add('cancel');
|
|
};
|
|
|
|
tap.addPointer(down);
|
|
drag.addPointer(down);
|
|
tester.closeArena(5);
|
|
tester.route(down);
|
|
expect(recognized, <String>[]);
|
|
tester.async.elapse(const Duration(milliseconds: 1000));
|
|
expect(recognized, <String>['down']);
|
|
tester.route(cancel);
|
|
expect(recognized, <String>['down', 'cancel']);
|
|
|
|
tap.dispose();
|
|
drag.dispose();
|
|
});
|
|
|
|
testGesture('losing tap gesture recognizer does not send onTapCancel', (GestureTester tester) {
|
|
final tap = TapGestureRecognizer();
|
|
addTearDown(tap.dispose);
|
|
final drag = HorizontalDragGestureRecognizer();
|
|
addTearDown(drag.dispose);
|
|
|
|
final recognized = <String>[];
|
|
tap.onTapDown = (_) {
|
|
recognized.add('down');
|
|
};
|
|
tap.onTapUp = (_) {
|
|
recognized.add('up');
|
|
};
|
|
tap.onTap = () {
|
|
recognized.add('tap');
|
|
};
|
|
tap.onTapCancel = () {
|
|
recognized.add('cancel');
|
|
};
|
|
|
|
tap.addPointer(down3);
|
|
drag.addPointer(down3);
|
|
tester.closeArena(3);
|
|
tester.route(move3);
|
|
GestureBinding.instance.gestureArena.sweep(3);
|
|
expect(recognized, isEmpty);
|
|
|
|
tap.dispose();
|
|
drag.dispose();
|
|
});
|
|
|
|
testGesture('non-primary pointers does not trigger timeout', (GestureTester tester) {
|
|
// Regression test for https://github.com/flutter/flutter/issues/43310
|
|
// Pointer1 down, pointer2 down, then pointer 1 up, all within the timeout.
|
|
// In this way, `BaseTapGestureRecognizer.didExceedDeadline` can be triggered
|
|
// after its `_reset`.
|
|
final tap = TapGestureRecognizer();
|
|
addTearDown(tap.dispose);
|
|
|
|
final recognized = <String>[];
|
|
tap.onTapDown = (_) {
|
|
recognized.add('down');
|
|
};
|
|
tap.onTapUp = (_) {
|
|
recognized.add('up');
|
|
};
|
|
tap.onTap = () {
|
|
recognized.add('tap');
|
|
};
|
|
tap.onTapCancel = () {
|
|
recognized.add('cancel');
|
|
};
|
|
|
|
tap.addPointer(down1);
|
|
tester.closeArena(down1.pointer);
|
|
|
|
tap.addPointer(down2);
|
|
tester.closeArena(down2.pointer);
|
|
|
|
expect(recognized, isEmpty);
|
|
|
|
tester.route(up1);
|
|
GestureBinding.instance.gestureArena.sweep(down1.pointer);
|
|
expect(recognized, <String>['down', 'up', 'tap']);
|
|
recognized.clear();
|
|
|
|
// If regression happens, the following step will throw error
|
|
tester.async.elapse(const Duration(milliseconds: 200));
|
|
expect(recognized, isEmpty);
|
|
|
|
tester.route(up2);
|
|
GestureBinding.instance.gestureArena.sweep(down2.pointer);
|
|
expect(recognized, isEmpty);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
group('Enforce consistent-button restriction:', () {
|
|
// Change buttons during down-up sequence 1
|
|
const move1lr = PointerMoveEvent(
|
|
pointer: 1,
|
|
position: Offset(10.0, 10.0),
|
|
buttons: kPrimaryMouseButton | kSecondaryMouseButton,
|
|
);
|
|
const move1r = PointerMoveEvent(
|
|
pointer: 1,
|
|
position: Offset(10.0, 10.0),
|
|
buttons: kSecondaryMouseButton,
|
|
);
|
|
|
|
final recognized = <String>[];
|
|
late TapGestureRecognizer tap;
|
|
setUp(() {
|
|
tap = TapGestureRecognizer()
|
|
..onTapDown = (TapDownDetails details) {
|
|
recognized.add('down');
|
|
}
|
|
..onTapUp = (TapUpDetails details) {
|
|
recognized.add('up');
|
|
}
|
|
..onTapCancel = () {
|
|
recognized.add('cancel');
|
|
};
|
|
addTearDown(tap.dispose);
|
|
});
|
|
|
|
tearDown(() {
|
|
tap.dispose();
|
|
recognized.clear();
|
|
});
|
|
|
|
testGesture('changing buttons before TapDown should cancel gesture without sending cancel', (
|
|
GestureTester tester,
|
|
) {
|
|
tap.addPointer(down1);
|
|
tester.closeArena(1);
|
|
expect(recognized, <String>[]);
|
|
|
|
tester.route(move1lr);
|
|
expect(recognized, <String>[]);
|
|
|
|
tester.route(move1r);
|
|
expect(recognized, <String>[]);
|
|
|
|
tester.route(up1);
|
|
expect(recognized, <String>[]);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('changing buttons before TapDown should not prevent the next tap', (
|
|
GestureTester tester,
|
|
) {
|
|
tap.addPointer(down1);
|
|
tester.closeArena(1);
|
|
|
|
tester.route(move1lr);
|
|
tester.route(move1r);
|
|
tester.route(up1);
|
|
expect(recognized, <String>[]);
|
|
|
|
tap.addPointer(down2);
|
|
tester.closeArena(2);
|
|
tester.async.elapse(const Duration(milliseconds: 1000));
|
|
tester.route(up2);
|
|
expect(recognized, <String>['down', 'up']);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('changing buttons after TapDown should cancel gesture and send cancel', (
|
|
GestureTester tester,
|
|
) {
|
|
tap.addPointer(down1);
|
|
tester.closeArena(1);
|
|
expect(recognized, <String>[]);
|
|
tester.async.elapse(const Duration(milliseconds: 1000));
|
|
expect(recognized, <String>['down']);
|
|
|
|
tester.route(move1lr);
|
|
expect(recognized, <String>['down', 'cancel']);
|
|
|
|
tester.route(move1r);
|
|
expect(recognized, <String>['down', 'cancel']);
|
|
|
|
tester.route(up1);
|
|
expect(recognized, <String>['down', 'cancel']);
|
|
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('changing buttons after TapDown should not prevent the next tap', (
|
|
GestureTester tester,
|
|
) {
|
|
tap.addPointer(down1);
|
|
tester.closeArena(1);
|
|
tester.async.elapse(const Duration(milliseconds: 1000));
|
|
|
|
tester.route(move1lr);
|
|
tester.route(move1r);
|
|
tester.route(up1);
|
|
GestureBinding.instance.gestureArena.sweep(1);
|
|
expect(recognized, <String>['down', 'cancel']);
|
|
|
|
tap.addPointer(down2);
|
|
tester.closeArena(2);
|
|
tester.async.elapse(const Duration(milliseconds: 1000));
|
|
tester.route(up2);
|
|
GestureBinding.instance.gestureArena.sweep(2);
|
|
expect(recognized, <String>['down', 'cancel', 'down', 'up']);
|
|
|
|
tap.dispose();
|
|
});
|
|
});
|
|
|
|
group('Recognizers listening on different buttons do not form competition:', () {
|
|
// If a tap gesture has no competitors, a pointer down event triggers
|
|
// onTapDown immediately; if there are competitors, onTapDown is triggered
|
|
// after a timeout. The following tests make sure that tap recognizers
|
|
// listening on different buttons do not form competition.
|
|
|
|
final recognized = <String>[];
|
|
late TapGestureRecognizer primary;
|
|
late TapGestureRecognizer primary2;
|
|
late TapGestureRecognizer secondary;
|
|
late TapGestureRecognizer tertiary;
|
|
setUp(() {
|
|
primary = TapGestureRecognizer()
|
|
..onTapDown = (TapDownDetails details) {
|
|
recognized.add('primaryDown');
|
|
}
|
|
..onTapUp = (TapUpDetails details) {
|
|
recognized.add('primaryUp');
|
|
}
|
|
..onTapCancel = () {
|
|
recognized.add('primaryCancel');
|
|
};
|
|
addTearDown(primary.dispose);
|
|
primary2 = TapGestureRecognizer()
|
|
..onTapDown = (TapDownDetails details) {
|
|
recognized.add('primary2Down');
|
|
}
|
|
..onTapUp = (TapUpDetails details) {
|
|
recognized.add('primary2Up');
|
|
}
|
|
..onTapCancel = () {
|
|
recognized.add('primary2Cancel');
|
|
};
|
|
addTearDown(primary2.dispose);
|
|
secondary = TapGestureRecognizer()
|
|
..onSecondaryTapDown = (TapDownDetails details) {
|
|
recognized.add('secondaryDown');
|
|
}
|
|
..onSecondaryTapUp = (TapUpDetails details) {
|
|
recognized.add('secondaryUp');
|
|
}
|
|
..onSecondaryTapCancel = () {
|
|
recognized.add('secondaryCancel');
|
|
};
|
|
addTearDown(secondary.dispose);
|
|
tertiary = TapGestureRecognizer()
|
|
..onTertiaryTapDown = (TapDownDetails details) {
|
|
recognized.add('tertiaryDown');
|
|
}
|
|
..onTertiaryTapUp = (TapUpDetails details) {
|
|
recognized.add('tertiaryUp');
|
|
}
|
|
..onTertiaryTapCancel = () {
|
|
recognized.add('tertiaryCancel');
|
|
};
|
|
addTearDown(tertiary.dispose);
|
|
});
|
|
|
|
tearDown(() {
|
|
primary.dispose();
|
|
primary2.dispose();
|
|
secondary.dispose();
|
|
tertiary.dispose();
|
|
recognized.clear();
|
|
});
|
|
|
|
testGesture(
|
|
'A primary tap recognizer does not form competition with a secondary tap recognizer',
|
|
(GestureTester tester) {
|
|
primary.addPointer(down1);
|
|
secondary.addPointer(down1);
|
|
tester.closeArena(1);
|
|
|
|
tester.route(down1);
|
|
expect(recognized, <String>['primaryDown']);
|
|
recognized.clear();
|
|
|
|
tester.route(up1);
|
|
expect(recognized, <String>['primaryUp']);
|
|
},
|
|
);
|
|
|
|
testGesture(
|
|
'A primary tap recognizer does not form competition with a tertiary tap recognizer',
|
|
(GestureTester tester) {
|
|
primary.addPointer(down1);
|
|
tertiary.addPointer(down1);
|
|
tester.closeArena(1);
|
|
|
|
tester.route(down1);
|
|
expect(recognized, <String>['primaryDown']);
|
|
recognized.clear();
|
|
|
|
tester.route(up1);
|
|
expect(recognized, <String>['primaryUp']);
|
|
},
|
|
);
|
|
|
|
testGesture('A primary tap recognizer forms competition with another primary tap recognizer', (
|
|
GestureTester tester,
|
|
) {
|
|
primary.addPointer(down1);
|
|
primary2.addPointer(down1);
|
|
tester.closeArena(1);
|
|
|
|
tester.route(down1);
|
|
expect(recognized, <String>[]);
|
|
|
|
tester.async.elapse(const Duration(milliseconds: 500));
|
|
expect(recognized, <String>['primaryDown', 'primary2Down']);
|
|
});
|
|
});
|
|
|
|
group('Gestures of different buttons trigger correct callbacks:', () {
|
|
final recognized = <String>[];
|
|
late TapGestureRecognizer tap;
|
|
const cancel1 = PointerCancelEvent(pointer: 1);
|
|
const cancel5 = PointerCancelEvent(pointer: 5);
|
|
const cancel6 = PointerCancelEvent(pointer: 6);
|
|
|
|
setUp(() {
|
|
tap = TapGestureRecognizer()
|
|
..onTapDown = (TapDownDetails details) {
|
|
recognized.add('primaryDown');
|
|
}
|
|
..onTap = () {
|
|
recognized.add('primary');
|
|
}
|
|
..onTapUp = (TapUpDetails details) {
|
|
recognized.add('primaryUp');
|
|
}
|
|
..onTapCancel = () {
|
|
recognized.add('primaryCancel');
|
|
}
|
|
..onSecondaryTapDown = (TapDownDetails details) {
|
|
recognized.add('secondaryDown');
|
|
}
|
|
..onSecondaryTapUp = (TapUpDetails details) {
|
|
recognized.add('secondaryUp');
|
|
}
|
|
..onSecondaryTapCancel = () {
|
|
recognized.add('secondaryCancel');
|
|
}
|
|
..onTertiaryTapDown = (TapDownDetails details) {
|
|
recognized.add('tertiaryDown');
|
|
}
|
|
..onTertiaryTapUp = (TapUpDetails details) {
|
|
recognized.add('tertiaryUp');
|
|
}
|
|
..onTertiaryTapCancel = () {
|
|
recognized.add('tertiaryCancel');
|
|
};
|
|
});
|
|
|
|
tearDown(() {
|
|
recognized.clear();
|
|
tap.dispose();
|
|
});
|
|
|
|
testGesture('A primary tap should trigger primary callbacks', (GestureTester tester) {
|
|
tap.addPointer(down1);
|
|
tester.closeArena(down1.pointer);
|
|
expect(recognized, <String>[]);
|
|
tester.async.elapse(const Duration(milliseconds: 500));
|
|
expect(recognized, <String>['primaryDown']);
|
|
recognized.clear();
|
|
|
|
tester.route(up1);
|
|
expect(recognized, <String>['primaryUp', 'primary']);
|
|
GestureBinding.instance.gestureArena.sweep(down1.pointer);
|
|
});
|
|
|
|
testGesture('A primary tap cancel trigger primary callbacks', (GestureTester tester) {
|
|
tap.addPointer(down1);
|
|
tester.closeArena(down1.pointer);
|
|
expect(recognized, <String>[]);
|
|
tester.async.elapse(const Duration(milliseconds: 500));
|
|
expect(recognized, <String>['primaryDown']);
|
|
recognized.clear();
|
|
|
|
tester.route(cancel1);
|
|
expect(recognized, <String>['primaryCancel']);
|
|
GestureBinding.instance.gestureArena.sweep(down1.pointer);
|
|
});
|
|
|
|
testGesture('A secondary tap should trigger secondary callbacks', (GestureTester tester) {
|
|
tap.addPointer(down5);
|
|
tester.closeArena(down5.pointer);
|
|
expect(recognized, <String>[]);
|
|
tester.async.elapse(const Duration(milliseconds: 500));
|
|
expect(recognized, <String>['secondaryDown']);
|
|
recognized.clear();
|
|
|
|
tester.route(up5);
|
|
GestureBinding.instance.gestureArena.sweep(down5.pointer);
|
|
expect(recognized, <String>['secondaryUp']);
|
|
});
|
|
|
|
testGesture('A tertiary tap should trigger tertiary callbacks', (GestureTester tester) {
|
|
tap.addPointer(down6);
|
|
tester.closeArena(down6.pointer);
|
|
expect(recognized, <String>[]);
|
|
tester.async.elapse(const Duration(milliseconds: 500));
|
|
expect(recognized, <String>['tertiaryDown']);
|
|
recognized.clear();
|
|
|
|
tester.route(up6);
|
|
GestureBinding.instance.gestureArena.sweep(down6.pointer);
|
|
expect(recognized, <String>['tertiaryUp']);
|
|
});
|
|
|
|
testGesture('A secondary tap cancel should trigger secondary callbacks', (
|
|
GestureTester tester,
|
|
) {
|
|
tap.addPointer(down5);
|
|
tester.closeArena(down5.pointer);
|
|
expect(recognized, <String>[]);
|
|
tester.async.elapse(const Duration(milliseconds: 500));
|
|
expect(recognized, <String>['secondaryDown']);
|
|
recognized.clear();
|
|
|
|
tester.route(cancel5);
|
|
GestureBinding.instance.gestureArena.sweep(down5.pointer);
|
|
expect(recognized, <String>['secondaryCancel']);
|
|
});
|
|
|
|
testGesture('A tertiary tap cancel should trigger tertiary callbacks', (GestureTester tester) {
|
|
tap.addPointer(down6);
|
|
tester.closeArena(down6.pointer);
|
|
expect(recognized, <String>[]);
|
|
tester.async.elapse(const Duration(milliseconds: 500));
|
|
expect(recognized, <String>['tertiaryDown']);
|
|
recognized.clear();
|
|
|
|
tester.route(cancel6);
|
|
GestureBinding.instance.gestureArena.sweep(down6.pointer);
|
|
expect(recognized, <String>['tertiaryCancel']);
|
|
});
|
|
});
|
|
|
|
testGesture('A second tap after rejection is ignored', (GestureTester tester) {
|
|
var didTap = false;
|
|
|
|
final tap = TapGestureRecognizer()
|
|
..onTap = () {
|
|
didTap = true;
|
|
};
|
|
addTearDown(tap.dispose);
|
|
|
|
// Add drag recognizer for competition
|
|
final drag = HorizontalDragGestureRecognizer()..onStart = (_) {};
|
|
addTearDown(drag.dispose);
|
|
|
|
final pointer1 = TestPointer();
|
|
|
|
final PointerDownEvent down = pointer1.down(Offset.zero);
|
|
drag.addPointer(down);
|
|
tap.addPointer(down);
|
|
|
|
tester.closeArena(1);
|
|
|
|
// One-finger moves, canceling the tap
|
|
tester.route(down);
|
|
tester.route(pointer1.move(const Offset(50.0, 0)));
|
|
|
|
// Add another finger
|
|
final pointer2 = TestPointer(2);
|
|
final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0));
|
|
drag.addPointer(down2);
|
|
tap.addPointer(down2);
|
|
tester.closeArena(2);
|
|
tester.route(down2);
|
|
|
|
expect(didTap, isFalse);
|
|
});
|
|
|
|
testGesture('onTapMove works', (GestureTester tester) {
|
|
TapMoveDetails? tapMoveDetails;
|
|
final tap = TapGestureRecognizer(postAcceptSlopTolerance: null)
|
|
..onTapMove = (TapMoveDetails detail) {
|
|
tapMoveDetails = detail;
|
|
};
|
|
addTearDown(tap.dispose);
|
|
|
|
final pointer1 = TestPointer();
|
|
final PointerDownEvent down = pointer1.down(Offset.zero);
|
|
tap.addPointer(down);
|
|
tester.closeArena(1);
|
|
tester.route(down);
|
|
tester.route(pointer1.move(const Offset(50.0, 0)));
|
|
expect(tapMoveDetails, isNotNull);
|
|
expect(tapMoveDetails!.globalPosition, const Offset(50.0, 0));
|
|
expect(tapMoveDetails!.delta, const Offset(50.0, 0));
|
|
tapMoveDetails = null;
|
|
|
|
tester.route(pointer1.move(const Offset(60.0, 10)));
|
|
expect(tapMoveDetails, isNotNull);
|
|
expect(tapMoveDetails!.globalPosition, const Offset(60.0, 10));
|
|
expect(tapMoveDetails!.delta, const Offset(10.0, 10));
|
|
});
|
|
}
|