mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> Use `gestureSettings.touchSlop` in `PrimaryPointerGestureRecognizer.handleEvent()` unless a non-default value (including `null`) is specified. This behavior will be reflected in subclasses (`TapGestureRecognizer` and `LongPressGestureRecognizer`) and enable `GestureDetector` tap and long-press touch slop to be configured out of the box with `MediaQuery` instead of providing custom gesture recognizers: ```dart // After this PR you can do this: child: MediaQuery( data: MediaQuery.of(context).copyWith( gestureSettings: const DeviceGestureSettings(touchSlop: 64), ), child: GestureDetector( onTap: widget.onTap, child: Container(), ), // instead of this: child: RawGestureDetector( gestures: <Type, GestureRecognizerFactory>{ TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( // Without https://github.com/flutter/flutter/pull/161541, we'll actually need to make // our own custom TapGestureRecognizer class to expose slop parameters too. () => TapGestureRecognizer(preAcceptSlopTolerance: 64, postAcceptSlopTolerance: 64, onTap: onTap), (TapGestureRecognizer instance) {}, ), }, child: Container(), ), ``` *List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.* https://github.com/flutter/flutter/issues/154215 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## 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]. <!-- 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
322 lines
11 KiB
Dart
322 lines
11 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:ui' show VoidCallback;
|
|
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
|
|
|
import 'gesture_tester.dart';
|
|
|
|
// Down/move/up pair 1: normal tap sequence
|
|
const PointerDownEvent down = PointerDownEvent(pointer: 5, position: Offset(10, 10));
|
|
|
|
const PointerMoveEvent move = PointerMoveEvent(pointer: 5, position: Offset(15, 15));
|
|
|
|
const PointerUpEvent up = PointerUpEvent(pointer: 5, position: Offset(15, 15));
|
|
|
|
// Down/move/up pair 2: tap sequence with a large move in the middle
|
|
const PointerDownEvent down2 = PointerDownEvent(pointer: 6, position: Offset(10, 10));
|
|
|
|
const PointerMoveEvent move2 = PointerMoveEvent(pointer: 6, position: Offset(100, 200));
|
|
|
|
const PointerUpEvent up2 = PointerUpEvent(pointer: 6, position: Offset(100, 200));
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
test('GestureRecognizer smoketest', () {
|
|
final TestGestureRecognizer recognizer = TestGestureRecognizer(debugOwner: 0);
|
|
expect(recognizer, hasAGoodToStringDeep);
|
|
});
|
|
|
|
test('GestureRecognizer dispatches memory events', () async {
|
|
await expectLater(
|
|
await memoryEvents(() => TestGestureRecognizer().dispose(), TestGestureRecognizer),
|
|
areCreateAndDispose,
|
|
);
|
|
});
|
|
|
|
test('OffsetPair', () {
|
|
const OffsetPair offset1 = OffsetPair(local: Offset(10, 20), global: Offset(30, 40));
|
|
|
|
expect(offset1.local, const Offset(10, 20));
|
|
expect(offset1.global, const Offset(30, 40));
|
|
|
|
const OffsetPair offset2 = OffsetPair(local: Offset(50, 60), global: Offset(70, 80));
|
|
|
|
final OffsetPair sum = offset2 + offset1;
|
|
expect(sum.local, const Offset(60, 80));
|
|
expect(sum.global, const Offset(100, 120));
|
|
|
|
final OffsetPair difference = offset2 - offset1;
|
|
expect(difference.local, const Offset(40, 40));
|
|
expect(difference.global, const Offset(40, 40));
|
|
});
|
|
|
|
group('PrimaryPointerGestureRecognizer', () {
|
|
testGesture('cleans up state after winning arena', (GestureTester tester) {
|
|
final List<String> resolutions = <String>[];
|
|
final IndefiniteGestureRecognizer indefinite = IndefiniteGestureRecognizer();
|
|
addTearDown(indefinite.dispose);
|
|
final TestPrimaryPointerGestureRecognizer<PointerUpEvent> accepting =
|
|
TestPrimaryPointerGestureRecognizer<PointerUpEvent>(
|
|
GestureDisposition.accepted,
|
|
onAcceptGesture: () => resolutions.add('accepted'),
|
|
onRejectGesture: () => resolutions.add('rejected'),
|
|
);
|
|
addTearDown(accepting.dispose);
|
|
expect(accepting.state, GestureRecognizerState.ready);
|
|
expect(accepting.primaryPointer, isNull);
|
|
expect(accepting.initialPosition, isNull);
|
|
expect(resolutions, <String>[]);
|
|
|
|
indefinite.addPointer(down);
|
|
accepting.addPointer(down);
|
|
expect(accepting.state, GestureRecognizerState.possible);
|
|
expect(accepting.primaryPointer, 5);
|
|
expect(accepting.initialPosition!.global, down.position);
|
|
expect(accepting.initialPosition!.local, down.localPosition);
|
|
expect(resolutions, <String>[]);
|
|
|
|
tester.closeArena(5);
|
|
tester.async.flushMicrotasks();
|
|
tester.route(down);
|
|
tester.route(up);
|
|
expect(accepting.state, GestureRecognizerState.ready);
|
|
expect(accepting.primaryPointer, 5);
|
|
expect(accepting.initialPosition, isNull);
|
|
expect(resolutions, <String>['accepted']);
|
|
});
|
|
|
|
testGesture('cleans up state after losing arena', (GestureTester tester) {
|
|
final List<String> resolutions = <String>[];
|
|
final IndefiniteGestureRecognizer indefinite = IndefiniteGestureRecognizer();
|
|
addTearDown(indefinite.dispose);
|
|
final TestPrimaryPointerGestureRecognizer<PointerMoveEvent> rejecting =
|
|
TestPrimaryPointerGestureRecognizer<PointerMoveEvent>(
|
|
GestureDisposition.rejected,
|
|
onAcceptGesture: () => resolutions.add('accepted'),
|
|
onRejectGesture: () => resolutions.add('rejected'),
|
|
);
|
|
addTearDown(rejecting.dispose);
|
|
expect(rejecting.state, GestureRecognizerState.ready);
|
|
expect(rejecting.primaryPointer, isNull);
|
|
expect(rejecting.initialPosition, isNull);
|
|
expect(resolutions, <String>[]);
|
|
|
|
indefinite.addPointer(down);
|
|
rejecting.addPointer(down);
|
|
expect(rejecting.state, GestureRecognizerState.possible);
|
|
expect(rejecting.primaryPointer, 5);
|
|
expect(rejecting.initialPosition!.global, down.position);
|
|
expect(rejecting.initialPosition!.local, down.localPosition);
|
|
expect(resolutions, <String>[]);
|
|
|
|
tester.closeArena(5);
|
|
tester.async.flushMicrotasks();
|
|
tester.route(down);
|
|
tester.route(move);
|
|
expect(rejecting.state, GestureRecognizerState.defunct);
|
|
expect(rejecting.primaryPointer, 5);
|
|
expect(rejecting.initialPosition!.global, down.position);
|
|
expect(rejecting.initialPosition!.local, down.localPosition);
|
|
expect(resolutions, <String>['rejected']);
|
|
|
|
tester.route(up);
|
|
expect(rejecting.state, GestureRecognizerState.ready);
|
|
expect(rejecting.primaryPointer, 5);
|
|
expect(rejecting.initialPosition, isNull);
|
|
expect(resolutions, <String>['rejected']);
|
|
});
|
|
|
|
testGesture('works properly when recycled', (GestureTester tester) {
|
|
final List<String> resolutions = <String>[];
|
|
final IndefiniteGestureRecognizer indefinite = IndefiniteGestureRecognizer();
|
|
addTearDown(indefinite.dispose);
|
|
final TestPrimaryPointerGestureRecognizer<PointerUpEvent> accepting =
|
|
TestPrimaryPointerGestureRecognizer<PointerUpEvent>(
|
|
GestureDisposition.accepted,
|
|
preAcceptSlopTolerance: 15,
|
|
postAcceptSlopTolerance: 1000,
|
|
onAcceptGesture: () => resolutions.add('accepted'),
|
|
onRejectGesture: () => resolutions.add('rejected'),
|
|
);
|
|
addTearDown(accepting.dispose);
|
|
|
|
// Send one complete pointer sequence
|
|
indefinite.addPointer(down);
|
|
accepting.addPointer(down);
|
|
tester.closeArena(5);
|
|
tester.async.flushMicrotasks();
|
|
tester.route(down);
|
|
tester.route(up);
|
|
expect(resolutions, <String>['accepted']);
|
|
resolutions.clear();
|
|
|
|
// Send a follow-on sequence that breaks preAcceptSlopTolerance
|
|
indefinite.addPointer(down2);
|
|
accepting.addPointer(down2);
|
|
tester.closeArena(6);
|
|
tester.async.flushMicrotasks();
|
|
tester.route(down2);
|
|
tester.route(move2);
|
|
expect(resolutions, <String>['rejected']);
|
|
tester.route(up2);
|
|
expect(resolutions, <String>['rejected']);
|
|
});
|
|
|
|
testGesture('uses expected pre-accept slop tolerance', (GestureTester tester) {
|
|
final List<String> resolutions = <String>[];
|
|
final IndefiniteGestureRecognizer indefinite = IndefiniteGestureRecognizer();
|
|
addTearDown(indefinite.dispose);
|
|
final TestPrimaryPointerGestureRecognizer<PointerUpEvent> defaultSlop =
|
|
TestPrimaryPointerGestureRecognizer<PointerUpEvent>(
|
|
GestureDisposition.accepted,
|
|
postAcceptSlopTolerance: null,
|
|
onAcceptGesture: () => resolutions.add('accepted'),
|
|
onRejectGesture: () => resolutions.add('rejected'),
|
|
);
|
|
addTearDown(defaultSlop.dispose);
|
|
final TestPrimaryPointerGestureRecognizer<PointerUpEvent> setSlop =
|
|
TestPrimaryPointerGestureRecognizer<PointerUpEvent>(
|
|
GestureDisposition.accepted,
|
|
preAcceptSlopTolerance: 5,
|
|
postAcceptSlopTolerance: null,
|
|
onAcceptGesture: () => resolutions.add('accepted'),
|
|
onRejectGesture: () => resolutions.add('rejected'),
|
|
);
|
|
addTearDown(setSlop.dispose);
|
|
final TestPrimaryPointerGestureRecognizer<PointerUpEvent> nullSlop =
|
|
TestPrimaryPointerGestureRecognizer<PointerUpEvent>(
|
|
GestureDisposition.accepted,
|
|
preAcceptSlopTolerance: null,
|
|
postAcceptSlopTolerance: null,
|
|
onAcceptGesture: () => resolutions.add('accepted'),
|
|
onRejectGesture: () => resolutions.add('rejected'),
|
|
);
|
|
addTearDown(nullSlop.dispose);
|
|
|
|
// Test getters
|
|
expect(defaultSlop.preAcceptSlopTolerance, equals(kTouchSlop));
|
|
expect(setSlop.preAcceptSlopTolerance, equals(5.0));
|
|
expect(nullSlop.preAcceptSlopTolerance, isNull);
|
|
|
|
indefinite.addPointer(down);
|
|
defaultSlop.addPointer(down);
|
|
tester.closeArena(5);
|
|
tester.async.flushMicrotasks();
|
|
tester.route(down);
|
|
tester.route(move);
|
|
tester.route(up);
|
|
expect(resolutions, <String>['accepted']);
|
|
resolutions.clear();
|
|
|
|
defaultSlop.gestureSettings = const DeviceGestureSettings(touchSlop: 5);
|
|
indefinite.addPointer(down);
|
|
defaultSlop.addPointer(down);
|
|
tester.closeArena(5);
|
|
tester.async.flushMicrotasks();
|
|
tester.route(down);
|
|
tester.route(move);
|
|
tester.route(up);
|
|
expect(resolutions, <String>['rejected']);
|
|
resolutions.clear();
|
|
|
|
indefinite.addPointer(down);
|
|
setSlop.addPointer(down);
|
|
tester.closeArena(5);
|
|
tester.async.flushMicrotasks();
|
|
tester.route(down);
|
|
tester.route(move);
|
|
tester.route(up);
|
|
expect(resolutions, <String>['rejected']);
|
|
resolutions.clear();
|
|
|
|
indefinite.addPointer(down2);
|
|
nullSlop.addPointer(down2);
|
|
tester.closeArena(6);
|
|
tester.async.flushMicrotasks();
|
|
tester.route(down2);
|
|
tester.route(move2);
|
|
tester.route(up2);
|
|
expect(resolutions, <String>['accepted']);
|
|
});
|
|
});
|
|
}
|
|
|
|
class TestGestureRecognizer extends GestureRecognizer {
|
|
TestGestureRecognizer({super.debugOwner});
|
|
|
|
@override
|
|
String get debugDescription => 'debugDescription content';
|
|
|
|
@override
|
|
void addPointer(PointerDownEvent event) {}
|
|
|
|
@override
|
|
void acceptGesture(int pointer) {}
|
|
|
|
@override
|
|
void rejectGesture(int pointer) {}
|
|
}
|
|
|
|
/// Gesture recognizer that adds itself to the gesture arena but never
|
|
/// resolves itself.
|
|
class IndefiniteGestureRecognizer extends GestureRecognizer {
|
|
@override
|
|
void addAllowedPointer(PointerDownEvent event) {
|
|
GestureBinding.instance.gestureArena.add(event.pointer, this);
|
|
}
|
|
|
|
@override
|
|
void acceptGesture(int pointer) {}
|
|
|
|
@override
|
|
void rejectGesture(int pointer) {}
|
|
|
|
@override
|
|
String get debugDescription => 'Unresolving';
|
|
}
|
|
|
|
/// Gesture recognizer that resolves with [resolution] when it handles an event
|
|
/// on the primary pointer of type [T]
|
|
class TestPrimaryPointerGestureRecognizer<T extends PointerEvent>
|
|
extends PrimaryPointerGestureRecognizer {
|
|
TestPrimaryPointerGestureRecognizer(
|
|
this.resolution, {
|
|
this.onAcceptGesture,
|
|
this.onRejectGesture,
|
|
super.preAcceptSlopTolerance,
|
|
super.postAcceptSlopTolerance,
|
|
});
|
|
|
|
final GestureDisposition resolution;
|
|
final VoidCallback? onAcceptGesture;
|
|
final VoidCallback? onRejectGesture;
|
|
|
|
@override
|
|
void acceptGesture(int pointer) {
|
|
super.acceptGesture(pointer);
|
|
onAcceptGesture?.call();
|
|
}
|
|
|
|
@override
|
|
void rejectGesture(int pointer) {
|
|
super.rejectGesture(pointer);
|
|
onRejectGesture?.call();
|
|
}
|
|
|
|
@override
|
|
void handlePrimaryPointer(PointerEvent event) {
|
|
if (event is T) {
|
|
resolve(resolution);
|
|
}
|
|
}
|
|
|
|
@override
|
|
String get debugDescription => 'TestPrimaryPointer';
|
|
}
|