From 71be3b14f96caf1d610c38ea5bbee345fa744817 Mon Sep 17 00:00:00 2001 From: masato Date: Wed, 26 Mar 2025 06:15:48 +0900 Subject: [PATCH] Add enableDrag property to CupertinoSheetRoute and showCupertinoSheet (#163923) Implemented. (flutter#163852) This PR adds the `enableDrag` option to showCupertinoSheet and CupertinoSheetRoute, allowing developers to control whether the sheet can be dismissed by dragging. ## How to Verify - Set enableDrag: false in CupertinoSheetRoute or showCupertinoSheet. ```dart CupertinoSheetRoute( builder: (BuildContext context) => const _SheetScaffold(), enableDrag: false, ), // or showCupertinoSheet( context: context, useNestedNavigation: true, pageBuilder: (BuildContext context) => const _SheetScaffold(), enableDrag: false, ); ``` - The following is a screenshot of `examples/api/lib/cupertino/sheet/cupertino_sheet.0.dart` after adding `enableDrag: false`. https://github.com/user-attachments/assets/315fb0e5-ceee-4150-be6e-dd919a7c2317 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] 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. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [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 --------- Co-authored-by: Tong Mu --- packages/flutter/lib/src/cupertino/sheet.dart | 24 +++++-- .../flutter/test/cupertino/sheet_test.dart | 69 +++++++++++++++++++ 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/sheet.dart b/packages/flutter/lib/src/cupertino/sheet.dart index 1016c2a8d5..e020f87dbe 100644 --- a/packages/flutter/lib/src/cupertino/sheet.dart +++ b/packages/flutter/lib/src/cupertino/sheet.dart @@ -104,6 +104,11 @@ final Animatable _kScaleTween = Tween(begin: 1.0, end: 1.0 - _kS /// The whole sheet can be popped at once by either dragging down on the sheet, /// or calling [CupertinoSheetRoute.popSheet]. /// +/// When `enableDrag` is set to `true` (the default), users can dismiss the sheet +/// by dragging it down or by calling [CupertinoSheetRoute.popSheet]. When +/// `enableDrag` is `false`, users cannot dismiss the sheet by dragging, and it +/// can only be closed by calling [CupertinoSheetRoute.popSheet]. +/// /// iOS sheet widgets are generally designed to be tightly coupled to the context /// of the widget that opened the sheet. As such, it is not recommended to push /// a non-sheet route that covers the sheet without first popping the sheet. If @@ -135,6 +140,7 @@ Future showCupertinoSheet({ required BuildContext context, required WidgetBuilder pageBuilder, bool useNestedNavigation = false, + bool enableDrag = true, }) { final WidgetBuilder builder; final GlobalKey nestedNavigatorKey = GlobalKey(); @@ -175,7 +181,7 @@ Future showCupertinoSheet({ return Navigator.of( context, rootNavigator: true, - ).push(CupertinoSheetRoute(builder: builder)); + ).push(CupertinoSheetRoute(builder: builder, enableDrag: enableDrag)); } /// Provides an iOS-style sheet transition. @@ -478,15 +484,17 @@ class _CupertinoSheetTransitionState extends State { /// `CupertinoSheetRoute`, with optional nested navigation built in. class CupertinoSheetRoute extends PageRoute with _CupertinoSheetRouteTransitionMixin { /// Creates a page route that displays an iOS styled sheet. - CupertinoSheetRoute({super.settings, required this.builder}); + CupertinoSheetRoute({super.settings, required this.builder, this.enableDrag = true}); /// Builds the primary contents of the sheet route. final WidgetBuilder builder; + @override + final bool enableDrag; + @override Widget buildContent(BuildContext context) { final double bottomPadding = MediaQuery.sizeOf(context).height * _kTopGapRatio; - return MediaQuery.removePadding( context: context, removeTop: true, @@ -563,6 +571,11 @@ mixin _CupertinoSheetRouteTransitionMixin on PageRoute { DelegatedTransitionBuilder? get delegatedTransition => CupertinoSheetTransition.delegateTransition; + /// Determines whether the content can be dragged. + /// + /// If `true`, dragging is enabled; otherwise, it remains fixed. + bool get enableDrag; + @override Widget buildPage( BuildContext context, @@ -588,6 +601,7 @@ mixin _CupertinoSheetRouteTransitionMixin on PageRoute { Animation animation, Animation secondaryAnimation, Widget child, + bool enableDrag, ) { final bool linearTransition = route.popGestureInProgress; return CupertinoSheetTransition( @@ -595,7 +609,7 @@ mixin _CupertinoSheetRouteTransitionMixin on PageRoute { secondaryRouteAnimation: secondaryAnimation, linearTransition: linearTransition, child: _CupertinoDownGestureDetector( - enabledCallback: () => true, + enabledCallback: () => enableDrag, onStartPopGesture: () => _startPopGesture(route), child: child, ), @@ -614,7 +628,7 @@ mixin _CupertinoSheetRouteTransitionMixin on PageRoute { Animation secondaryAnimation, Widget child, ) { - return buildPageTransitions(this, context, animation, secondaryAnimation, child); + return buildPageTransitions(this, context, animation, secondaryAnimation, child, enableDrag); } } diff --git a/packages/flutter/test/cupertino/sheet_test.dart b/packages/flutter/test/cupertino/sheet_test.dart index da8ad9f1cf..42a671a76b 100644 --- a/packages/flutter/test/cupertino/sheet_test.dart +++ b/packages/flutter/test/cupertino/sheet_test.dart @@ -1076,5 +1076,74 @@ void main() { await gesture.up(); await tester.pumpAndSettle(); }); + + testWidgets('dragging does not move the sheet when enableDrag is false', ( + WidgetTester tester, + ) async { + Widget nonDragGestureApp(GlobalKey homeScaffoldKey, GlobalKey sheetScaffoldKey) { + return CupertinoApp( + home: CupertinoPageScaffold( + key: homeScaffoldKey, + child: Center( + child: Column( + children: [ + const Text('Page 1'), + CupertinoButton( + onPressed: () { + showCupertinoSheet( + context: homeScaffoldKey.currentContext!, + pageBuilder: (BuildContext context) { + return CupertinoPageScaffold( + key: sheetScaffoldKey, + child: const Center(child: Text('Page 2')), + ); + }, + enableDrag: false, + ); + }, + child: const Text('Push Page 2'), + ), + ], + ), + ), + ), + ); + } + + final GlobalKey homeKey = GlobalKey(); + final GlobalKey sheetKey = GlobalKey(); + + await tester.pumpWidget(nonDragGestureApp(homeKey, sheetKey)); + + await tester.tap(find.text('Push Page 2')); + await tester.pumpAndSettle(); + + expect(find.text('Page 2'), findsOneWidget); + + RenderBox box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; + final double initialPosition = box.localToGlobal(Offset.zero).dy; + + final TestGesture gesture = await tester.startGesture(const Offset(100, 200)); + // Partial drag down + await gesture.moveBy(const Offset(0, 200)); + await tester.pump(); + + // Release gesture. Sheet should not move. + box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; + final double middlePosition = box.localToGlobal(Offset.zero).dy; + + expect(middlePosition, equals(initialPosition)); + + await gesture.up(); + await tester.pumpAndSettle(); + + expect(find.text('Page 2'), findsOneWidget); + + box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; + final double finalPosition = box.localToGlobal(Offset.zero).dy; + + expect(finalPosition, equals(middlePosition)); + expect(finalPosition, equals(initialPosition)); + }); }); }