Skip to content

Conversation

@MitchellGoodwin
Copy link
Contributor

@MitchellGoodwin MitchellGoodwin commented Oct 21, 2025

Fixes #161687

Enables scrollable content within the sheet to work together with the dragging animation.

Flutter:

Screen.Recording.2025-11-25.at.2.14.02.PM.mov

Native:

Screen.Recording.2025-10-16.at.1.42.10.PM.mov

Fling when scrolling to the top vs fling when the scrollable content is already at the top

Screen.Recording.2025-11-25.at.2.16.13.PM.mov

When scrolling is enabled, then the sheet will no longer have a drag gesture recognizer over the sheet content, and will instead rely on the scrollable content to trigger the drag. A non-scrolling area can be wrapped with CupertinoSheetDragArea to put a drag gesture recognizer only on that area, convenient for navbars. See cupertino_sheet.3.dart for a full example.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: cupertino flutter/packages/flutter/cupertino repository labels Oct 21, 2025
@lukepighetti
Copy link
Contributor

missing one minor bit of fidelity. if you are scrolled down and fling to the top it should spring at the top instead of slam to a halt

@MitchellGoodwin
Copy link
Contributor Author

The physics when scrolling to the top is definitely not there yet. It either stops suddenly or bounces when it shouldn't when the drag to dismiss happens. I opened this draft PR to get some feedback on the overall approach.

return Navigator.of(context, rootNavigator: true).push<T>(route);
} else {
widgetBuilder = (BuildContext context) {
Widget nestedNavigationContent(Widget child) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is changed to define an inline function, is the else necessary?

context,
rootNavigator: true,
).push<T>(CupertinoSheetRoute<T>(builder: widgetBuilder, enableDrag: enableDrag));
final PageRoute<T> route = effectiveBuilder != null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you simplify the logic to de-dupe and call on your new wrapper method based on useNestedNavigation?

}
}

class _NeverUserScrollableScrollPhysics extends ScrollPhysics {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed over using NeverScrollableScrollPhysics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to set allowImplicitScrolling to false.

@github-actions github-actions bot added f: scrolling Viewports, list views, slivers, etc. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos and removed f: scrolling Viewports, list views, slivers, etc. labels Nov 21, 2025
child: _CupertinoSheetScope(child: builder(context)),
child: Builder(
builder: (BuildContext context) {
return _CupertinoSheetScope(sheetContext: context, child: _effectiveBuilder(context));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to pass around context? That's an anti-pattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately it's so that _CupertinoDragGestureDetector is able to figure out the height of the sheet for knowing how much to adjust the page transition in response to a user drag.

It gets there through CupertinoSheetDragArea looking up the _CupertinoSheetScope above it and passing along the context to _CupertinoDragGestureDetector.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's really not something we should do. Passing around build context is not safe. DraggableScrollableSheet keeps track of the sheet's extent. Are we able to do that here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can change it so that _CupertinoSheetScope tracks the height/extent rather than the context and that'll work pretty smoothly.

@chunhtai chunhtai self-requested a review November 25, 2025 23:07
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Accessibility, we would need to focus on two things

  1. dismissable

in talkback this is fine as user can do gesture down and left to issue a android back.

For voiceOver though, it is two fingers Z gesture and it calls Semantics(onDismiss). Looking at the semantics tree, I didnt see the semantics action is set.

  1. the draghandle

In talkback, tap should be able to dismiss the sheet similar to

onSemanticsTap: widget.onClosing,

For voicerOver, we may need to check the iOS behavior, do you know whether there is builtin app that uses the native draggable sheet?

bool get enableDrag;

/// Determines whether the content can be scrolled.
bool get enableScroll;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be private?

@chunhtai
Copy link
Contributor

also, while not yet wired up in the engine, can you also apply SemanticsRole.dragHandle to the handle so that we won't forget about this in the future when it is wired up

@MitchellGoodwin
Copy link
Contributor Author

For voicerOver, we may need to check the iOS behavior, do you know whether there is builtin app that uses the native draggable sheet?

You can find the native sheet in the Contacts app, when you add a new contact. You can also find another one in the Settings app through General -> Language & Region -> Add Language

@MitchellGoodwin
Copy link
Contributor Author

also, while not yet wired up in the engine, can you also apply SemanticsRole.dragHandle to the handle so that we won't forget about this in the future when it is wired up

CupertinoSheet currently does not add a drag handle. For the native sheet widget, the drag handle is normally added when the sheet is resizable, which CupertinoSheet does not do by default, nor is this PR adding that.

However, CupertinoSheetDragArea could be wrapped around a drag handle type widget manually added by a dev.

So should we either

  1. Not do anything with the drag handle role for this PR.
  2. Add either a isDragHandle or a more generic SemanticsRole property to CupertinoSheetDragArea.
  3. Add an other SemanticsRole somewhere else that I'm missing.

@MitchellGoodwin
Copy link
Contributor Author

@chunhtai

I tested on a device both the native behavior and this code.

On native the two finger Z-scrub gesture does not dismiss the sheet, it seems. Double tap, then drag does dismiss it however.

For CupertinoSheet, currently the Z-scrub gesture does dismiss it, the same with double tap and drag.

Should we block the two finger Z gesture?

@Piinks
Copy link
Contributor

Piinks commented Dec 2, 2025

CupertinoSheet currently does not add a drag handle. For the native sheet widget, the drag handle is normally added when the sheet is resizable

@MitchellGoodwin have you checked the native behavior with accessibility controls enabled? Does SwiftUI expose the ability to add drag handles?

@MitchellGoodwin
Copy link
Contributor Author

MitchellGoodwin commented Dec 2, 2025

@MitchellGoodwin have you checked the native behavior with accessibility controls enabled? Does SwiftUI expose the ability to add drag handles?

@Piinks I have checked the native behavior with accessibility controls, using a physical device.

For the drag handle on native, they do expose a way to add it through prefersGrabberVisibile. They refer to the drag handle as a "grabber".

From the HIGs documentation on sheets, they say:

"Include a grabber in a resizable sheet. A grabber shows people that they can drag the sheet to resize it; they can also tap it to cycle through the detents. In addition to providing a visual indicator of resizability, a grabber also works with VoiceOver so people can resize the sheet without seeing the screen."

I filled an issue for the drag handle #179358. I think that can be done in a separate PR from this one.

@MitchellGoodwin MitchellGoodwin marked this pull request as ready for review December 5, 2025 23:01
Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just trying to think through an alternative approach... Rather than a new constructor for CupertinoSheetRoute that passes a ScrollController, could you use PrimaryScrollController.of inside of CupertinoSheetRoute and ask users to do the same and pass that to their relevant scroll view? I'm no expert in this area but it seems kind of aligned with the intention of PrimaryScrollController.

If not PrimaryScrollController, then would a new similar InheritedWidget make sense? ...Or would it just make this more complicated and less discoverable?

Otherwise, do you know how this kind of thing works in SwiftUI? Is it always the entire CupertinoSheet that is scrollable?

Assuming none of these drive-by ideas is viable, and that this covers the main use cases from SwiftUI, then this approach looks good to me!

Comment on lines 99 to 106
/// This is a convenience method for displaying [CupertinoSheetRoute] for common,
/// straightforward use cases. The Widget returned from `pageBuilder` will be
/// used to display the content on the [CupertinoSheetRoute].
/// straightforward use cases. There are two different build methods that are provided
/// by the method for building the content of the [CupertinoSheetRoute]. Use `builder`
/// if there is no content within the sheet that needs to scroll. This is good
/// for sheets with a simple display, however scrolling gestures will conflict
/// with the drag to dismiss gesture. `scrollableBuilder` will enable scrollable
/// content within the sheet. See [CupertinoSheetRoute.scrollable] for more
/// information.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Should you mention that this is for vertical scrolling?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout

@MitchellGoodwin
Copy link
Contributor Author

Just trying to think through an alternative approach... Rather than a new constructor for CupertinoSheetRoute that passes a ScrollController, could you use PrimaryScrollController.of inside of CupertinoSheetRoute and ask users to do the same and pass that to their relevant scroll view? I'm no expert in this area but it seems kind of aligned with the intention of PrimaryScrollController.

@justinmc The only issue with that is that we would need to know that the sheet content is intended to be scrollable. So, we could provide the controller with PrimaryScrollController but I think we'd need to have at least a scrollable boolean property in the constructor. When set to true it doesn't apply the drag gesture detector over the whole sheet.

If not PrimaryScrollController, then would a new similar InheritedWidget make sense? ...Or would it just make this more complicated and less discoverable?

I don't think a new widget would be needed. As far the user of this API would be concerned, the scroll controller isn't unique in how they implement it. This would make this API further different from DraggableScrollableSheet, so there's a difference in consistency there.

Otherwise, do you know how this kind of thing works in SwiftUI? Is it always the entire CupertinoSheet that is scrollable?

You can just put a scroll view within the sheet and it works as expected. Scrollable areas in Swift know when they are the child of a resizable area and handle things accordingly. Similarly they are able to have priority over the drag gesture that might be on the rest of the sheet.

The most common use case is that the majority of the sheet, except for the nav bar is scrollable. So the situation in the example.

opacity:
kind: 'fragment'
value: 'arguments[0]'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping comment to link to for reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: tech-debt Technical debt, code quality, testing, etc. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: cupertino flutter/packages/flutter/cupertino repository framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cupertino Sheet should have drag to dismiss and nested scrolling work together

5 participants