281 lines
10 KiB
Dart
281 lines
10 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';
|
|
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'gesture_tester.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
test('acceptGesture tolerates a null lastPendingEventTimestamp', () {
|
|
// Regression test for https://github.com/flutter/flutter/issues/112403
|
|
// and b/249091367
|
|
final DragGestureRecognizer recognizer = VerticalDragGestureRecognizer();
|
|
const event = PointerDownEvent(timeStamp: Duration(days: 10));
|
|
|
|
expect(recognizer.debugLastPendingEventTimestamp, null);
|
|
|
|
recognizer.addAllowedPointer(event);
|
|
expect(recognizer.debugLastPendingEventTimestamp, event.timeStamp);
|
|
|
|
// Normal case: acceptGesture called and we have a last timestamp set.
|
|
recognizer.acceptGesture(event.pointer);
|
|
expect(recognizer.debugLastPendingEventTimestamp, null);
|
|
|
|
// Reject the gesture to reset state and allow accepting it again.
|
|
recognizer.rejectGesture(event.pointer);
|
|
expect(recognizer.debugLastPendingEventTimestamp, null);
|
|
|
|
// Not entirely clear how this can happen, but the bugs mentioned above show
|
|
// we can end up in this state empirically.
|
|
recognizer.acceptGesture(event.pointer);
|
|
expect(recognizer.debugLastPendingEventTimestamp, null);
|
|
});
|
|
|
|
testGesture(
|
|
'do not crash on up event for a pending pointer after winning arena for another pointer',
|
|
(GestureTester tester) {
|
|
// Regression test for https://github.com/flutter/flutter/issues/75061.
|
|
|
|
final v = VerticalDragGestureRecognizer()..onStart = (_) {};
|
|
addTearDown(v.dispose);
|
|
final h = HorizontalDragGestureRecognizer()..onStart = (_) {};
|
|
addTearDown(h.dispose);
|
|
|
|
const down90 = PointerDownEvent(pointer: 90, position: Offset(10.0, 10.0));
|
|
|
|
const up90 = PointerUpEvent(pointer: 90, position: Offset(10.0, 10.0));
|
|
|
|
const down91 = PointerDownEvent(pointer: 91, position: Offset(20.0, 20.0));
|
|
|
|
const up91 = PointerUpEvent(pointer: 91, position: Offset(20.0, 20.0));
|
|
|
|
v.addPointer(down90);
|
|
GestureBinding.instance.gestureArena.close(90);
|
|
h.addPointer(down91);
|
|
v.addPointer(down91);
|
|
GestureBinding.instance.gestureArena.close(91);
|
|
tester.async.flushMicrotasks();
|
|
|
|
GestureBinding.instance.handleEvent(up90, HitTestEntry(MockHitTestTarget()));
|
|
GestureBinding.instance.handleEvent(up91, HitTestEntry(MockHitTestTarget()));
|
|
},
|
|
);
|
|
|
|
testGesture(
|
|
'DragGestureRecognizer should not dispatch drag callbacks when it wins the arena if onlyAcceptDragOnThreshold is true and the threshold has not been met',
|
|
(GestureTester tester) {
|
|
final verticalDrag = VerticalDragGestureRecognizer();
|
|
final dragCallbacks = <String>[];
|
|
verticalDrag
|
|
..onlyAcceptDragOnThreshold = true
|
|
..onStart = (DragStartDetails details) {
|
|
dragCallbacks.add('onStart');
|
|
}
|
|
..onUpdate = (DragUpdateDetails details) {
|
|
dragCallbacks.add('onUpdate');
|
|
}
|
|
..onEnd = (DragEndDetails details) {
|
|
dragCallbacks.add('onEnd');
|
|
};
|
|
|
|
const down1 = PointerDownEvent(pointer: 6, position: Offset(10.0, 10.0));
|
|
|
|
const up1 = PointerUpEvent(pointer: 6, position: Offset(10.0, 10.0));
|
|
|
|
verticalDrag.addPointer(down1);
|
|
tester.closeArena(down1.pointer);
|
|
tester.route(down1);
|
|
tester.route(up1);
|
|
expect(dragCallbacks.isEmpty, true);
|
|
verticalDrag.dispose();
|
|
dragCallbacks.clear();
|
|
},
|
|
);
|
|
|
|
testGesture(
|
|
'DragGestureRecognizer should dispatch drag callbacks when it wins the arena if onlyAcceptDragOnThreshold is false and the threshold has not been met',
|
|
(GestureTester tester) {
|
|
final verticalDrag = VerticalDragGestureRecognizer();
|
|
final dragCallbacks = <String>[];
|
|
verticalDrag
|
|
..onlyAcceptDragOnThreshold = false
|
|
..onStart = (DragStartDetails details) {
|
|
dragCallbacks.add('onStart');
|
|
}
|
|
..onUpdate = (DragUpdateDetails details) {
|
|
dragCallbacks.add('onUpdate');
|
|
}
|
|
..onEnd = (DragEndDetails details) {
|
|
dragCallbacks.add('onEnd');
|
|
};
|
|
|
|
const down1 = PointerDownEvent(pointer: 6, position: Offset(10.0, 10.0));
|
|
|
|
const up1 = PointerUpEvent(pointer: 6, position: Offset(10.0, 10.0));
|
|
|
|
verticalDrag.addPointer(down1);
|
|
tester.closeArena(down1.pointer);
|
|
tester.route(down1);
|
|
tester.route(up1);
|
|
expect(dragCallbacks.isEmpty, false);
|
|
expect(dragCallbacks, <String>['onStart', 'onEnd']);
|
|
verticalDrag.dispose();
|
|
dragCallbacks.clear();
|
|
},
|
|
);
|
|
|
|
testWidgets('DragGestureRecognizer can be subclassed to beat a CustomScrollView in the arena', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final GlobalKey tapTargetKey = GlobalKey();
|
|
var wasPanStartCalled = false;
|
|
|
|
// Pump a tree with panable widget inside a CustomScrollView. The CustomScrollView
|
|
// has a more aggressive drag recognizer that will typically beat other drag
|
|
// recognizers in the arena. This pan recognizer uses a smaller threshold to
|
|
// accept the gesture, that should make it win the arena.
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: CustomScrollView(
|
|
slivers: <Widget>[
|
|
SliverToBoxAdapter(
|
|
child: RawGestureDetector(
|
|
behavior: HitTestBehavior.translucent,
|
|
gestures: <Type, GestureRecognizerFactory>{
|
|
_EagerPanGestureRecognizer:
|
|
GestureRecognizerFactoryWithHandlers<_EagerPanGestureRecognizer>(
|
|
() => _EagerPanGestureRecognizer(),
|
|
(_EagerPanGestureRecognizer recognizer) {
|
|
recognizer.onStart = (DragStartDetails details) =>
|
|
wasPanStartCalled = true;
|
|
},
|
|
),
|
|
},
|
|
child: SizedBox(key: tapTargetKey, width: 100, height: 100),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Tap down on the tap target inside the gesture recognizer.
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byKey(tapTargetKey)),
|
|
);
|
|
await tester.pump();
|
|
|
|
// Move the pointer predominantly on the x-axis, with a y-axis movement that
|
|
// is sufficient bigger so that both the CustomScrollScrollView and the
|
|
// pan gesture recognizer want to accept the gesture.
|
|
await gesture.moveBy(const Offset(30, kTouchSlop + 1));
|
|
await tester.pump();
|
|
|
|
// Ensure our gesture recognizer won the arena.
|
|
expect(wasPanStartCalled, isTrue);
|
|
});
|
|
|
|
group('Recognizers on different button filters:', () {
|
|
final recognized = <String>[];
|
|
late HorizontalDragGestureRecognizer primaryRecognizer;
|
|
late HorizontalDragGestureRecognizer secondaryRecognizer;
|
|
setUp(() {
|
|
primaryRecognizer =
|
|
HorizontalDragGestureRecognizer(
|
|
allowedButtonsFilter: (int buttons) => kPrimaryButton == buttons,
|
|
)
|
|
..onStart = (DragStartDetails details) {
|
|
recognized.add('onStartPrimary');
|
|
};
|
|
secondaryRecognizer =
|
|
HorizontalDragGestureRecognizer(
|
|
allowedButtonsFilter: (int buttons) => kSecondaryButton == buttons,
|
|
)
|
|
..onStart = (DragStartDetails details) {
|
|
recognized.add('onStartSecondary');
|
|
};
|
|
});
|
|
|
|
tearDown(() {
|
|
recognized.clear();
|
|
primaryRecognizer.dispose();
|
|
secondaryRecognizer.dispose();
|
|
});
|
|
|
|
testGesture('Primary button works', (GestureTester tester) {
|
|
const down1 = PointerDownEvent(pointer: 6, position: Offset(10.0, 10.0));
|
|
|
|
primaryRecognizer.addPointer(down1);
|
|
secondaryRecognizer.addPointer(down1);
|
|
tester.closeArena(down1.pointer);
|
|
tester.route(down1);
|
|
expect(recognized, <String>['onStartPrimary']);
|
|
});
|
|
|
|
testGesture('Secondary button works', (GestureTester tester) {
|
|
const down1 = PointerDownEvent(
|
|
pointer: 6,
|
|
position: Offset(10.0, 10.0),
|
|
buttons: kSecondaryMouseButton,
|
|
);
|
|
|
|
primaryRecognizer.addPointer(down1);
|
|
secondaryRecognizer.addPointer(down1);
|
|
tester.closeArena(down1.pointer);
|
|
tester.route(down1);
|
|
expect(recognized, <String>['onStartSecondary']);
|
|
});
|
|
});
|
|
}
|
|
|
|
class MockHitTestTarget implements HitTestTarget {
|
|
@override
|
|
void handleEvent(PointerEvent event, HitTestEntry entry) {}
|
|
}
|
|
|
|
/// A [PanGestureRecognizer] that tries to beat [VerticalDragGestureRecognizer] in the arena.
|
|
///
|
|
/// Typically, [VerticalDragGestureRecognizer] wins because it has a smaller threshold to
|
|
/// accept the gesture. This recognizer uses the same threshold that [VerticalDragGestureRecognizer]
|
|
/// uses.
|
|
class _EagerPanGestureRecognizer extends PanGestureRecognizer {
|
|
@override
|
|
bool hasSufficientGlobalDistanceToAccept(
|
|
PointerDeviceKind pointerDeviceKind,
|
|
double? deviceTouchSlop,
|
|
) {
|
|
return globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
|
|
}
|
|
|
|
@override
|
|
bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
|
|
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
|
|
final double minDistance = minFlingDistance ?? computeHitSlop(kind, gestureSettings);
|
|
return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity &&
|
|
estimate.offset.distanceSquared > minDistance * minDistance;
|
|
}
|
|
|
|
@override
|
|
DragEndDetails? considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
|
|
if (!isFlingGesture(estimate, kind)) {
|
|
return null;
|
|
}
|
|
final double maxVelocity = maxFlingVelocity ?? kMaxFlingVelocity;
|
|
final double dy = clampDouble(estimate.pixelsPerSecond.dy, -maxVelocity, maxVelocity);
|
|
return DragEndDetails(
|
|
velocity: Velocity(pixelsPerSecond: Offset(0, dy)),
|
|
primaryVelocity: dy,
|
|
globalPosition: lastPosition.global,
|
|
localPosition: lastPosition.local,
|
|
);
|
|
}
|
|
}
|