flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
164.8k stars 27.16k forks source link

[iOS 13] new fullscreen stack type route transition #33798

Open xster opened 5 years ago

xster commented 5 years ago

While the old slide style transition still exists:

Kapture 2019-06-03 at 14 31 13

There's an entirely new style now:

Kapture 2019-06-03 at 14 32 28

xster commented 5 years ago

It seems like there's some inconsistency with the swipe gesture. In a list, sometimes it overscrolls the list, sometimes it dismisses the stack.

xster commented 5 years ago

Ah, the dismissal is isModalInPresentation which overscrolls but doesn't dismiss

justinmc commented 5 years ago

Here's the video from WWDC about this, around the 12:00 mark talking about the tabbed top modals. https://developer.apple.com/videos/play/wwdc2019/224/

jamesblasco commented 4 years ago

Hello, I have been working on this feature here - jamesblasco/flutter-cupertino-modal.

Preview

I have been playing with showModalBottomSheet and DraggableScrollableSheet and I create showCupertinoModalBottomSheet.

Also for scaling the previous route, I use a custom inherited CupertinoScaffold that I add upper inside the tree. I thought about using secondAnimation from TransitionRoute but that makes it way more complex as I would need to modify files like CupertinoPageRoute.

I wanted to know some opinion about this approach before getting my hands back on the code.

(The code is bit disorganized, no test, and a bug needs to be fixed)

justinmc commented 4 years ago

I briefly looked into this awhile ago and I thought that I would need to modify CupertinoPageRoute. I looked at native iOS and assumed that I'd also need to implement multiple routes on the stack, swiping to dismiss all routes on the stack at once, and different route transitions on top of the entire stack, though. This preview gif looks great for the simple case.

CC @xster

jamesblasco commented 4 years ago

@justinmc I think for implementing multiple routes on stack, the best solution would be to add inside a new navigator.

The complex case I can't implement is using a stack of modals like the below case.

See modals stack example
  1. The root route is scaled down when the first modal appears

  2. When the second modal appears, the root route hides down, and the first modal scales up

  3. New modals behave the same as 2

Also it would be also nice to implement support for onWillPopScope. My implementation is a ModalRoute, so I don't think would be very difficult

jamesblasco commented 4 years ago

Ok, I managed to make work onWillPopScope.(It should be improved to behave as native)

spkersten commented 4 years ago

There is also the possibility of these modals to start out covering only half the screen: ezgif-6-62477330719a And to refuse to be dismissed when there is unsaved content: ezgif-6-fc3bda9d28ef

jamesblasco commented 4 years ago

I managed to implement stacking modals with a route transition (not the initial route)

I almost able to accomplish everything except the half screen (I think not even Apple gives that option to developers).

Let me know if I can do something to help to speed up the implementation of this feature.

Matt1700 commented 4 years ago

@jamesblasco I've tried your solution, it works great with a CupertinoPageScaffold. I also tried on a CupertinoTabScaffold and it doesn't work. An example of this usage could be Apple Music, where the tab bar gets out of the way before showing the modal.

jamesblasco commented 4 years ago

@Matt1700 I will try that thanks. CupertinoTabScaffold contains a Navigator so depending of what context you pass it, the new route will be pushed inside CupertinoTabScaffold or in MaterialApp/CupertinoApp or whatever Navigator you have before CupertinoTabScaffold. I think adding the param rootNavigator: true should fix that.

Notice if you pushed inside the CupertinoTabScaffold the tab bar will be displayed the same as if you push a new page with Navigator.of(context).push

Matt1700 commented 4 years ago

@jamesblasco setting useRootNavigator: true does the trick 🎉🎉 Thanks a lot for the help!

jamesblasco commented 4 years ago

@justinmc I managed to animate the previous route by extending MaterialPageRoute(also can be done in CupertinoPageRoute) and override some Route methods. I still think it would be better to implement something like CupertinoPageScaffold.of(context).showModalBottomSheet() .

With that it would be all needed for this feature.

Except the case @spkersten commented here where the modal starts out covering only half the screen. Implementing this with the secondaryAnimation ofCupertinoPageRoute would be difficult or would need a big refactor, as the previous route only animates between the full size-half size states and not when it is pushed. I think it would be doable with CupertinoPageScaffold.of(context).showModalBottomSheet() and animate CupertinoPageScaffold instead of the route.

Also I am not sure it would be correct to add a CupertinoTabScaffold inside by default to be able to push new routes inside the modal or let decide the developer to add it himself

I would need a bit more time to review the code, fix a bug and add documentation, but I am interested on getting some feedback if possible

Preview code of the custom PageRoute ```dart class MaterialWithModalsPageRoute extends MaterialPageRoute { /// Construct a MaterialPageRoute whose contents are defined by [builder]. /// /// The values of [builder], [maintainState], and [fullScreenDialog] must not /// be null. MaterialWithModalsPageRoute({ @required WidgetBuilder builder, RouteSettings settings, bool maintainState = true, bool fullscreenDialog = false, }) : assert(builder != null), assert(maintainState != null), assert(fullscreenDialog != null), assert(opaque), super(settings: settings, fullscreenDialog: fullscreenDialog, builder: builder, maintainState: maintainState); bool shouldAnimateCupertinoModalTranslation = false; @override bool canTransitionTo(TransitionRoute nextRoute) { // Don't perform outgoing animation if the next route is a fullscreen dialog. return (nextRoute is MaterialPageRoute && !nextRoute.fullscreenDialog) || (nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog) || (nextRoute is MaterialWithModalsPageRoute && !nextRoute.fullscreenDialog) || (nextRoute is ModalBottomSheetRoute); } @override void didChangeNext(Route nextRoute) { shouldAnimateCupertinoModalTranslation = (nextRoute is ModalBottomSheetRoute); super.didChangeNext(nextRoute); } @override void didPopNext(Route nextRoute) { shouldAnimateCupertinoModalTranslation = false; super.didPopNext(nextRoute); } @override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme; if(shouldAnimateCupertinoModalTranslation) { // Avoid default transition theme to animate when a new modal view is pushed final fakeSecondaryAnimation = Tween(begin: 0, end: 0).animate(secondaryAnimation); return _CupertinoModalTransition( secondAnimation: secondaryAnimation, body: theme.buildTransitions( this, context, animation, fakeSecondaryAnimation, child)); } else { return theme.buildTransitions( this, context, animation, secondaryAnimation, child); } } } class _CupertinoModalTransition extends StatelessWidget { final Animation secondaryAnimation; final Widget body; const _CupertinoModalTransition( {Key key, @required this.secondaryAnimation, @required this.body}) : super(key: key); @override Widget build(BuildContext context) { double startRoundCorner = 0; final paddingTop = MediaQuery.of(context).padding.top; if (defaultTargetPlatform == TargetPlatform.iOS && paddingTop > 20) { startRoundCorner = 38.5; //https://kylebashour.com/posts/finding-the-real-iphone-x-corner-radius } final curvedAnimation = CurvedAnimation( parent: secondaryAnimation, curve: Curves.easeOut, ); return AnnotatedRegion( value: SystemUiOverlayStyle.light, child: AnimatedBuilder( animation: curvedAnimation, child: body, builder: (context, child) { Widget result = child; final progress = curvedAnimation.value; print(progress); final yOffset = progress * paddingTop; final scale = 1 - progress / 10; final radius = (1 - progress) * startRoundCorner + progress * 12; return Stack( children: [ Container(color: Colors.black), Transform.translate( offset: Offset(0, yOffset), child: Transform.scale( scale: scale, alignment: Alignment.topCenter, child: ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(radius), topRight: Radius.circular(radius)), child: result), ), ) ], ); }, )); } } ```
Matt1700 commented 4 years ago

@jamesblasco is there a way to dismiss the modal programmatically? I've tried with Navigator.of(context).pop() but it doesn't work. I need to implement the cancel button on the navbar.

Gorniv commented 4 years ago

@Matt1700 fix: work Navigator.pop(context)

jamesblasco commented 4 years ago

Thanks @Gorniv. I have been working on an improved version of the solution and I am going to archive the previous project.

As it doesn't look like this issue is going to be worked soon, I have published my code as a pub package. All feedback and PR are welcome.

https://github.com/jamesblasco/modal_bottom_sheet.

I would be happy to help to implement this feature in the future. For now I will stop annoying you with notifications 😅