CupertinoContextMenu improvement (#131030)
Fixes overlapping gestures in CupertinoContextMenu's subtree
This commit is contained in:
parent
1a31682e6c
commit
ff5b0e1457
@ -6,7 +6,7 @@ import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart' show kMinFlingVelocity;
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart' show HapticFeedback;
|
||||
import 'package:flutter/widgets.dart';
|
||||
@ -480,6 +480,7 @@ class _CupertinoContextMenuState extends State<CupertinoContextMenu> with Ticker
|
||||
OverlayEntry? _lastOverlayEntry;
|
||||
_ContextMenuRoute<void>? _route;
|
||||
final double _midpoint = CupertinoContextMenu.animationOpensAt / 2;
|
||||
late final TapGestureRecognizer _tapGestureRecognizer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -490,13 +491,20 @@ class _CupertinoContextMenuState extends State<CupertinoContextMenu> with Ticker
|
||||
upperBound: CupertinoContextMenu.animationOpensAt,
|
||||
);
|
||||
_openController.addStatusListener(_onDecoyAnimationStatusChange);
|
||||
_tapGestureRecognizer = TapGestureRecognizer()
|
||||
..onTapCancel = _onTapCancel
|
||||
..onTapDown = _onTapDown
|
||||
..onTapUp = _onTapUp
|
||||
..onTap = _onTap;
|
||||
}
|
||||
|
||||
void _listenerCallback() {
|
||||
if (_openController.status != AnimationStatus.reverse &&
|
||||
_openController.value >= _midpoint &&
|
||||
widget.enableHapticFeedback) {
|
||||
HapticFeedback.heavyImpact();
|
||||
_openController.value >= _midpoint) {
|
||||
if (widget.enableHapticFeedback) {
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
_tapGestureRecognizer.resolve(GestureDisposition.accepted);
|
||||
_openController.removeListener(_listenerCallback);
|
||||
}
|
||||
}
|
||||
@ -663,11 +671,8 @@ class _CupertinoContextMenuState extends State<CupertinoContextMenu> with Ticker
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
|
||||
child: GestureDetector(
|
||||
onTapCancel: _onTapCancel,
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTap: _onTap,
|
||||
child: Listener(
|
||||
onPointerDown: _tapGestureRecognizer.addPointer,
|
||||
child: TickerMode(
|
||||
enabled: !_childHidden,
|
||||
child: Visibility.maintain(
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -810,4 +811,74 @@ void main() {
|
||||
expect(right.dx, lessThan(left.dx));
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('Conflicting gesture detectors', (WidgetTester tester) async {
|
||||
int? onPointerDownTime;
|
||||
int? onPointerUpTime;
|
||||
bool insideTapTriggered = false;
|
||||
// The required duration of the route to be pushed in is [500, 900]ms.
|
||||
// 500ms is calculated from kPressTimeout+_previewLongPressTimeout/2.
|
||||
// 900ms is calculated from kPressTimeout+_previewLongPressTimeout.
|
||||
const Duration pressDuration = Duration(milliseconds: 501);
|
||||
|
||||
int now() => clock.now().millisecondsSinceEpoch;
|
||||
|
||||
await tester.pumpWidget(Listener(
|
||||
onPointerDown: (PointerDownEvent event) => onPointerDownTime = now(),
|
||||
onPointerUp: (PointerUpEvent event) => onPointerUpTime = now(),
|
||||
child: CupertinoApp(
|
||||
home: Align(
|
||||
child: CupertinoContextMenu(
|
||||
actions: const <CupertinoContextMenuAction>[
|
||||
CupertinoContextMenuAction(
|
||||
child: Text('CupertinoContextMenuAction'),
|
||||
),
|
||||
],
|
||||
child: GestureDetector(
|
||||
onTap: () => insideTapTriggered = true,
|
||||
child: Container(
|
||||
width: 200,
|
||||
height: 200,
|
||||
key: const Key('container'),
|
||||
color: const Color(0xFF00FF00),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Start a press on the child.
|
||||
final TestGesture gesture = await tester.createGesture();
|
||||
await gesture.down(tester.getCenter(find.byKey(const Key('container'))));
|
||||
// Simulate the actual situation:
|
||||
// the user keeps pressing and requesting frames.
|
||||
// If there is only one frame,
|
||||
// the animation is mutant and cannot drive the value of the animation controller.
|
||||
for (int i = 0; i < 100; i++) {
|
||||
await tester.pump(pressDuration ~/ 100);
|
||||
}
|
||||
await gesture.up();
|
||||
// Await pushing route.
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Judge whether _ContextMenuRouteStatic present on the screen.
|
||||
final Finder routeStatic = find.byWidgetPredicate(
|
||||
(Widget w) => '${w.runtimeType}' == '_ContextMenuRouteStatic',
|
||||
);
|
||||
|
||||
// The insideTap and the route should not be triggered at the same time.
|
||||
if (insideTapTriggered) {
|
||||
// Calculate the actual duration.
|
||||
final int actualDuration = onPointerUpTime! - onPointerDownTime!;
|
||||
|
||||
expect(routeStatic, findsNothing,
|
||||
reason: 'When actualDuration($actualDuration) is in the range of 500ms~900ms, '
|
||||
'which means the route is pushed, '
|
||||
'but insideTap should not be triggered at the same time.');
|
||||
} else {
|
||||
// The route should be pushed when the insideTap is not triggered.
|
||||
expect(routeStatic, findsOneWidget);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user