Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment thread
Melvin-Abraham marked this conversation as resolved.
/// argument.
Comment thread
Melvin-Abraham marked this conversation as resolved.
///
/// 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 _) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is technically a breaking change for any implementation of OpenContainer since the function signature has changed. Do we just mention this in the changelog?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since there is no other way to make the parameter optional, I think just mentioning it in the changelog would be the way to go.

@shihaohong shihaohong May 27, 2020

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

That makes sense! Could you add an entry to the changelog with a migration guide?

It should just outline what a user would have to do to get back to the previous, working state of their code if they were to update their version of the animations package

@Melvin-Abraham Melvin-Abraham May 28, 2020

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@shihaohong I had another look at it and I feel that the changes should not break existing codebase which is evident by the fact that not much changes are made to the open_container_test.dart file and still it works just fine.

  • It is not necessary to specify the type arguments for OpenContainer
  • Action callback in OpenContainer.openBuilder could either be CloseContainerActionCallback or the good old VoidCallback.
  • onClosed is a new feature as it was not available in 1.0.0+5. Also, specifying it is not necessary.

Still, let me know if there are any specific cases where it might break.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I was mostly worried about existing users of the onClosed parameter, but as you pointed out, it was not available in 1.0.0+5 so it should be okay!

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