diff --git a/packages/animations/example/lib/container_transition.dart b/packages/animations/example/lib/container_transition.dart index 867ace0958b2..155b5a4eec55 100644 --- a/packages/animations/example/lib/container_transition.dart +++ b/packages/animations/example/lib/container_transition.dart @@ -49,6 +49,14 @@ class OpenContainerTransformDemo extends StatefulWidget { class _OpenContainerTransformDemoState extends State { ContainerTransitionType _transitionType = ContainerTransitionType.fade; + final GlobalKey scaffoldKey = GlobalKey(); + + void _showMarkedAsDoneSnackbar(bool isMarkedAsDone) { + if (isMarkedAsDone ?? false) + scaffoldKey.currentState.showSnackBar(const SnackBar( + content: Text('Marked as done!'), + )); + } void _showSettingsBottomModalSheet(BuildContext context) { showModalBottomSheet( @@ -103,6 +111,7 @@ class _OpenContainerTransformDemoState @override Widget build(BuildContext context) { return Scaffold( + key: scaffoldKey, appBar: AppBar( title: const Text('Container transform'), actions: [ @@ -122,6 +131,7 @@ class _OpenContainerTransformDemoState closedBuilder: (BuildContext _, VoidCallback openContainer) { return _ExampleCard(openContainer: openContainer); }, + onClosed: _showMarkedAsDoneSnackbar, ), const SizedBox(height: 16.0), _OpenContainerWrapper( @@ -129,6 +139,7 @@ class _OpenContainerTransformDemoState closedBuilder: (BuildContext _, VoidCallback openContainer) { return _ExampleSingleTile(openContainer: openContainer); }, + onClosed: _showMarkedAsDoneSnackbar, ), const SizedBox(height: 16.0), Row( @@ -142,6 +153,7 @@ class _OpenContainerTransformDemoState subtitle: 'Secondary text', ); }, + onClosed: _showMarkedAsDoneSnackbar, ), ), const SizedBox(width: 8.0), @@ -154,6 +166,7 @@ class _OpenContainerTransformDemoState subtitle: 'Secondary text', ); }, + onClosed: _showMarkedAsDoneSnackbar, ), ), ], @@ -170,6 +183,7 @@ class _OpenContainerTransformDemoState subtitle: 'Secondary', ); }, + onClosed: _showMarkedAsDoneSnackbar, ), ), const SizedBox(width: 8.0), @@ -182,6 +196,7 @@ class _OpenContainerTransformDemoState subtitle: 'Secondary', ); }, + onClosed: _showMarkedAsDoneSnackbar, ), ), const SizedBox(width: 8.0), @@ -194,17 +209,19 @@ class _OpenContainerTransformDemoState subtitle: 'Secondary', ); }, + onClosed: _showMarkedAsDoneSnackbar, ), ), ], ), const SizedBox(height: 16.0), ...List.generate(10, (int index) { - return OpenContainer( + return OpenContainer( transitionType: _transitionType, openBuilder: (BuildContext _, VoidCallback openContainer) { - return _DetailsPage(); + return const _DetailsPage(); }, + onClosed: _showMarkedAsDoneSnackbar, tappable: false, closedShape: const RoundedRectangleBorder(), closedElevation: 0.0, @@ -226,7 +243,9 @@ class _OpenContainerTransformDemoState floatingActionButton: OpenContainer( transitionType: _transitionType, openBuilder: (BuildContext context, VoidCallback _) { - return _DetailsPage(); + return const _DetailsPage( + includeMarkAsDoneButton: false, + ); }, closedElevation: 6.0, closedShape: const RoundedRectangleBorder( @@ -256,18 +275,21 @@ class _OpenContainerWrapper extends StatelessWidget { const _OpenContainerWrapper({ this.closedBuilder, this.transitionType, + this.onClosed, }); final OpenContainerBuilder closedBuilder; final ContainerTransitionType transitionType; + final ClosedCallback onClosed; @override Widget build(BuildContext context) { - return OpenContainer( + return OpenContainer( transitionType: transitionType, openBuilder: (BuildContext context, VoidCallback _) { - return _DetailsPage(); + return const _DetailsPage(); }, + onClosed: onClosed, tappable: false, closedBuilder: closedBuilder, ); @@ -453,10 +475,24 @@ class _InkWellOverlay extends StatelessWidget { } class _DetailsPage extends StatelessWidget { + const _DetailsPage({this.includeMarkAsDoneButton = true}); + + final bool includeMarkAsDoneButton; + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Details page')), + appBar: AppBar( + title: const Text('Details page'), + actions: [ + if (includeMarkAsDoneButton) + IconButton( + icon: const Icon(Icons.done), + onPressed: () => Navigator.pop(context, true), + tooltip: 'Mark as done', + ) + ], + ), body: ListView( children: [ Container( diff --git a/packages/animations/lib/src/open_container.dart b/packages/animations/lib/src/open_container.dart index c4a1f6f066c0..52743229342d 100644 --- a/packages/animations/lib/src/open_container.dart +++ b/packages/animations/lib/src/open_container.dart @@ -5,13 +5,28 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -/// Signature for a function that creates a [Widget] to be used within an +/// Signature for `action` callback function provided to [OpenContainer.openBuilder]. +/// +/// Parameter `returnValue` is the value which will be provided to [OpenContainer.onClosed] +/// when `action` is called. +typedef CloseContainerActionCallback = void Function({S returnValue}); + +/// Signature for a function that creates a [Widget] in open state within an /// [OpenContainer]. /// /// The `action` callback provided to [OpenContainer.openBuilder] can be used -/// to close the container. The `action` callback provided to -/// [OpenContainer.closedBuilder] can be used to open the container again. -typedef OpenContainerBuilder = Widget Function( +/// to close the container. +typedef OpenContainerBuilder = Widget Function( + BuildContext context, + CloseContainerActionCallback action, +); + +/// Signature for a function that creates a [Widget] in closed state within an +/// [OpenContainer]. +/// +/// The `action` callback provided to [OpenContainer.closedBuilder] can be used +/// to open the container. +typedef CloseContainerBuilder = Widget Function( BuildContext context, VoidCallback action, ); @@ -29,6 +44,10 @@ enum ContainerTransitionType { fadeThrough, } +/// Callback function which is called when the [OpenContainer] +/// is closed. +typedef ClosedCallback = void Function(S data); + /// A container that grows to fill the screen to reveal new content when tapped. /// /// While the container is closed, it shows the [Widget] returned by @@ -45,17 +64,21 @@ enum ContainerTransitionType { /// [closedBuilder] exist in the tree at the same time. Therefore, the widgets /// returned by these builders cannot include the same global key. /// +/// `T` refers to the type of data returned by the route when the container +/// is closed. This value can be accessed in the `onClosed` function. +/// // TODO(goderbauer): Add example animations and sample code. /// /// See also: /// /// * [Transitions with animated containers](https://material.io/design/motion/choreography.html#transformation) /// in the Material spec. -class OpenContainer extends StatefulWidget { +@optionalTypeArgs +class OpenContainer extends StatefulWidget { /// Creates an [OpenContainer]. /// /// All arguments except for [key] must not be null. The arguments - /// [closedBuilder] and [openBuilder] are required. + /// [openBuilder] and [closedBuilder] are required. const OpenContainer({ Key key, this.closedColor = Colors.white, @@ -167,7 +190,13 @@ class OpenContainer extends StatefulWidget { final ShapeBorder openShape; /// Called when the container was popped and has returned to the closed state. - final VoidCallback onClosed; + /// + /// The return value from the popped screen is passed to this function as an + /// argument. + /// + /// If no value is returned via [Navigator.pop] or [OpenContainer.openBuilder.action], + /// `null` will be returned by default. + final ClosedCallback onClosed; /// Called to obtain the child for the container in the closed state. /// @@ -177,7 +206,7 @@ class OpenContainer extends StatefulWidget { /// /// The `action` callback provided to the builder can be called to open the /// container. - final OpenContainerBuilder closedBuilder; + final CloseContainerBuilder closedBuilder; /// Called to obtain the child for the container in the open state. /// @@ -187,7 +216,7 @@ class OpenContainer extends StatefulWidget { /// /// The `action` callback provided to the builder can be called to close the /// container. - final OpenContainerBuilder openBuilder; + final OpenContainerBuilder openBuilder; /// Whether the entire closed container can be tapped to open it. /// @@ -218,10 +247,10 @@ class OpenContainer extends StatefulWidget { final bool useRootNavigator; @override - _OpenContainerState createState() => _OpenContainerState(); + _OpenContainerState createState() => _OpenContainerState(); } -class _OpenContainerState extends State { +class _OpenContainerState extends State> { // Key used in [_OpenContainerRoute] to hide the widget returned by // [OpenContainer.openBuilder] in the source route while the container is // opening/open. A copy of that widget is included in the @@ -235,10 +264,10 @@ class _OpenContainerState extends State { final GlobalKey _closedBuilderKey = GlobalKey(); Future openContainer() async { - await Navigator.of( + final T data = await Navigator.of( context, rootNavigator: widget.useRootNavigator, - ).push(_OpenContainerRoute( + ).push(_OpenContainerRoute( closedColor: widget.closedColor, openColor: widget.openColor, closedElevation: widget.closedElevation, @@ -254,7 +283,7 @@ class _OpenContainerState extends State { useRootNavigator: widget.useRootNavigator, )); if (widget.onClosed != null) { - widget.onClosed(); + widget.onClosed(data); } } @@ -350,7 +379,7 @@ class _HideableState extends State<_Hideable> { } } -class _OpenContainerRoute extends ModalRoute { +class _OpenContainerRoute extends ModalRoute { _OpenContainerRoute({ @required this.closedColor, @required this.openColor, @@ -506,8 +535,8 @@ class _OpenContainerRoute extends ModalRoute { final Color openColor; final double openElevation; final ShapeBorder openShape; - final OpenContainerBuilder closedBuilder; - final OpenContainerBuilder openBuilder; + final CloseContainerBuilder closedBuilder; + final OpenContainerBuilder openBuilder; // See [_OpenContainerState._hideableKey]. final GlobalKey<_HideableState> hideableKey; @@ -587,7 +616,7 @@ class _OpenContainerRoute extends ModalRoute { } @override - bool didPop(void result) { + bool didPop(T result) { _takeMeasurements( navigatorContext: subtreeContext, delayForSourceRoute: true, @@ -667,8 +696,8 @@ class _OpenContainerRoute extends ModalRoute { return wasInProgress && isInProgress; } - void closeContainer() { - Navigator.of(subtreeContext).pop(); + void closeContainer({T returnValue}) { + Navigator.of(subtreeContext).pop(returnValue); } @override diff --git a/packages/animations/test/open_container_test.dart b/packages/animations/test/open_container_test.dart index df54258ea72b..77e9eb9e03e2 100644 --- a/packages/animations/test/open_container_test.dart +++ b/packages/animations/test/open_container_test.dart @@ -1486,7 +1486,7 @@ void main() { (WidgetTester tester) async { bool hasClosed = false; final Widget openContainer = OpenContainer( - onClosed: () { + onClosed: (dynamic _) { hasClosed = true; }, closedBuilder: (BuildContext context, VoidCallback action) { @@ -1525,6 +1525,51 @@ void main() { expect(hasClosed, isTrue); }); + testWidgets( + 'onClosed callback receives popped value when container has closed', + (WidgetTester tester) async { + bool value = false; + final Widget openContainer = OpenContainer( + onClosed: (bool poppedValue) { + value = poppedValue; + }, + closedBuilder: (BuildContext context, VoidCallback action) { + return GestureDetector( + onTap: action, + child: const Text('Closed'), + ); + }, + openBuilder: + (BuildContext context, CloseContainerActionCallback action) { + return GestureDetector( + onTap: () => action(returnValue: true), + child: const Text('Open'), + ); + }, + ); + + await tester.pumpWidget( + _boilerplate(child: openContainer), + ); + + expect(find.text('Open'), findsNothing); + expect(find.text('Closed'), findsOneWidget); + expect(value, isFalse); + + await tester.tap(find.text('Closed')); + await tester.pumpAndSettle(); + + expect(find.text('Open'), findsOneWidget); + expect(find.text('Closed'), findsNothing); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + expect(find.text('Open'), findsNothing); + expect(find.text('Closed'), findsOneWidget); + expect(value, isTrue); + }); + Widget _createRootNavigatorTest({ @required Key appKey, @required Key nestedNavigatorKey,