Skip to content

[animations] Introduce layoutBuilder parameter to PageTransitionSwitcher #170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Sep 3, 2020
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
98 changes: 85 additions & 13 deletions packages/animations/lib/src/page_transition_switcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

// Internal representation of a child that, now or in the past, was set on the
// PageTransitionSwitcher.child field, but is now in the process of
// transitioning. The internal representation includes fields that we don't want
Copy link
Member

Choose a reason for hiding this comment

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

Leave the comment about how we don't want to expose some of these fields?

// to expose to the public API (like the controllers).
/// An internal representation of a child widget subtree that, now or in the past,
/// was set on the [PageTransitionSwitcher.child] field and is now in the process of
/// transitioning.
///
/// The internal representation includes fields that we don't want to expose to
/// the public API (like the controllers).
class _ChildEntry {
/// Creates a [_ChildEntry].
///
/// The [primaryController], [secondaryController], [transition] and
/// [widgetChild] parameters must not be null.
_ChildEntry({
@required this.primaryController,
@required this.secondaryController,
Expand All @@ -21,17 +27,22 @@ class _ChildEntry {
assert(widgetChild != null),
assert(transition != null);

/// The animation controller for the child's transition.
final AnimationController primaryController;

/// The (curved) animation being used to drive the transition.
final AnimationController secondaryController;

// The currently built transition for this child.
/// The currently built transition for this child.
Widget transition;

// The widget's child at the time this entry was created or updated.
// Used to rebuild the transition if necessary.
/// The widget's child at the time this entry was created or updated.
/// Used to rebuild the transition if necessary.
Widget widgetChild;

/// Release the resources used by this object.
///
/// The object is no longer usable after this method is called.
void dispose() {
primaryController.dispose();
secondaryController.dispose();
Expand All @@ -43,6 +54,16 @@ class _ChildEntry {
}
}

/// Signature for builders used to generate custom layouts for
/// [PageTransitionSwitcher].
///
/// The builder should return a widget which contains the given children, laid
/// out as desired. It must not return null. The builder should be able to
/// handle an empty list of `entries`.
typedef PageTransitionSwitcherLayoutBuilder = Widget Function(
List<Widget> entries,
);

/// Signature for builders used to generate custom transitions for
/// [PageTransitionSwitcher].
///
Expand Down Expand Up @@ -139,6 +160,12 @@ typedef PageTransitionSwitcherTransitionBuilder = Widget Function(
/// progress indicator with key A again, all in rapid succession, then the old
/// progress indicator and the image will be fading out while a new progress
/// indicator is fading in.
///
/// PageTransitionSwitcher uses the [layoutBuilder] property to lay out the
/// old and new child widgets. By default, [defaultLayoutBuilder] is used.
/// See the documentation for [layoutBuilder] for suggestions on how to
/// configure the layout of the incoming and outgoing child widgets if
/// [defaultLayoutBuilder] is not your desired layout.
class PageTransitionSwitcher extends StatefulWidget {
/// Creates a [PageTransitionSwitcher].
///
Expand All @@ -149,10 +176,12 @@ class PageTransitionSwitcher extends StatefulWidget {
this.duration = const Duration(milliseconds: 300),
this.reverse = false,
@required this.transitionBuilder,
this.layoutBuilder = defaultLayoutBuilder,
this.child,
}) : assert(duration != null),
assert(reverse != null),
assert(transitionBuilder != null),
assert(layoutBuilder != null),
super(key: key);

/// The current child widget to display.
Expand Down Expand Up @@ -203,6 +232,52 @@ class PageTransitionSwitcher extends StatefulWidget {
/// The child provided to the transitionBuilder may be null.
final PageTransitionSwitcherTransitionBuilder transitionBuilder;

/// A function that wraps all of the children that are transitioning out, and
/// the [child] that's transitioning in, with a widget that lays all of them
/// out. This is called every time this widget is built. The function must not
/// return null.
///

Choose a reason for hiding this comment

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

We should add some sample code here for one or two use cases for modifying the layoutBuilder here. Otherwise, developers may not know how to go to this parameter to resolve their problem.

Also, since this property's purpose might be less intuitive from just reading its name, it might be helpful to put a pointer to here from the constructor or class API docs as well. Something like: "If the layout of the transitioning child widgets are not what you are looking for, see layoutBuilder for suggestions on how to configure the layout of the incoming and outgoing child widgets."

/// The default [PageTransitionSwitcherLayoutBuilder] used is
/// [defaultLayoutBuilder].
///
/// The following example shows a [layoutBuilder] that places all entries in a
/// [Stack] that sizes itself to match the largest of the active entries.
/// All children are aligned on the top left corner of the [Stack].
///
/// ```dart
/// PageTransitionSwitcher(
/// duration: const Duration(milliseconds: 100),
/// child: Container(color: Colors.red),
/// layoutBuilder: (
/// List<Widget> entries,
/// ) {
/// return Stack(
/// children: entries,
/// alignment: Alignment.topLeft,
/// );
/// },
/// ),
/// ```
/// See [PageTransitionSwitcherLayoutBuilder] for more information about
/// how a layout builder should function.
final PageTransitionSwitcherLayoutBuilder layoutBuilder;

/// The default layout builder for [PageTransitionSwitcher].
///
/// This function is the default way for how the new and old child widgets are placed
/// during the transition between the two widgets. All children are placed in a
/// [Stack] that sizes itself to match the largest of the child or a previous child.
/// The children are centered on each other.
///
/// See [PageTransitionSwitcherTransitionBuilder] for more information on the function
/// signature.
static Widget defaultLayoutBuilder(List<Widget> entries) {
return Stack(
children: entries,
alignment: Alignment.center,
);
}

@override
_PageTransitionSwitcherState createState() => _PageTransitionSwitcherState();
}
Expand Down Expand Up @@ -371,11 +446,8 @@ class _PageTransitionSwitcherState extends State<PageTransitionSwitcher>

@override
Widget build(BuildContext context) {
return Stack(
children: _activeEntries
.map<Widget>((_ChildEntry entry) => entry.transition)
.toList(),
alignment: Alignment.center,
);
return widget.layoutBuilder(_activeEntries
.map<Widget>((_ChildEntry entry) => entry.transition)
.toList());
}
}
19 changes: 19 additions & 0 deletions packages/animations/test/page_transition_switcher_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,25 @@ void main() {
await tester.pumpAndSettle();
});

testWidgets('using custom layout', (WidgetTester tester) async {
Widget newLayoutBuilder(List<Widget> activeEntries) {
return Column(
children: activeEntries,
);
}

await tester.pumpWidget(
PageTransitionSwitcher(
duration: const Duration(milliseconds: 100),
child: Container(color: const Color(0x00000000)),
transitionBuilder: _transitionBuilder,
layoutBuilder: newLayoutBuilder,
),
);

expect(find.byType(Column), findsOneWidget);
});

testWidgets("doesn't transition in a new child of the same type.",
(WidgetTester tester) async {
await tester.pumpWidget(
Expand Down