Skip to content

Commit

Permalink
[animations] Support for returning value from popped route (#153)
Browse files Browse the repository at this point in the history
* Added support for returning value from popped route

* Example for `onClosed` added
  • Loading branch information
Melvin-Abraham committed Jun 1, 2020
1 parent 7400380 commit 22cf8a9
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 27 deletions.
48 changes: 42 additions & 6 deletions packages/animations/example/lib/container_transition.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ class OpenContainerTransformDemo extends StatefulWidget {
class _OpenContainerTransformDemoState
extends State<OpenContainerTransformDemo> {
ContainerTransitionType _transitionType = ContainerTransitionType.fade;
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();

void _showMarkedAsDoneSnackbar(bool isMarkedAsDone) {
if (isMarkedAsDone ?? false)
scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text('Marked as done!'),
));
}

void _showSettingsBottomModalSheet(BuildContext context) {
showModalBottomSheet<void>(
Expand Down Expand Up @@ -103,6 +111,7 @@ class _OpenContainerTransformDemoState
@override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
title: const Text('Container transform'),
actions: <Widget>[
Expand All @@ -122,13 +131,15 @@ class _OpenContainerTransformDemoState
closedBuilder: (BuildContext _, VoidCallback openContainer) {
return _ExampleCard(openContainer: openContainer);
},
onClosed: _showMarkedAsDoneSnackbar,
),
const SizedBox(height: 16.0),
_OpenContainerWrapper(
transitionType: _transitionType,
closedBuilder: (BuildContext _, VoidCallback openContainer) {
return _ExampleSingleTile(openContainer: openContainer);
},
onClosed: _showMarkedAsDoneSnackbar,
),
const SizedBox(height: 16.0),
Row(
Expand All @@ -142,6 +153,7 @@ class _OpenContainerTransformDemoState
subtitle: 'Secondary text',
);
},
onClosed: _showMarkedAsDoneSnackbar,
),
),
const SizedBox(width: 8.0),
Expand All @@ -154,6 +166,7 @@ class _OpenContainerTransformDemoState
subtitle: 'Secondary text',
);
},
onClosed: _showMarkedAsDoneSnackbar,
),
),
],
Expand All @@ -170,6 +183,7 @@ class _OpenContainerTransformDemoState
subtitle: 'Secondary',
);
},
onClosed: _showMarkedAsDoneSnackbar,
),
),
const SizedBox(width: 8.0),
Expand All @@ -182,6 +196,7 @@ class _OpenContainerTransformDemoState
subtitle: 'Secondary',
);
},
onClosed: _showMarkedAsDoneSnackbar,
),
),
const SizedBox(width: 8.0),
Expand All @@ -194,17 +209,19 @@ class _OpenContainerTransformDemoState
subtitle: 'Secondary',
);
},
onClosed: _showMarkedAsDoneSnackbar,
),
),
],
),
const SizedBox(height: 16.0),
...List<Widget>.generate(10, (int index) {
return OpenContainer(
return OpenContainer<bool>(
transitionType: _transitionType,
openBuilder: (BuildContext _, VoidCallback openContainer) {
return _DetailsPage();
return const _DetailsPage();
},
onClosed: _showMarkedAsDoneSnackbar,
tappable: false,
closedShape: const RoundedRectangleBorder(),
closedElevation: 0.0,
Expand All @@ -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(
Expand Down Expand Up @@ -256,18 +275,21 @@ class _OpenContainerWrapper extends StatelessWidget {
const _OpenContainerWrapper({
this.closedBuilder,
this.transitionType,
this.onClosed,
});

final OpenContainerBuilder closedBuilder;
final ContainerTransitionType transitionType;
final ClosedCallback<bool> onClosed;

@override
Widget build(BuildContext context) {
return OpenContainer(
return OpenContainer<bool>(
transitionType: transitionType,
openBuilder: (BuildContext context, VoidCallback _) {
return _DetailsPage();
return const _DetailsPage();
},
onClosed: onClosed,
tappable: false,
closedBuilder: closedBuilder,
);
Expand Down Expand Up @@ -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: <Widget>[
if (includeMarkAsDoneButton)
IconButton(
icon: const Icon(Icons.done),
onPressed: () => Navigator.pop(context, true),
tooltip: 'Mark as done',
)
],
),
body: ListView(
children: <Widget>[
Container(
Expand Down
69 changes: 49 additions & 20 deletions packages/animations/lib/src/open_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<S> = 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<S> = Widget Function(
BuildContext context,
CloseContainerActionCallback<S> 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,
);
Expand All @@ -29,6 +44,10 @@ enum ContainerTransitionType {
fadeThrough,
}

/// Callback function which is called when the [OpenContainer]
/// is closed.
typedef ClosedCallback<S> = 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
Expand All @@ -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<T extends Object> 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,
Expand Down Expand Up @@ -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<T> onClosed;

/// Called to obtain the child for the container in the closed state.
///
Expand All @@ -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.
///
Expand All @@ -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<T> openBuilder;

/// Whether the entire closed container can be tapped to open it.
///
Expand Down Expand Up @@ -218,10 +247,10 @@ class OpenContainer extends StatefulWidget {
final bool useRootNavigator;

@override
_OpenContainerState createState() => _OpenContainerState();
_OpenContainerState<T> createState() => _OpenContainerState<T>();
}

class _OpenContainerState extends State<OpenContainer> {
class _OpenContainerState<T> extends State<OpenContainer<T>> {
// 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
Expand All @@ -235,10 +264,10 @@ class _OpenContainerState extends State<OpenContainer> {
final GlobalKey _closedBuilderKey = GlobalKey();

Future<void> openContainer() async {
await Navigator.of(
final T data = await Navigator.of(
context,
rootNavigator: widget.useRootNavigator,
).push(_OpenContainerRoute(
).push(_OpenContainerRoute<T>(
closedColor: widget.closedColor,
openColor: widget.openColor,
closedElevation: widget.closedElevation,
Expand All @@ -254,7 +283,7 @@ class _OpenContainerState extends State<OpenContainer> {
useRootNavigator: widget.useRootNavigator,
));
if (widget.onClosed != null) {
widget.onClosed();
widget.onClosed(data);
}
}

Expand Down Expand Up @@ -350,7 +379,7 @@ class _HideableState extends State<_Hideable> {
}
}

class _OpenContainerRoute extends ModalRoute<void> {
class _OpenContainerRoute<T> extends ModalRoute<T> {
_OpenContainerRoute({
@required this.closedColor,
@required this.openColor,
Expand Down Expand Up @@ -506,8 +535,8 @@ class _OpenContainerRoute extends ModalRoute<void> {
final Color openColor;
final double openElevation;
final ShapeBorder openShape;
final OpenContainerBuilder closedBuilder;
final OpenContainerBuilder openBuilder;
final CloseContainerBuilder closedBuilder;
final OpenContainerBuilder<T> openBuilder;

// See [_OpenContainerState._hideableKey].
final GlobalKey<_HideableState> hideableKey;
Expand Down Expand Up @@ -587,7 +616,7 @@ class _OpenContainerRoute extends ModalRoute<void> {
}

@override
bool didPop(void result) {
bool didPop(T result) {
_takeMeasurements(
navigatorContext: subtreeContext,
delayForSourceRoute: true,
Expand Down Expand Up @@ -667,8 +696,8 @@ class _OpenContainerRoute extends ModalRoute<void> {
return wasInProgress && isInProgress;
}

void closeContainer() {
Navigator.of(subtreeContext).pop();
void closeContainer({T returnValue}) {
Navigator.of(subtreeContext).pop(returnValue);
}

@override
Expand Down
47 changes: 46 additions & 1 deletion packages/animations/test/open_container_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<bool>(
onClosed: (bool poppedValue) {
value = poppedValue;
},
closedBuilder: (BuildContext context, VoidCallback action) {
return GestureDetector(
onTap: action,
child: const Text('Closed'),
);
},
openBuilder:
(BuildContext context, CloseContainerActionCallback<bool> 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,
Expand Down

0 comments on commit 22cf8a9

Please sign in to comment.