Unverified Commit ff5b0e14 authored by xhzq233's avatar xhzq233 Committed by GitHub

CupertinoContextMenu improvement (#131030)

Fixes overlapping gestures in CupertinoContextMenu's subtree
parent 1a31682e
......@@ -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);
}
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment