[CupertinoActionSheet] Support legacy buttons (#151136)

Fixes https://github.com/flutter/flutter/issues/150980

This PR allows buttons implemented with `GestureDetector.onTap` to be selected in the action sheet.
This commit is contained in:
Tong Mu 2024-07-16 22:09:24 -07:00 committed by GitHub
parent f50feec5be
commit de04c13fdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 97 additions and 8 deletions

View File

@ -546,21 +546,35 @@ class _SlidingTapGestureRecognizer extends VerticalDragGestureRecognizer {
if (event is PointerMoveEvent) {
onResponsiveUpdate?.call(event.position);
}
// If this gesture has a competing gesture (such as scrolling), and the
// pointer has not moved far enough to get this panning accepted, a
// pointer up event should still be considered as an accepted tap up.
// Manually accept this gesture here, which triggers onDragEnd.
// Sliding tap needs to handle 'up' events differently compared to typical
// drag gestures. If there's another gesture recognizer (like scrolling)
// competing and the pointer hasn't moved beyond the tolerance limit
// (slop), this gesture must still be accepted.
//
// Simply calling `accept()` here to handle this won't work because it
// would break backward compatibility with legacy buttons (see
// https://github.com/flutter/flutter/issues/150980 for more details).
// Legacy buttons recognize taps using `GestureDetector.onTap`, which
// neither accepts nor rejects for short taps. Instead, they wait for the
// default resolution as the last contender in the gesture arena.
//
// Therefore, this gesture should also follow the same strategy of not
// immediately accepting or rejecting. This allows tap gestures to take
// precedence for being inner, while sliding taps can take precedence over
// scroll gestures when the latter give up.
if (event is PointerUpEvent) {
resolve(GestureDisposition.accepted);
stopTrackingPointer(_primaryPointer!);
onResponsiveEnd?.call(event.position);
} else {
super.handleEvent(event);
_primaryPointer = null;
// Do not call `super.handleEvent`, which gives up the pointer and thus
// rejects the gesture.
return;
}
if (event is PointerUpEvent || event is PointerCancelEvent) {
if (event is PointerCancelEvent) {
_primaryPointer = null;
}
}
super.handleEvent(event);
}
@override

View File

@ -1155,6 +1155,52 @@ void main() {
expect(pressed, null);
});
testWidgets('Taps on legacy button calls onPressed and renders correctly', (WidgetTester tester) async {
// Legacy buttons are implemented with [GestureDetector.onTap]. Apps that
// use customized legacy buttons should continue to work.
//
// Regression test for https://github.com/flutter/flutter/issues/150980 .
bool wasPressed = false;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Builder(builder: (BuildContext context) {
return CupertinoActionSheet(
actions: <Widget>[
LegacyAction(
child: const Text('Legacy'),
onPressed: () {
expect(wasPressed, false);
wasPressed = true;
Navigator.pop(context);
},
),
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
);
}),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
expect(wasPressed, isFalse);
// Push the legacy button and hold for a while to activate the pressing effect.
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Legacy')));
await tester.pump(const Duration(seconds: 1));
expect(wasPressed, isFalse);
await expectLater(
find.byType(CupertinoActionSheet),
matchesGoldenFile('cupertinoActionSheet.legacyButton.png'),
);
await gesture.up();
await tester.pumpAndSettle();
expect(wasPressed, isTrue);
expect(find.text('Legacy'), findsNothing);
});
testWidgets('Action sheet width is correct when given infinite horizontal space', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
@ -2054,3 +2100,32 @@ class OverrideMediaQuery extends StatelessWidget {
);
}
}
// Old-style action sheet buttons, which are implemented with
// `GestureDetector.onTap`.
class LegacyAction extends StatelessWidget {
const LegacyAction({
super.key,
required this.onPressed,
required this.child,
});
final VoidCallback onPressed;
final Widget child;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
behavior: HitTestBehavior.opaque,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 57),
child: Container(
alignment: AlignmentDirectional.center,
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 10.0),
child: child,
),
),
);
}
}