From 0ab081783f6c5d93fffe97d8e813446102d55550 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 27 Nov 2019 12:09:28 -0800 Subject: [PATCH 1/9] switcher works --- packages/animations/lib/animations.dart | 1 + packages/animations/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/animations/lib/animations.dart b/packages/animations/lib/animations.dart index f58279ddaa58..bfeba39c7e9b 100644 --- a/packages/animations/lib/animations.dart +++ b/packages/animations/lib/animations.dart @@ -3,3 +3,4 @@ // found in the LICENSE file. export 'src/open_container.dart'; +export 'src/page_transition_switcher.dart'; diff --git a/packages/animations/pubspec.yaml b/packages/animations/pubspec.yaml index 4679d9b67b64..6bfc61343745 100644 --- a/packages/animations/pubspec.yaml +++ b/packages/animations/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.0.1 homepage: https://github.com/flutter/packages/tree/master/packages/animations environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.2.2 <3.0.0" dependencies: flutter: From 1c95b8651d8b29380c2a0f65f43c9070ce1cda08 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Mon, 2 Dec 2019 10:21:49 -0800 Subject: [PATCH 2/9] add actual implementation --- .../lib/src/page_transition_switcher.dart | 303 +++++++++ .../test/page_transition_switcher_test.dart | 582 ++++++++++++++++++ 2 files changed, 885 insertions(+) create mode 100644 packages/animations/lib/src/page_transition_switcher.dart create mode 100644 packages/animations/test/page_transition_switcher_test.dart diff --git a/packages/animations/lib/src/page_transition_switcher.dart b/packages/animations/lib/src/page_transition_switcher.dart new file mode 100644 index 000000000000..4d2e3c74dc09 --- /dev/null +++ b/packages/animations/lib/src/page_transition_switcher.dart @@ -0,0 +1,303 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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 +// to expose to the public API (like the controllers). +class _ChildEntry { + _ChildEntry({ + @required this.primaryController, + @required this.secondaryController, + @required this.transition, + @required this.widgetChild, + }) : assert(primaryController != null), + assert(secondaryController != null), + assert(widgetChild != null), + assert(transition != null); + + final AnimationController primaryController; + + final AnimationController secondaryController; + + // 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. + Widget widgetChild; + + void dispose() { + primaryController.dispose(); + secondaryController.dispose(); + } + + @override + String toString() => 'Entry#${shortHash(this)}($widgetChild)'; +} + +/// Signature for builders used to generate custom transitions for +/// [PageTransitionSwitcher]. +/// +/// The function should return a widget which wraps the given `child`. +typedef PageTransitionSwitcherTransitionBuilder = Widget Function( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, +); + +/// A widget that transitions from a previously set child to a new child using +/// an animation specified by [transitionBuilder]. +/// +/// This is a variation of an [AnimatedSwitcher], but it instead of using the +/// same transition for enter and exit, two separate transitions can be +/// specified, similar to how the enter and exit transitions of a [PageRoute] +/// are defined. +/// +/// The transitions returned by the [transitionBuilder] are driven by two +/// animations: a primary one and a secondary one. When a new child is +/// transitioning in while [reverse] is false, the primary animation of the +/// transition associated with that new child is running forward. At the same +/// time, the secondary animation of the previous child is playing forward to +/// transition that child out. In other words, the primary animation defines +/// how a child enters, and the secondary animation determines how it leaves. +/// This is similar to the transition associated with pushing a new [PageRoute] +/// on top of another. +/// +/// When [reverse] is true, then the primary animation of the previous child +/// is playing in reverse to reveal the new child underneath, whose secondary +/// animation is also playing in reverse. This is similar to popping a +/// [PageRoute] to reveal a new [PageRoute] underneath it. +/// +/// If the children are swapped fast enough (i.e. before [duration] elapses), +/// more than one previous child can exist and be transitioning out while the +/// newest one is transitioning in. +/// +/// If the "new" child is the same widget type and key as the "old" child, but +/// with different parameters, then [PageTransitionSwitcher] will *not* do a +/// transition between them, since as far as the framework is concerned, they +/// are the same widget and the existing widget can be updated with the new +/// parameters. To force the transition to occur, set a [Key] on each child +/// widget that you wish to be considered unique (typically a [ValueKey] on the +/// widget data that distinguishes this child from the others). +/// +/// The same key can be used for a new child as was used for an already-outgoing +/// child; the two will not be considered related. (For example, if a progress +/// indicator with key A is first shown, then an image with key B, then another +/// 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.) +class PageTransitionSwitcher extends StatefulWidget { + /// Creates an [PageTransitionSwitcher]. + /// + /// The [duration], [reverse], and [transitionBuilder] parameters + /// must not be null. + const PageTransitionSwitcher({ + Key key, + this.duration = const Duration(milliseconds: 300), + this.reverse = false, + @required this.transitionBuilder, + this.child, + }) : assert(duration != null), + assert(reverse != null), + assert(transitionBuilder != null), + super(key: key); + + /// The current child widget to display. + /// + /// If there was a previous child, it will be transitioning out using the + /// secondary animation of the [transitionBuilder], while the new child + /// transitions in using the primary animation of the [transitionBuilder]. + /// + /// If there was no previous child, then this child will transition in using + /// the primary animation of the [transitionBuilder]. + /// + /// The child is considered to be "new" if it has a different type or [Key] + /// (see [Widget.canUpdate]). + final Widget child; + + /// The duration of the transition from the old [child] value to the new one. + /// + /// This duration is applied to the given [child] when that property is set to + /// a new child. Changing [duration] will not affect the + /// durations of transitions already in progress. + final Duration duration; + + /// Indicates the direction of the animation. + /// + /// When this is false, the new child will transitioning in on top of the + /// previous child while its primary animation and the secondary animation + /// of the previous child are running forward. This is similar to the + /// transition associated with pushing a new [PageRoute] on top of another. + /// + /// When this is true, the new child will transitioning in below the + /// previous child while its secondary animation and the primary animation + /// of the previous child are running in reverse. This is similar to the + /// transition associated with popping a [PageRoute] to reveal a new + /// [PageRoute] below it. + final bool reverse; + + /// A function that wraps a new [child] with a primary and secondary animation + /// to transition between the previously set child and the current child. + /// + /// This is only called when a new [child] is set (not for each build), or + /// when a new [transitionBuilder] is set. If a new [transitionBuilder] is + /// set, then the transition is rebuilt for the current child and all previous + /// children using the new [transitionBuilder]. The function must not return + /// null. + final PageTransitionSwitcherTransitionBuilder transitionBuilder; + + @override + _PageTransitionSwitcherState createState() => _PageTransitionSwitcherState(); +} + +class _PageTransitionSwitcherState extends State with TickerProviderStateMixin { + final List<_ChildEntry> _activeEntries = <_ChildEntry>[]; + _ChildEntry _currentEntry; + int _childNumber = 0; + + @override + void initState() { + super.initState(); + _addEntryForNewChild(animate: false); + } + + @override + void didUpdateWidget(PageTransitionSwitcher oldWidget) { + super.didUpdateWidget(oldWidget); + + // If the transition builder changed, then update all of the previous + // transitions. + if (widget.transitionBuilder != oldWidget.transitionBuilder) { + _activeEntries.forEach(_updateTransitionForEntry); + } + + final bool hasNewChild = widget.child != null; + final bool hasOldChild = _currentEntry != null; + if (hasNewChild != hasOldChild || hasNewChild && !Widget.canUpdate(widget.child, _currentEntry.widgetChild)) { + // Child has changed, fade current entry out and add new entry. + _childNumber += 1; + _addEntryForNewChild(animate: true); + } else if (_currentEntry != null) { + assert(hasOldChild && hasNewChild); + assert(Widget.canUpdate(widget.child, _currentEntry.widgetChild)); + // Child has been updated. Make sure we update the child widget and + // transition in _currentEntry even though we're not going to start a new + // animation, but keep the key from the previous transition so that we + // update the transition instead of replacing it. + _currentEntry.widgetChild = widget.child; + _updateTransitionForEntry(_currentEntry); // uses entry.widgetChild + } + } + + void _addEntryForNewChild({@required bool animate}) { + assert(animate || _currentEntry == null); + if (_currentEntry != null) { + assert(animate); + if (widget.reverse) { + _currentEntry.primaryController.reverse(); + } else { + _currentEntry.secondaryController.forward(); + } + _currentEntry = null; + } + if (widget.child == null) + return; + final AnimationController primaryController = AnimationController( + duration: widget.duration, + vsync: this, + ); + final AnimationController secondaryController = AnimationController( + duration: widget.duration, + vsync: this, + ); + if (animate) { + if (widget.reverse) { + primaryController.value = 1.0; + secondaryController.value = 1.0; + secondaryController.reverse(); + } else { + primaryController.forward(); + } + } else { + assert(_activeEntries.isEmpty); + primaryController.value = 1.0; + } + _currentEntry = _newEntry( + child: widget.child, + primaryController: primaryController, + secondaryController: secondaryController, + builder: widget.transitionBuilder, + ); + if (widget.reverse && _activeEntries.isNotEmpty) { + // Add below previous child. + _activeEntries.insert(_activeEntries.length - 1, _currentEntry); + } else { + // Add on top of previous child. + _activeEntries.add(_currentEntry); + } + } + + _ChildEntry _newEntry({ + @required Widget child, + @required PageTransitionSwitcherTransitionBuilder builder, + @required AnimationController primaryController, + @required AnimationController secondaryController, + }) { + final _ChildEntry entry = _ChildEntry( + widgetChild: child, + transition: KeyedSubtree.wrap(builder(child, primaryController, secondaryController), _childNumber), + primaryController: primaryController, + secondaryController: secondaryController, + ); + secondaryController.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + assert(mounted); + assert(_activeEntries.contains(entry)); + setState(() { + _activeEntries.remove(entry); + entry.dispose(); + }); + } + }); + primaryController.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.dismissed) { + assert(mounted); + assert(_activeEntries.contains(entry)); + setState(() { + _activeEntries.remove(entry); + entry.dispose(); + }); + } + }); + return entry; + } + + void _updateTransitionForEntry(_ChildEntry entry) { + entry.transition = KeyedSubtree( + key: entry.transition.key, + child: widget.transitionBuilder(entry.widgetChild, entry.primaryController, entry.secondaryController), + ); + } + + @override + void dispose() { + for (_ChildEntry entry in _activeEntries) { + entry.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: _activeEntries.map((_ChildEntry entry) => entry.transition).toList(), + alignment: Alignment.center, + ); + } +} diff --git a/packages/animations/test/page_transition_switcher_test.dart b/packages/animations/test/page_transition_switcher_test.dart new file mode 100644 index 000000000000..673f39b26679 --- /dev/null +++ b/packages/animations/test/page_transition_switcher_test.dart @@ -0,0 +1,582 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:animations/animations.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('transitions in a new child.', (WidgetTester tester) async { + final UniqueKey containerOne = UniqueKey(); + final UniqueKey containerTwo = UniqueKey(); + final UniqueKey containerThree = UniqueKey(); + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerOne, color: const Color(0x00000000)), + transitionBuilder: _transitionBuilder, + ), + ); + + Map _primaryAnimation = _getPrimaryAnimation([containerOne], tester); + Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); + expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerTwo, color: const Color(0xff000000)), + transitionBuilder: _transitionBuilder, + ), + ); + await tester.pump(const Duration(milliseconds: 40)); + + _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo], tester); + _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); + // Secondary is running for outgoing widget. + expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.4)); + // Primary is running for incoming widget. + expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.4)); + expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.0)); + + // Container one is at the bottom. + final Container container = tester.firstWidget(find.byType(Container)); + expect(container.key, containerOne); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerThree, color: const Color(0xffff0000)), + transitionBuilder: _transitionBuilder, + ), + ); + await tester.pump(const Duration(milliseconds: 20)); + + _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo, containerThree], tester); + _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo, containerThree], tester); + expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.6)); + expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.6)); + expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.2)); + expect(_primaryAnimation[containerThree], moreOrLessEquals(0.2)); + expect(_secondaryAnimation[containerThree], moreOrLessEquals(0.0)); + await tester.pumpAndSettle(); + }); + + testWidgets('transitions in a new child in reverse.', (WidgetTester tester) async { + final UniqueKey containerOne = UniqueKey(); + final UniqueKey containerTwo = UniqueKey(); + final UniqueKey containerThree = UniqueKey(); + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerOne, color: const Color(0x00000000)), + transitionBuilder: _transitionBuilder, + reverse: true, + ), + ); + + Map _primaryAnimation = _getPrimaryAnimation([containerOne], tester); + Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); + expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerTwo, color: const Color(0xff000000)), + transitionBuilder: _transitionBuilder, + reverse: true, + ), + ); + await tester.pump(const Duration(milliseconds: 40)); + + _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo], tester); + _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); + // Primary is running for outgoing widget. + expect(_primaryAnimation[containerOne], moreOrLessEquals(0.6)); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + // Secondary is running for incoming widget. + expect(_primaryAnimation[containerTwo], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.6)); + + // Container two is at the bottom. + final Container container = tester.firstWidget(find.byType(Container)); + expect(container.key, containerTwo); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerThree, color: const Color(0xffff0000)), + transitionBuilder: _transitionBuilder, + reverse: true, + ), + ); + await tester.pump(const Duration(milliseconds: 20)); + + _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo, containerThree], tester); + _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo, containerThree], tester); + expect(_primaryAnimation[containerOne], moreOrLessEquals(0.4)); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.8)); + expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.4)); + expect(_primaryAnimation[containerThree], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerThree], moreOrLessEquals(0.8)); + await tester.pumpAndSettle(); + }); + + testWidgets('switch from forward to reverse', (WidgetTester tester) async { + final UniqueKey containerOne = UniqueKey(); + final UniqueKey containerTwo = UniqueKey(); + final UniqueKey containerThree = UniqueKey(); + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerOne, color: const Color(0x00000000)), + transitionBuilder: _transitionBuilder, + ), + ); + + Map _primaryAnimation = _getPrimaryAnimation([containerOne], tester); + Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); + expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerTwo, color: const Color(0xff000000)), + transitionBuilder: _transitionBuilder, + ), + ); + await tester.pump(const Duration(milliseconds: 40)); + + _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo], tester); + _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.4)); + expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.0)); + expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.4)); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerThree, color: const Color(0xffff0000)), + transitionBuilder: _transitionBuilder, + reverse: true, + ), + ); + await tester.pump(const Duration(milliseconds: 20)); + + _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo, containerThree], tester); + _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo, containerThree], tester); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.6)); + expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.0)); + expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.2)); + expect(_secondaryAnimation[containerThree], moreOrLessEquals(0.8)); + expect(_primaryAnimation[containerThree], moreOrLessEquals(1.0)); + await tester.pumpAndSettle(); + }); + + testWidgets('switch from reverse to forward.', (WidgetTester tester) async { + final UniqueKey containerOne = UniqueKey(); + final UniqueKey containerTwo = UniqueKey(); + final UniqueKey containerThree = UniqueKey(); + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerOne, color: const Color(0x00000000)), + transitionBuilder: _transitionBuilder, + reverse: true, + ), + ); + + Map _primaryAnimation = _getPrimaryAnimation([containerOne], tester); + Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); + expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerTwo, color: const Color(0xff000000)), + transitionBuilder: _transitionBuilder, + reverse: true, + ), + ); + await tester.pump(const Duration(milliseconds: 40)); + + _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo], tester); + _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); + // Primary is running for outgoing widget. + expect(_primaryAnimation[containerOne], moreOrLessEquals(0.6)); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + // Secondary is running for incoming widget. + expect(_primaryAnimation[containerTwo], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.6)); + + // Container two is at the bottom. + final Container container = tester.firstWidget(find.byType(Container)); + expect(container.key, containerTwo); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerThree, color: const Color(0xffff0000)), + transitionBuilder: _transitionBuilder, + reverse: false, + ), + ); + await tester.pump(const Duration(milliseconds: 20)); + + _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo, containerThree], tester); + _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo, containerThree], tester); + expect(_primaryAnimation[containerOne], moreOrLessEquals(0.4)); + expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + expect(_primaryAnimation[containerTwo], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.8)); + expect(_primaryAnimation[containerThree], moreOrLessEquals(0.2)); + expect(_secondaryAnimation[containerThree], moreOrLessEquals(0.0)); + await tester.pumpAndSettle(); + }); + + testWidgets("doesn't transition in a new child of the same type.", (WidgetTester tester) async { + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(color: const Color(0x00000000)), + transitionBuilder: _transitionBuilder, + ), + ); + + expect(find.byType(FadeTransition), findsOneWidget); + expect(find.byType(ScaleTransition), findsOneWidget); + FadeTransition fade = tester.firstWidget(find.byType(FadeTransition)); + ScaleTransition scale = tester.firstWidget(find.byType(ScaleTransition)); + expect(fade.opacity.value, equals(1.0)); + expect(scale.scale.value, equals(1.0)); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(color: const Color(0xff000000)), + transitionBuilder: _transitionBuilder, + ), + ); + + await tester.pump(const Duration(milliseconds: 50)); + expect(find.byType(FadeTransition), findsOneWidget); + expect(find.byType(ScaleTransition), findsOneWidget); + fade = tester.firstWidget(find.byType(FadeTransition)); + scale = tester.firstWidget(find.byType(ScaleTransition)); + expect(fade.opacity.value, equals(1.0)); + expect(scale.scale.value, equals(1.0)); + await tester.pumpAndSettle(); + }); + + testWidgets('handles null children.', (WidgetTester tester) async { + await tester.pumpWidget( + const PageTransitionSwitcher( + duration: Duration(milliseconds: 100), + child: null, + transitionBuilder: _transitionBuilder, + ), + ); + + expect(find.byType(FadeTransition), findsNothing); + expect(find.byType(ScaleTransition), findsNothing); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(color: const Color(0xff000000)), + transitionBuilder: _transitionBuilder, + ), + ); + + await tester.pump(const Duration(milliseconds: 40)); + expect(find.byType(FadeTransition), findsOneWidget); + expect(find.byType(ScaleTransition), findsOneWidget); + FadeTransition fade = tester.firstWidget(find.byType(FadeTransition)); + ScaleTransition scale = tester.firstWidget(find.byType(ScaleTransition)); + expect(fade.opacity.value, moreOrLessEquals(1.0)); + expect(scale.scale.value, moreOrLessEquals(0.4)); + await tester.pumpAndSettle(); + + await tester.pumpWidget( + const PageTransitionSwitcher( + duration: Duration(milliseconds: 100), + child: null, + transitionBuilder: _transitionBuilder, + ), + ); + + await tester.pump(const Duration(milliseconds: 50)); + expect(find.byType(FadeTransition), findsOneWidget); + expect(find.byType(ScaleTransition), findsOneWidget); + fade = tester.firstWidget(find.byType(FadeTransition)); + scale = tester.firstWidget(find.byType(ScaleTransition)); + expect(fade.opacity.value, moreOrLessEquals(0.5)); + expect(scale.scale.value, moreOrLessEquals(1.0)); + await tester.pumpAndSettle(); + }); + + testWidgets("doesn't start any animations after dispose.", (WidgetTester tester) async { + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: UniqueKey(), color: const Color(0xff000000)), + transitionBuilder: _transitionBuilder, + ), + ); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: UniqueKey(), color: const Color(0xff000000)), + transitionBuilder: _transitionBuilder, + ), + ); + await tester.pump(const Duration(milliseconds: 50)); + + expect(find.byType(FadeTransition), findsNWidgets(2)); + expect(find.byType(ScaleTransition), findsNWidgets(2)); + final FadeTransition fade = tester.firstWidget(find.byType(FadeTransition)); + final ScaleTransition scale = tester.firstWidget(find.byType(ScaleTransition)); + expect(fade.opacity.value, moreOrLessEquals(0.5)); + expect(scale.scale.value, moreOrLessEquals(1.0)); + + // Change the widget tree in the middle of the animation. + await tester.pumpWidget(Container(color: const Color(0xffff0000))); + expect(await tester.pumpAndSettle(const Duration(milliseconds: 100)), equals(1)); + }); + + testWidgets("doesn't reset state of the children in transitions.", (WidgetTester tester) async { + final UniqueKey statefulOne = UniqueKey(); + final UniqueKey statefulTwo = UniqueKey(); + final UniqueKey statefulThree = UniqueKey(); + + StatefulTestState.generation = 0; + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: StatefulTest(key: statefulOne), + transitionBuilder: _transitionBuilder, + ), + ); + + Map _primaryAnimation = _getPrimaryAnimation([statefulOne], tester); + Map _secondaryAnimation = _getSecondaryAnimation([statefulOne], tester); + expect(_primaryAnimation[statefulOne], equals(1.0)); + expect(_secondaryAnimation[statefulOne], equals(0.0)); + expect(StatefulTestState.generation, equals(1)); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: StatefulTest(key: statefulTwo), + transitionBuilder: _transitionBuilder, + ), + ); + + await tester.pump(const Duration(milliseconds: 50)); + expect(find.byType(FadeTransition), findsNWidgets(2)); + _primaryAnimation = _getPrimaryAnimation([statefulOne, statefulTwo], tester); + _secondaryAnimation = _getSecondaryAnimation([statefulOne, statefulTwo], tester); + expect(_primaryAnimation[statefulTwo], equals(0.5)); + expect(_secondaryAnimation[statefulTwo], equals(0.0)); + expect(StatefulTestState.generation, equals(2)); + + await tester.pumpWidget( + PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: StatefulTest(key: statefulThree), + transitionBuilder: _transitionBuilder, + ), + ); + + await tester.pump(const Duration(milliseconds: 10)); + expect(StatefulTestState.generation, equals(3)); + await tester.pumpAndSettle(); + expect(StatefulTestState.generation, equals(3)); + }); + + testWidgets('updates widgets without animating if they are isomorphic.', (WidgetTester tester) async { + Future pumpChild(Widget child) async { + return tester.pumpWidget( + Directionality( + textDirection: TextDirection.rtl, + child: PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: child, + transitionBuilder: _transitionBuilder, + ), + ), + ); + } + + await pumpChild(const Text('1')); + await tester.pump(const Duration(milliseconds: 10)); + FadeTransition fade = tester.widget(find.byType(FadeTransition).first); + ScaleTransition scale = tester.widget(find.byType(ScaleTransition).first); + expect(fade.opacity.value, equals(1.0)); + expect(scale.scale.value, equals(1.0)); + expect(find.text('1'), findsOneWidget); + expect(find.text('2'), findsNothing); + await pumpChild(const Text('2')); + fade = tester.widget(find.byType(FadeTransition).first); + scale = tester.widget(find.byType(ScaleTransition).first); + await tester.pump(const Duration(milliseconds: 20)); + expect(fade.opacity.value, equals(1.0)); + expect(scale.scale.value, equals(1.0)); + expect(find.text('1'), findsNothing); + expect(find.text('2'), findsOneWidget); + }); + + testWidgets('updates previous child transitions if the transitionBuilder changes.', (WidgetTester tester) async { + final UniqueKey containerOne = UniqueKey(); + final UniqueKey containerTwo = UniqueKey(); + final UniqueKey containerThree = UniqueKey(); + + // Insert three unique children so that we have some previous children. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerOne, color: const Color(0xFFFF0000)), + transitionBuilder: _transitionBuilder, + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 10)); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerTwo, color: const Color(0xFF00FF00)), + transitionBuilder: _transitionBuilder, + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 10)); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerThree, color: const Color(0xFF0000FF)), + transitionBuilder: _transitionBuilder, + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 10)); + + expect(find.byType(FadeTransition), findsNWidgets(3)); + expect(find.byType(ScaleTransition), findsNWidgets(3)); + expect(find.byType(SlideTransition), findsNothing); + expect(find.byType(SizeTransition), findsNothing); + + Widget newTransitionBuilder(Widget child, Animation primary, Animation secondary) { + return SlideTransition( + position: Tween(begin: Offset.zero, end: const Offset(20, 30)).animate(primary), + child: SizeTransition( + sizeFactor: Tween(begin: 10, end: 0.0).animate(secondary), + child: child, + ), + ); + } + + // Now set a new transition builder and make sure all the previous + // transitions are replaced. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: PageTransitionSwitcher( + duration: const Duration(milliseconds: 100), + child: Container(key: containerThree, color: const Color(0x00000000)), + transitionBuilder: newTransitionBuilder, + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 10)); + + expect(find.byType(FadeTransition), findsNothing); + expect(find.byType(ScaleTransition), findsNothing); + expect(find.byType(SlideTransition), findsNWidgets(3)); + expect(find.byType(SizeTransition), findsNWidgets(3)); + }); +} + +class StatefulTest extends StatefulWidget { + const StatefulTest({Key key}) : super(key: key); + + @override + StatefulTestState createState() => StatefulTestState(); +} + +class StatefulTestState extends State { + StatefulTestState(); + static int generation = 0; + + @override + void initState() { + super.initState(); + generation++; + } + + @override + Widget build(BuildContext context) => Container(); +} + +Widget _transitionBuilder(Widget child, Animation primary, Animation secondary) { + return ScaleTransition( + scale: Tween(begin: 0.0, end: 1.0).animate(primary), + child: FadeTransition( + opacity: Tween(begin: 1.0, end: 0.0).animate(secondary), + child: child, + ), + ); +} + +Map _getSecondaryAnimation(List keys, WidgetTester tester) { + expect(find.byType(FadeTransition), findsNWidgets(keys.length)); + final Map result = {}; + for (Key key in keys) { + final FadeTransition transition = tester.firstWidget( + find.ancestor( + of: find.byKey(key), + matching: find.byType(FadeTransition), + ), + ); + result[key] = 1.0 - transition.opacity.value; + } + return result; +} + +Map _getPrimaryAnimation(List keys, WidgetTester tester) { + expect(find.byType(ScaleTransition), findsNWidgets(keys.length)); + final Map result = {}; + for (Key key in keys) { + final ScaleTransition transition = tester.firstWidget( + find.ancestor( + of: find.byKey(key), + matching: find.byType(ScaleTransition), + ), + ); + result[key] = transition.scale.value; + } + return result; +} From 782621fe151aca7643dfe033673f95afe3ae95b1 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Mon, 2 Dec 2019 10:28:06 -0800 Subject: [PATCH 3/9] typos --- packages/animations/lib/src/page_transition_switcher.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/animations/lib/src/page_transition_switcher.dart b/packages/animations/lib/src/page_transition_switcher.dart index 4d2e3c74dc09..6e1f46fefb7d 100644 --- a/packages/animations/lib/src/page_transition_switcher.dart +++ b/packages/animations/lib/src/page_transition_switcher.dart @@ -54,7 +54,7 @@ typedef PageTransitionSwitcherTransitionBuilder = Widget Function( /// A widget that transitions from a previously set child to a new child using /// an animation specified by [transitionBuilder]. /// -/// This is a variation of an [AnimatedSwitcher], but it instead of using the +/// This is a variation of an [AnimatedSwitcher], but instead of using the /// same transition for enter and exit, two separate transitions can be /// specified, similar to how the enter and exit transitions of a [PageRoute] /// are defined. @@ -93,7 +93,7 @@ typedef PageTransitionSwitcherTransitionBuilder = Widget Function( /// progress indicator and the image will be fading out while a new progress /// indicator is fading in.) class PageTransitionSwitcher extends StatefulWidget { - /// Creates an [PageTransitionSwitcher]. + /// Creates a [PageTransitionSwitcher]. /// /// The [duration], [reverse], and [transitionBuilder] parameters /// must not be null. @@ -130,12 +130,12 @@ class PageTransitionSwitcher extends StatefulWidget { /// Indicates the direction of the animation. /// - /// When this is false, the new child will transitioning in on top of the + /// When this is false, the new child will transition in on top of the /// previous child while its primary animation and the secondary animation /// of the previous child are running forward. This is similar to the /// transition associated with pushing a new [PageRoute] on top of another. /// - /// When this is true, the new child will transitioning in below the + /// When this is true, the new child will transition in below the /// previous child while its secondary animation and the primary animation /// of the previous child are running in reverse. This is similar to the /// transition associated with popping a [PageRoute] to reveal a new From d62a35b29cd4079a34be43eeae8b9934cbaa12ad Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Mon, 2 Dec 2019 10:50:04 -0800 Subject: [PATCH 4/9] format --- .../lib/src/page_transition_switcher.dart | 25 +++- .../test/page_transition_switcher_test.dart | 120 ++++++++++++------ 2 files changed, 99 insertions(+), 46 deletions(-) diff --git a/packages/animations/lib/src/page_transition_switcher.dart b/packages/animations/lib/src/page_transition_switcher.dart index 6e1f46fefb7d..14ee055c13a8 100644 --- a/packages/animations/lib/src/page_transition_switcher.dart +++ b/packages/animations/lib/src/page_transition_switcher.dart @@ -156,7 +156,8 @@ class PageTransitionSwitcher extends StatefulWidget { _PageTransitionSwitcherState createState() => _PageTransitionSwitcherState(); } -class _PageTransitionSwitcherState extends State with TickerProviderStateMixin { +class _PageTransitionSwitcherState extends State + with TickerProviderStateMixin { final List<_ChildEntry> _activeEntries = <_ChildEntry>[]; _ChildEntry _currentEntry; int _childNumber = 0; @@ -179,7 +180,9 @@ class _PageTransitionSwitcherState extends State with Ti final bool hasNewChild = widget.child != null; final bool hasOldChild = _currentEntry != null; - if (hasNewChild != hasOldChild || hasNewChild && !Widget.canUpdate(widget.child, _currentEntry.widgetChild)) { + if (hasNewChild != hasOldChild || + hasNewChild && + !Widget.canUpdate(widget.child, _currentEntry.widgetChild)) { // Child has changed, fade current entry out and add new entry. _childNumber += 1; _addEntryForNewChild(animate: true); @@ -206,8 +209,9 @@ class _PageTransitionSwitcherState extends State with Ti } _currentEntry = null; } - if (widget.child == null) + if (widget.child == null) { return; + } final AnimationController primaryController = AnimationController( duration: widget.duration, vsync: this, @@ -251,7 +255,10 @@ class _PageTransitionSwitcherState extends State with Ti }) { final _ChildEntry entry = _ChildEntry( widgetChild: child, - transition: KeyedSubtree.wrap(builder(child, primaryController, secondaryController), _childNumber), + transition: KeyedSubtree.wrap( + builder(child, primaryController, secondaryController), + _childNumber, + ), primaryController: primaryController, secondaryController: secondaryController, ); @@ -281,7 +288,11 @@ class _PageTransitionSwitcherState extends State with Ti void _updateTransitionForEntry(_ChildEntry entry) { entry.transition = KeyedSubtree( key: entry.transition.key, - child: widget.transitionBuilder(entry.widgetChild, entry.primaryController, entry.secondaryController), + child: widget.transitionBuilder( + entry.widgetChild, + entry.primaryController, + entry.secondaryController, + ), ); } @@ -296,7 +307,9 @@ class _PageTransitionSwitcherState extends State with Ti @override Widget build(BuildContext context) { return Stack( - children: _activeEntries.map((_ChildEntry entry) => entry.transition).toList(), + children: _activeEntries + .map((_ChildEntry entry) => entry.transition) + .toList(), alignment: Alignment.center, ); } diff --git a/packages/animations/test/page_transition_switcher_test.dart b/packages/animations/test/page_transition_switcher_test.dart index 673f39b26679..3e56022df76a 100644 --- a/packages/animations/test/page_transition_switcher_test.dart +++ b/packages/animations/test/page_transition_switcher_test.dart @@ -18,9 +18,11 @@ void main() { transitionBuilder: _transitionBuilder, ), ); - - Map _primaryAnimation = _getPrimaryAnimation([containerOne], tester); - Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); + + Map _primaryAnimation = + _getPrimaryAnimation([containerOne], tester); + Map _secondaryAnimation = + _getSecondaryAnimation([containerOne], tester); expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); @@ -33,8 +35,10 @@ void main() { ); await tester.pump(const Duration(milliseconds: 40)); - _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo], tester); - _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); + _primaryAnimation = + _getPrimaryAnimation([containerOne, containerTwo], tester); + _secondaryAnimation = + _getSecondaryAnimation([containerOne, containerTwo], tester); // Secondary is running for outgoing widget. expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.4)); @@ -55,8 +59,10 @@ void main() { ); await tester.pump(const Duration(milliseconds: 20)); - _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo, containerThree], tester); - _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo, containerThree], tester); + _primaryAnimation = _getPrimaryAnimation( + [containerOne, containerTwo, containerThree], tester); + _secondaryAnimation = _getSecondaryAnimation( + [containerOne, containerTwo, containerThree], tester); expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.6)); expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.6)); @@ -66,7 +72,8 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('transitions in a new child in reverse.', (WidgetTester tester) async { + testWidgets('transitions in a new child in reverse.', + (WidgetTester tester) async { final UniqueKey containerOne = UniqueKey(); final UniqueKey containerTwo = UniqueKey(); final UniqueKey containerThree = UniqueKey(); @@ -79,8 +86,10 @@ void main() { ), ); - Map _primaryAnimation = _getPrimaryAnimation([containerOne], tester); - Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); + Map _primaryAnimation = + _getPrimaryAnimation([containerOne], tester); + Map _secondaryAnimation = + _getSecondaryAnimation([containerOne], tester); expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); @@ -94,8 +103,10 @@ void main() { ); await tester.pump(const Duration(milliseconds: 40)); - _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo], tester); - _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); + _primaryAnimation = + _getPrimaryAnimation([containerOne, containerTwo], tester); + _secondaryAnimation = + _getSecondaryAnimation([containerOne, containerTwo], tester); // Primary is running for outgoing widget. expect(_primaryAnimation[containerOne], moreOrLessEquals(0.6)); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); @@ -117,8 +128,10 @@ void main() { ); await tester.pump(const Duration(milliseconds: 20)); - _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo, containerThree], tester); - _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo, containerThree], tester); + _primaryAnimation = _getPrimaryAnimation( + [containerOne, containerTwo, containerThree], tester); + _secondaryAnimation = _getSecondaryAnimation( + [containerOne, containerTwo, containerThree], tester); expect(_primaryAnimation[containerOne], moreOrLessEquals(0.4)); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.8)); @@ -140,8 +153,10 @@ void main() { ), ); - Map _primaryAnimation = _getPrimaryAnimation([containerOne], tester); - Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); + Map _primaryAnimation = + _getPrimaryAnimation([containerOne], tester); + Map _secondaryAnimation = + _getSecondaryAnimation([containerOne], tester); expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); @@ -154,8 +169,10 @@ void main() { ); await tester.pump(const Duration(milliseconds: 40)); - _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo], tester); - _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); + _primaryAnimation = + _getPrimaryAnimation([containerOne, containerTwo], tester); + _secondaryAnimation = + _getSecondaryAnimation([containerOne, containerTwo], tester); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.4)); expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.0)); @@ -171,8 +188,10 @@ void main() { ); await tester.pump(const Duration(milliseconds: 20)); - _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo, containerThree], tester); - _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo, containerThree], tester); + _primaryAnimation = _getPrimaryAnimation( + [containerOne, containerTwo, containerThree], tester); + _secondaryAnimation = _getSecondaryAnimation( + [containerOne, containerTwo, containerThree], tester); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.6)); expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.0)); @@ -195,8 +214,10 @@ void main() { ), ); - Map _primaryAnimation = _getPrimaryAnimation([containerOne], tester); - Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); + Map _primaryAnimation = + _getPrimaryAnimation([containerOne], tester); + Map _secondaryAnimation = + _getSecondaryAnimation([containerOne], tester); expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); @@ -210,8 +231,10 @@ void main() { ); await tester.pump(const Duration(milliseconds: 40)); - _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo], tester); - _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); + _primaryAnimation = + _getPrimaryAnimation([containerOne, containerTwo], tester); + _secondaryAnimation = + _getSecondaryAnimation([containerOne, containerTwo], tester); // Primary is running for outgoing widget. expect(_primaryAnimation[containerOne], moreOrLessEquals(0.6)); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); @@ -233,8 +256,10 @@ void main() { ); await tester.pump(const Duration(milliseconds: 20)); - _primaryAnimation = _getPrimaryAnimation([containerOne, containerTwo, containerThree], tester); - _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo, containerThree], tester); + _primaryAnimation = _getPrimaryAnimation( + [containerOne, containerTwo, containerThree], tester); + _secondaryAnimation = _getSecondaryAnimation( + [containerOne, containerTwo, containerThree], tester); expect(_primaryAnimation[containerOne], moreOrLessEquals(0.4)); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); expect(_primaryAnimation[containerTwo], moreOrLessEquals(1.0)); @@ -244,7 +269,8 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets("doesn't transition in a new child of the same type.", (WidgetTester tester) async { + testWidgets("doesn't transition in a new child of the same type.", + (WidgetTester tester) async { await tester.pumpWidget( PageTransitionSwitcher( duration: const Duration(milliseconds: 100), @@ -325,7 +351,8 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets("doesn't start any animations after dispose.", (WidgetTester tester) async { + testWidgets("doesn't start any animations after dispose.", + (WidgetTester tester) async { await tester.pumpWidget( PageTransitionSwitcher( duration: const Duration(milliseconds: 100), @@ -346,16 +373,19 @@ void main() { expect(find.byType(FadeTransition), findsNWidgets(2)); expect(find.byType(ScaleTransition), findsNWidgets(2)); final FadeTransition fade = tester.firstWidget(find.byType(FadeTransition)); - final ScaleTransition scale = tester.firstWidget(find.byType(ScaleTransition)); + final ScaleTransition scale = + tester.firstWidget(find.byType(ScaleTransition)); expect(fade.opacity.value, moreOrLessEquals(0.5)); expect(scale.scale.value, moreOrLessEquals(1.0)); // Change the widget tree in the middle of the animation. await tester.pumpWidget(Container(color: const Color(0xffff0000))); - expect(await tester.pumpAndSettle(const Duration(milliseconds: 100)), equals(1)); + expect(await tester.pumpAndSettle(const Duration(milliseconds: 100)), + equals(1)); }); - testWidgets("doesn't reset state of the children in transitions.", (WidgetTester tester) async { + testWidgets("doesn't reset state of the children in transitions.", + (WidgetTester tester) async { final UniqueKey statefulOne = UniqueKey(); final UniqueKey statefulTwo = UniqueKey(); final UniqueKey statefulThree = UniqueKey(); @@ -370,8 +400,10 @@ void main() { ), ); - Map _primaryAnimation = _getPrimaryAnimation([statefulOne], tester); - Map _secondaryAnimation = _getSecondaryAnimation([statefulOne], tester); + Map _primaryAnimation = + _getPrimaryAnimation([statefulOne], tester); + Map _secondaryAnimation = + _getSecondaryAnimation([statefulOne], tester); expect(_primaryAnimation[statefulOne], equals(1.0)); expect(_secondaryAnimation[statefulOne], equals(0.0)); expect(StatefulTestState.generation, equals(1)); @@ -386,8 +418,10 @@ void main() { await tester.pump(const Duration(milliseconds: 50)); expect(find.byType(FadeTransition), findsNWidgets(2)); - _primaryAnimation = _getPrimaryAnimation([statefulOne, statefulTwo], tester); - _secondaryAnimation = _getSecondaryAnimation([statefulOne, statefulTwo], tester); + _primaryAnimation = + _getPrimaryAnimation([statefulOne, statefulTwo], tester); + _secondaryAnimation = + _getSecondaryAnimation([statefulOne, statefulTwo], tester); expect(_primaryAnimation[statefulTwo], equals(0.5)); expect(_secondaryAnimation[statefulTwo], equals(0.0)); expect(StatefulTestState.generation, equals(2)); @@ -406,7 +440,8 @@ void main() { expect(StatefulTestState.generation, equals(3)); }); - testWidgets('updates widgets without animating if they are isomorphic.', (WidgetTester tester) async { + testWidgets('updates widgets without animating if they are isomorphic.', + (WidgetTester tester) async { Future pumpChild(Widget child) async { return tester.pumpWidget( Directionality( @@ -438,7 +473,9 @@ void main() { expect(find.text('2'), findsOneWidget); }); - testWidgets('updates previous child transitions if the transitionBuilder changes.', (WidgetTester tester) async { + testWidgets( + 'updates previous child transitions if the transitionBuilder changes.', + (WidgetTester tester) async { final UniqueKey containerOne = UniqueKey(); final UniqueKey containerTwo = UniqueKey(); final UniqueKey containerThree = UniqueKey(); @@ -488,9 +525,11 @@ void main() { expect(find.byType(SlideTransition), findsNothing); expect(find.byType(SizeTransition), findsNothing); - Widget newTransitionBuilder(Widget child, Animation primary, Animation secondary) { + Widget newTransitionBuilder( + Widget child, Animation primary, Animation secondary) { return SlideTransition( - position: Tween(begin: Offset.zero, end: const Offset(20, 30)).animate(primary), + position: Tween(begin: Offset.zero, end: const Offset(20, 30)) + .animate(primary), child: SizeTransition( sizeFactor: Tween(begin: 10, end: 0.0).animate(secondary), child: child, @@ -541,7 +580,8 @@ class StatefulTestState extends State { Widget build(BuildContext context) => Container(); } -Widget _transitionBuilder(Widget child, Animation primary, Animation secondary) { +Widget _transitionBuilder( + Widget child, Animation primary, Animation secondary) { return ScaleTransition( scale: Tween(begin: 0.0, end: 1.0).animate(primary), child: FadeTransition( From b0c5f576854ecdde99713d8f2712128310afebbc Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 3 Dec 2019 18:11:15 -0800 Subject: [PATCH 5/9] review --- .../lib/src/page_transition_switcher.dart | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/animations/lib/src/page_transition_switcher.dart b/packages/animations/lib/src/page_transition_switcher.dart index 14ee055c13a8..781547eea41a 100644 --- a/packages/animations/lib/src/page_transition_switcher.dart +++ b/packages/animations/lib/src/page_transition_switcher.dart @@ -51,8 +51,8 @@ typedef PageTransitionSwitcherTransitionBuilder = Widget Function( Animation secondaryAnimation, ); -/// A widget that transitions from a previously set child to a new child using -/// an animation specified by [transitionBuilder]. +/// A widget that transitions from a previously set child to a newly set child +/// using an animation specified by [transitionBuilder]. /// /// This is a variation of an [AnimatedSwitcher], but instead of using the /// same transition for enter and exit, two separate transitions can be @@ -78,8 +78,8 @@ typedef PageTransitionSwitcherTransitionBuilder = Widget Function( /// more than one previous child can exist and be transitioning out while the /// newest one is transitioning in. /// -/// If the "new" child is the same widget type and key as the "old" child, but -/// with different parameters, then [PageTransitionSwitcher] will *not* do a +/// If the *new* child is the same widget type and key as the *previous* child, +/// but with different parameters, then [PageTransitionSwitcher] will *not* do a /// transition between them, since as far as the framework is concerned, they /// are the same widget and the existing widget can be updated with the new /// parameters. To force the transition to occur, set a [Key] on each child @@ -128,22 +128,23 @@ class PageTransitionSwitcher extends StatefulWidget { /// durations of transitions already in progress. final Duration duration; - /// Indicates the direction of the animation. + /// Indicates the direction of the animation when a new [child] is set. /// /// When this is false, the new child will transition in on top of the - /// previous child while its primary animation and the secondary animation - /// of the previous child are running forward. This is similar to the - /// transition associated with pushing a new [PageRoute] on top of another. + /// previously set child while its primary animation and the secondary + /// animation of the previous child are running forward. This is similar to + /// the transition associated with pushing a new [PageRoute] on top of + /// another. /// /// When this is true, the new child will transition in below the - /// previous child while its secondary animation and the primary animation - /// of the previous child are running in reverse. This is similar to the - /// transition associated with popping a [PageRoute] to reveal a new + /// previously set child while its secondary animation and the primary + /// animation of the previous child are running in reverse. This is similar to + /// the transition associated with popping a [PageRoute] to reveal a new /// [PageRoute] below it. final bool reverse; /// A function that wraps a new [child] with a primary and secondary animation - /// to transition between the previously set child and the current child. + /// to transition between the previously set child and the new child. /// /// This is only called when a new [child] is set (not for each build), or /// when a new [transitionBuilder] is set. If a new [transitionBuilder] is From c7bd29bc4e2d60ebb703201cd42cc4e4b704438f Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 4 Dec 2019 15:03:37 -0800 Subject: [PATCH 6/9] review --- .../lib/src/page_transition_switcher.dart | 112 +++++++++----- .../test/page_transition_switcher_test.dart | 138 +++++++++--------- 2 files changed, 147 insertions(+), 103 deletions(-) diff --git a/packages/animations/lib/src/page_transition_switcher.dart b/packages/animations/lib/src/page_transition_switcher.dart index 781547eea41a..aa03b0b2a6bf 100644 --- a/packages/animations/lib/src/page_transition_switcher.dart +++ b/packages/animations/lib/src/page_transition_switcher.dart @@ -38,41 +38,57 @@ class _ChildEntry { } @override - String toString() => 'Entry#${shortHash(this)}($widgetChild)'; + String toString() { + return 'PageTransitionSwitcherEntry#${shortHash(this)}($widgetChild)'; + } } /// Signature for builders used to generate custom transitions for /// [PageTransitionSwitcher]. /// /// The function should return a widget which wraps the given `child`. +/// +/// When a [PageTransitionSwitcher]'s `child` is replaced, the new child's +/// `primaryAnimation` runs forward and the value of its `secondaryAnimation` is +/// usually fixed at 0.0. At the same time, the old child's `secondaryAnimation` +/// runs forward, and the value of its primaryAnimation is usually fixed at 1.0. +/// +/// The widget returned by the [PageTransitionSwitcherTransitionBuilder] can +/// incorporate both animations. It will use the primary animation to define how +/// its child appears, and the secondary animation to define how its child +/// disappears. typedef PageTransitionSwitcherTransitionBuilder = Widget Function( Widget child, Animation primaryAnimation, Animation secondaryAnimation, ); -/// A widget that transitions from a previously set child to a newly set child -/// using an animation specified by [transitionBuilder]. +/// A widget that transitions from an old child to a new child whenever [child] +/// changes using an animation specified by [transitionBuilder]. /// /// This is a variation of an [AnimatedSwitcher], but instead of using the /// same transition for enter and exit, two separate transitions can be /// specified, similar to how the enter and exit transitions of a [PageRoute] /// are defined. /// -/// The transitions returned by the [transitionBuilder] are driven by two -/// animations: a primary one and a secondary one. When a new child is -/// transitioning in while [reverse] is false, the primary animation of the -/// transition associated with that new child is running forward. At the same -/// time, the secondary animation of the previous child is playing forward to -/// transition that child out. In other words, the primary animation defines -/// how a child enters, and the secondary animation determines how it leaves. -/// This is similar to the transition associated with pushing a new [PageRoute] -/// on top of another. +/// When a new [child] is specified, the [transitionBuilder] is effectively +/// applied twice, once to the old child and once to the new one. When +/// [reverse] is false, the old child's `secondaryAnimation` runs forward, and +/// the value of its `primaryAnimation` is usually fixed at 1.0. The new child's +/// `primaryAnimation` runs forward and the value of its `secondaryAnimation` is +/// usually fixed at 0.0. The widget returned by the [transitionBuilder] can +/// incorporate both animations. It will use the primary animation to define how +/// its child appears, and the secondary animation to define how its child +/// disappears. This is similar to the transition associated with pushing a new +/// [PageRoute] on top of another. /// -/// When [reverse] is true, then the primary animation of the previous child -/// is playing in reverse to reveal the new child underneath, whose secondary -/// animation is also playing in reverse. This is similar to popping a -/// [PageRoute] to reveal a new [PageRoute] underneath it. +/// When [reverse] is true, the old child's `primaryAnimation` runs in reverse +/// and the value of its `secondaryAnimation` is usually fixed at 0.0. The new +/// child's `secondaryAnimation` runs in reverse and the value of its +/// `primaryAnimation` is usually fixed at 1.0. This is similar to popping a +/// [PageRoute] to reveal another [PageRoute] underneath it. +/// +/// This process is the same as the one used by [PageRoute.buildTransitions]. /// /// If the children are swapped fast enough (i.e. before [duration] elapses), /// more than one previous child can exist and be transitioning out while the @@ -84,14 +100,19 @@ typedef PageTransitionSwitcherTransitionBuilder = Widget Function( /// are the same widget and the existing widget can be updated with the new /// parameters. To force the transition to occur, set a [Key] on each child /// widget that you wish to be considered unique (typically a [ValueKey] on the -/// widget data that distinguishes this child from the others). +/// widget data that distinguishes this child from the others). For example, +/// changing the child from `SizedBox(width: 10)` to `SizedBox(width: 100)` +/// would not trigger a transition but changing the child from +/// `SizedBox(width: 10)` to `SizedBox(key: Key('foo'), width: 100)` would. +/// Similarly, changing the child to `Container(width: 10)` would trigger a +/// transition. /// /// The same key can be used for a new child as was used for an already-outgoing -/// child; the two will not be considered related. (For example, if a progress +/// child; the two will not be considered related. For example, if a progress /// indicator with key A is first shown, then an image with key B, then another /// 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.) +/// indicator is fading in. class PageTransitionSwitcher extends StatefulWidget { /// Creates a [PageTransitionSwitcher]. /// @@ -110,7 +131,7 @@ class PageTransitionSwitcher extends StatefulWidget { /// The current child widget to display. /// - /// If there was a previous child, it will be transitioning out using the + /// If there was a previous child, it will be transitioned out using the /// secondary animation of the [transitionBuilder], while the new child /// transitions in using the primary animation of the [transitionBuilder]. /// @@ -124,11 +145,12 @@ class PageTransitionSwitcher extends StatefulWidget { /// The duration of the transition from the old [child] value to the new one. /// /// This duration is applied to the given [child] when that property is set to - /// a new child. Changing [duration] will not affect the - /// durations of transitions already in progress. + /// a new child. Changing [duration] will not affect the durations of + /// transitions already in progress. final Duration duration; - /// Indicates the direction of the animation when a new [child] is set. + /// Indicates whether the new [child] will visually appear on top or + /// underneath the old child. /// /// When this is false, the new child will transition in on top of the /// previously set child while its primary animation and the secondary @@ -137,20 +159,22 @@ class PageTransitionSwitcher extends StatefulWidget { /// another. /// /// When this is true, the new child will transition in below the - /// previously set child while its secondary animation and the primary + /// previous child while its secondary animation and the primary /// animation of the previous child are running in reverse. This is similar to /// the transition associated with popping a [PageRoute] to reveal a new /// [PageRoute] below it. final bool reverse; /// A function that wraps a new [child] with a primary and secondary animation - /// to transition between the previously set child and the new child. + /// set define how the child appears and disappears. /// /// This is only called when a new [child] is set (not for each build), or /// when a new [transitionBuilder] is set. If a new [transitionBuilder] is /// set, then the transition is rebuilt for the current child and all previous /// children using the new [transitionBuilder]. The function must not return /// null. + /// + /// The child provided to the transitionBuilder may be null. final PageTransitionSwitcherTransitionBuilder transitionBuilder; @override @@ -166,7 +190,7 @@ class _PageTransitionSwitcherState extends State @override void initState() { super.initState(); - _addEntryForNewChild(animate: false); + _addEntryForNewChild(shouldAnimate: false); } @override @@ -186,7 +210,7 @@ class _PageTransitionSwitcherState extends State !Widget.canUpdate(widget.child, _currentEntry.widgetChild)) { // Child has changed, fade current entry out and add new entry. _childNumber += 1; - _addEntryForNewChild(animate: true); + _addEntryForNewChild(shouldAnimate: true); } else if (_currentEntry != null) { assert(hasOldChild && hasNewChild); assert(Widget.canUpdate(widget.child, _currentEntry.widgetChild)); @@ -199,10 +223,10 @@ class _PageTransitionSwitcherState extends State } } - void _addEntryForNewChild({@required bool animate}) { - assert(animate || _currentEntry == null); + void _addEntryForNewChild({@required bool shouldAnimate}) { + assert(shouldAnimate || _currentEntry == null); if (_currentEntry != null) { - assert(animate); + assert(shouldAnimate); if (widget.reverse) { _currentEntry.primaryController.reverse(); } else { @@ -221,7 +245,7 @@ class _PageTransitionSwitcherState extends State duration: widget.duration, vsync: this, ); - if (animate) { + if (shouldAnimate) { if (widget.reverse) { primaryController.value = 1.0; secondaryController.value = 1.0; @@ -254,10 +278,19 @@ class _PageTransitionSwitcherState extends State @required AnimationController primaryController, @required AnimationController secondaryController, }) { + final Widget transition = builder( + child, + primaryController, + secondaryController, + ); + assert( + transition != null, + 'PageTransitionSwitcher.builder must not return null.', + ); final _ChildEntry entry = _ChildEntry( widgetChild: child, transition: KeyedSubtree.wrap( - builder(child, primaryController, secondaryController), + transition, _childNumber, ), primaryController: primaryController, @@ -287,13 +320,18 @@ class _PageTransitionSwitcherState extends State } void _updateTransitionForEntry(_ChildEntry entry) { + final Widget transition = widget.transitionBuilder( + entry.widgetChild, + entry.primaryController, + entry.secondaryController, + ); + assert( + transition != null, + 'PageTransitionSwitcher.builder must not return null.', + ); entry.transition = KeyedSubtree( key: entry.transition.key, - child: widget.transitionBuilder( - entry.widgetChild, - entry.primaryController, - entry.secondaryController, - ), + child: transition, ); } diff --git a/packages/animations/test/page_transition_switcher_test.dart b/packages/animations/test/page_transition_switcher_test.dart index 3e56022df76a..27659c01cc93 100644 --- a/packages/animations/test/page_transition_switcher_test.dart +++ b/packages/animations/test/page_transition_switcher_test.dart @@ -23,8 +23,8 @@ void main() { _getPrimaryAnimation([containerOne], tester); Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); - expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); - expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + expect(_primaryAnimation[containerOne], equals(1.0)); + expect(_secondaryAnimation[containerOne], equals(0.0)); await tester.pumpWidget( PageTransitionSwitcher( @@ -40,13 +40,13 @@ void main() { _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); // Secondary is running for outgoing widget. - expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); + expect(_primaryAnimation[containerOne], equals(1.0)); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.4)); // Primary is running for incoming widget. expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.4)); - expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.0)); + expect(_secondaryAnimation[containerTwo], equals(0.0)); - // Container one is at the bottom. + // Container one is underneath container two final Container container = tester.firstWidget(find.byType(Container)); expect(container.key, containerOne); @@ -63,12 +63,12 @@ void main() { [containerOne, containerTwo, containerThree], tester); _secondaryAnimation = _getSecondaryAnimation( [containerOne, containerTwo, containerThree], tester); - expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); - expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.6)); - expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.6)); + expect(_primaryAnimation[containerOne], equals(1.0)); + expect(_secondaryAnimation[containerOne], equals(0.6)); + expect(_primaryAnimation[containerTwo], equals(0.6)); expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.2)); expect(_primaryAnimation[containerThree], moreOrLessEquals(0.2)); - expect(_secondaryAnimation[containerThree], moreOrLessEquals(0.0)); + expect(_secondaryAnimation[containerThree], equals(0.0)); await tester.pumpAndSettle(); }); @@ -90,8 +90,8 @@ void main() { _getPrimaryAnimation([containerOne], tester); Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); - expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); - expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + expect(_primaryAnimation[containerOne], equals(1.0)); + expect(_secondaryAnimation[containerOne], equals(0.0)); await tester.pumpWidget( PageTransitionSwitcher( @@ -107,14 +107,14 @@ void main() { _getPrimaryAnimation([containerOne, containerTwo], tester); _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); - // Primary is running for outgoing widget. + // Primary is running forward for outgoing widget. expect(_primaryAnimation[containerOne], moreOrLessEquals(0.6)); - expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); - // Secondary is running for incoming widget. - expect(_primaryAnimation[containerTwo], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerOne], equals(0.0)); + // Secondary is running forward for incoming widget. + expect(_primaryAnimation[containerTwo], equals(1.0)); expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.6)); - // Container two is at the bottom. + // Container two two is underneath container one. final Container container = tester.firstWidget(find.byType(Container)); expect(container.key, containerTwo); @@ -132,12 +132,12 @@ void main() { [containerOne, containerTwo, containerThree], tester); _secondaryAnimation = _getSecondaryAnimation( [containerOne, containerTwo, containerThree], tester); - expect(_primaryAnimation[containerOne], moreOrLessEquals(0.4)); - expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); - expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.8)); - expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.4)); - expect(_primaryAnimation[containerThree], moreOrLessEquals(1.0)); - expect(_secondaryAnimation[containerThree], moreOrLessEquals(0.8)); + expect(_primaryAnimation[containerOne], equals(0.4)); + expect(_secondaryAnimation[containerOne], equals(0.0)); + expect(_primaryAnimation[containerTwo], equals(0.8)); + expect(_secondaryAnimation[containerTwo], equals(0.4)); + expect(_primaryAnimation[containerThree], equals(1.0)); + expect(_secondaryAnimation[containerThree], equals(0.8)); await tester.pumpAndSettle(); }); @@ -157,8 +157,8 @@ void main() { _getPrimaryAnimation([containerOne], tester); Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); - expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); - expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + expect(_primaryAnimation[containerOne], equals(1.0)); + expect(_secondaryAnimation[containerOne], equals(0.0)); await tester.pumpWidget( PageTransitionSwitcher( @@ -174,8 +174,8 @@ void main() { _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.4)); - expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); - expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.0)); + expect(_primaryAnimation[containerOne], equals(1.0)); + expect(_secondaryAnimation[containerTwo], equals(0.0)); expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.4)); await tester.pumpWidget( @@ -192,12 +192,12 @@ void main() { [containerOne, containerTwo, containerThree], tester); _secondaryAnimation = _getSecondaryAnimation( [containerOne, containerTwo, containerThree], tester); - expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.6)); - expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); - expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.0)); + expect(_secondaryAnimation[containerOne], equals(0.6)); + expect(_primaryAnimation[containerOne], equals(1.0)); + expect(_secondaryAnimation[containerTwo], equals(0.0)); expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.2)); - expect(_secondaryAnimation[containerThree], moreOrLessEquals(0.8)); - expect(_primaryAnimation[containerThree], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerThree], equals(0.8)); + expect(_primaryAnimation[containerThree], equals(1.0)); await tester.pumpAndSettle(); }); @@ -218,8 +218,8 @@ void main() { _getPrimaryAnimation([containerOne], tester); Map _secondaryAnimation = _getSecondaryAnimation([containerOne], tester); - expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0)); - expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); + expect(_primaryAnimation[containerOne], equals(1.0)); + expect(_secondaryAnimation[containerOne], equals(0.0)); await tester.pumpWidget( PageTransitionSwitcher( @@ -235,14 +235,14 @@ void main() { _getPrimaryAnimation([containerOne, containerTwo], tester); _secondaryAnimation = _getSecondaryAnimation([containerOne, containerTwo], tester); - // Primary is running for outgoing widget. + // Primary is running in reverse for outgoing widget. expect(_primaryAnimation[containerOne], moreOrLessEquals(0.6)); - expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); - // Secondary is running for incoming widget. - expect(_primaryAnimation[containerTwo], moreOrLessEquals(1.0)); + expect(_secondaryAnimation[containerOne], equals(0.0)); + // Secondary is running in reverse for incoming widget. + expect(_primaryAnimation[containerTwo], equals(1.0)); expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.6)); - // Container two is at the bottom. + // Container two is underneath container one. final Container container = tester.firstWidget(find.byType(Container)); expect(container.key, containerTwo); @@ -256,16 +256,22 @@ void main() { ); await tester.pump(const Duration(milliseconds: 20)); + // Container one is expected to continue running its primary animation in + // reverse since it is exiting. Container two's secondary animation switches + // from running its secondary animation in reverse to running forwards since + // it should now be exiting underneath container three. Container three's + // primary animation should be running forwards since it is entering above + // container two. _primaryAnimation = _getPrimaryAnimation( [containerOne, containerTwo, containerThree], tester); _secondaryAnimation = _getSecondaryAnimation( [containerOne, containerTwo, containerThree], tester); - expect(_primaryAnimation[containerOne], moreOrLessEquals(0.4)); - expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.0)); - expect(_primaryAnimation[containerTwo], moreOrLessEquals(1.0)); - expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.8)); + expect(_primaryAnimation[containerOne], equals(0.4)); + expect(_secondaryAnimation[containerOne], equals(0.0)); + expect(_primaryAnimation[containerTwo], equals(1.0)); + expect(_secondaryAnimation[containerTwo], equals(0.8)); expect(_primaryAnimation[containerThree], moreOrLessEquals(0.2)); - expect(_secondaryAnimation[containerThree], moreOrLessEquals(0.0)); + expect(_secondaryAnimation[containerThree], equals(0.0)); await tester.pumpAndSettle(); }); @@ -329,9 +335,9 @@ void main() { expect(find.byType(ScaleTransition), findsOneWidget); FadeTransition fade = tester.firstWidget(find.byType(FadeTransition)); ScaleTransition scale = tester.firstWidget(find.byType(ScaleTransition)); - expect(fade.opacity.value, moreOrLessEquals(1.0)); + expect(fade.opacity.value, equals(1.0)); expect(scale.scale.value, moreOrLessEquals(0.4)); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); // finish transitions. await tester.pumpWidget( const PageTransitionSwitcher( @@ -346,8 +352,8 @@ void main() { expect(find.byType(ScaleTransition), findsOneWidget); fade = tester.firstWidget(find.byType(FadeTransition)); scale = tester.firstWidget(find.byType(ScaleTransition)); - expect(fade.opacity.value, moreOrLessEquals(0.5)); - expect(scale.scale.value, moreOrLessEquals(1.0)); + expect(fade.opacity.value, equals(0.5)); + expect(scale.scale.value, equals(1.0)); await tester.pumpAndSettle(); }); @@ -375,8 +381,8 @@ void main() { final FadeTransition fade = tester.firstWidget(find.byType(FadeTransition)); final ScaleTransition scale = tester.firstWidget(find.byType(ScaleTransition)); - expect(fade.opacity.value, moreOrLessEquals(0.5)); - expect(scale.scale.value, moreOrLessEquals(1.0)); + expect(fade.opacity.value, equals(0.5)); + expect(scale.scale.value, equals(1.0)); // Change the widget tree in the middle of the animation. await tester.pumpWidget(Container(color: const Color(0xffff0000))); @@ -390,12 +396,12 @@ void main() { final UniqueKey statefulTwo = UniqueKey(); final UniqueKey statefulThree = UniqueKey(); - StatefulTestState.generation = 0; + StatefulTestWidgetState.generation = 0; await tester.pumpWidget( PageTransitionSwitcher( duration: const Duration(milliseconds: 100), - child: StatefulTest(key: statefulOne), + child: StatefulTestWidget(key: statefulOne), transitionBuilder: _transitionBuilder, ), ); @@ -406,12 +412,12 @@ void main() { _getSecondaryAnimation([statefulOne], tester); expect(_primaryAnimation[statefulOne], equals(1.0)); expect(_secondaryAnimation[statefulOne], equals(0.0)); - expect(StatefulTestState.generation, equals(1)); + expect(StatefulTestWidgetState.generation, equals(1)); await tester.pumpWidget( PageTransitionSwitcher( duration: const Duration(milliseconds: 100), - child: StatefulTest(key: statefulTwo), + child: StatefulTestWidget(key: statefulTwo), transitionBuilder: _transitionBuilder, ), ); @@ -424,20 +430,20 @@ void main() { _getSecondaryAnimation([statefulOne, statefulTwo], tester); expect(_primaryAnimation[statefulTwo], equals(0.5)); expect(_secondaryAnimation[statefulTwo], equals(0.0)); - expect(StatefulTestState.generation, equals(2)); + expect(StatefulTestWidgetState.generation, equals(2)); await tester.pumpWidget( PageTransitionSwitcher( duration: const Duration(milliseconds: 100), - child: StatefulTest(key: statefulThree), + child: StatefulTestWidget(key: statefulThree), transitionBuilder: _transitionBuilder, ), ); await tester.pump(const Duration(milliseconds: 10)); - expect(StatefulTestState.generation, equals(3)); + expect(StatefulTestWidgetState.generation, equals(3)); await tester.pumpAndSettle(); - expect(StatefulTestState.generation, equals(3)); + expect(StatefulTestWidgetState.generation, equals(3)); }); testWidgets('updates widgets without animating if they are isomorphic.', @@ -457,15 +463,15 @@ void main() { await pumpChild(const Text('1')); await tester.pump(const Duration(milliseconds: 10)); - FadeTransition fade = tester.widget(find.byType(FadeTransition).first); - ScaleTransition scale = tester.widget(find.byType(ScaleTransition).first); + FadeTransition fade = tester.widget(find.byType(FadeTransition)); + ScaleTransition scale = tester.widget(find.byType(ScaleTransition)); expect(fade.opacity.value, equals(1.0)); expect(scale.scale.value, equals(1.0)); expect(find.text('1'), findsOneWidget); expect(find.text('2'), findsNothing); await pumpChild(const Text('2')); - fade = tester.widget(find.byType(FadeTransition).first); - scale = tester.widget(find.byType(ScaleTransition).first); + fade = tester.widget(find.byType(FadeTransition)); + scale = tester.widget(find.byType(ScaleTransition)); await tester.pump(const Duration(milliseconds: 20)); expect(fade.opacity.value, equals(1.0)); expect(scale.scale.value, equals(1.0)); @@ -559,15 +565,15 @@ void main() { }); } -class StatefulTest extends StatefulWidget { - const StatefulTest({Key key}) : super(key: key); +class StatefulTestWidget extends StatefulWidget { + const StatefulTestWidget({Key key}) : super(key: key); @override - StatefulTestState createState() => StatefulTestState(); + StatefulTestWidgetState createState() => StatefulTestWidgetState(); } -class StatefulTestState extends State { - StatefulTestState(); +class StatefulTestWidgetState extends State { + StatefulTestWidgetState(); static int generation = 0; @override From ff1b55e3740f70b02c4c0a3e56e639aac8a37334 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 4 Dec 2019 15:23:32 -0800 Subject: [PATCH 7/9] better docs --- .../lib/src/page_transition_switcher.dart | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/packages/animations/lib/src/page_transition_switcher.dart b/packages/animations/lib/src/page_transition_switcher.dart index aa03b0b2a6bf..48cb60ef28e8 100644 --- a/packages/animations/lib/src/page_transition_switcher.dart +++ b/packages/animations/lib/src/page_transition_switcher.dart @@ -90,11 +90,37 @@ typedef PageTransitionSwitcherTransitionBuilder = Widget Function( /// /// This process is the same as the one used by [PageRoute.buildTransitions]. /// +/// The following example shows a [transitionBuilder] that slides out the +/// old child to the right (driven by the `secondaryAnimation`) while the new +/// child fades in (driven by the `primaryAnimation`): +/// +/// ```dart +/// transitionBuilder: ( +/// Widget child, +/// Animation primaryAnimation, +/// Animation secondaryAnimation, +/// ) { +/// return SlideTransition( +/// position: Tween( +/// begin: Offset.zero, +/// end: const Offset(1.5, 0.0), +/// ).animate(secondaryAnimation), +/// child: FadeTransition( +/// opacity: Tween( +/// begin: 0.0, +/// end: 1.0, +/// ).animate(primaryAnimation), +/// child: child, +/// ), +/// ); +/// }, +/// ``` +/// /// If the children are swapped fast enough (i.e. before [duration] elapses), -/// more than one previous child can exist and be transitioning out while the +/// more than one old child can exist and be transitioning out while the /// newest one is transitioning in. /// -/// If the *new* child is the same widget type and key as the *previous* child, +/// If the *new* child is the same widget type and key as the *old* child, /// but with different parameters, then [PageTransitionSwitcher] will *not* do a /// transition between them, since as far as the framework is concerned, they /// are the same widget and the existing widget can be updated with the new @@ -131,11 +157,11 @@ class PageTransitionSwitcher extends StatefulWidget { /// The current child widget to display. /// - /// If there was a previous child, it will be transitioned out using the + /// If there was an old child, it will be transitioned out using the /// secondary animation of the [transitionBuilder], while the new child /// transitions in using the primary animation of the [transitionBuilder]. /// - /// If there was no previous child, then this child will transition in using + /// If there was no old child, then this child will transition in using /// the primary animation of the [transitionBuilder]. /// /// The child is considered to be "new" if it has a different type or [Key] @@ -153,14 +179,14 @@ class PageTransitionSwitcher extends StatefulWidget { /// underneath the old child. /// /// When this is false, the new child will transition in on top of the - /// previously set child while its primary animation and the secondary - /// animation of the previous child are running forward. This is similar to + /// old child while its primary animation and the secondary + /// animation of the old child are running forward. This is similar to /// the transition associated with pushing a new [PageRoute] on top of /// another. /// /// When this is true, the new child will transition in below the - /// previous child while its secondary animation and the primary - /// animation of the previous child are running in reverse. This is similar to + /// old child while its secondary animation and the primary + /// animation of the old child are running in reverse. This is similar to /// the transition associated with popping a [PageRoute] to reveal a new /// [PageRoute] below it. final bool reverse; @@ -170,7 +196,7 @@ class PageTransitionSwitcher extends StatefulWidget { /// /// This is only called when a new [child] is set (not for each build), or /// when a new [transitionBuilder] is set. If a new [transitionBuilder] is - /// set, then the transition is rebuilt for the current child and all previous + /// set, then the transition is rebuilt for the current child and all old /// children using the new [transitionBuilder]. The function must not return /// null. /// @@ -197,7 +223,7 @@ class _PageTransitionSwitcherState extends State void didUpdateWidget(PageTransitionSwitcher oldWidget) { super.didUpdateWidget(oldWidget); - // If the transition builder changed, then update all of the previous + // If the transition builder changed, then update all of the old // transitions. if (widget.transitionBuilder != oldWidget.transitionBuilder) { _activeEntries.forEach(_updateTransitionForEntry); @@ -216,7 +242,7 @@ class _PageTransitionSwitcherState extends State assert(Widget.canUpdate(widget.child, _currentEntry.widgetChild)); // Child has been updated. Make sure we update the child widget and // transition in _currentEntry even though we're not going to start a new - // animation, but keep the key from the previous transition so that we + // animation, but keep the key from the old transition so that we // update the transition instead of replacing it. _currentEntry.widgetChild = widget.child; _updateTransitionForEntry(_currentEntry); // uses entry.widgetChild @@ -264,10 +290,10 @@ class _PageTransitionSwitcherState extends State builder: widget.transitionBuilder, ); if (widget.reverse && _activeEntries.isNotEmpty) { - // Add below previous child. + // Add below old child. _activeEntries.insert(_activeEntries.length - 1, _currentEntry); } else { - // Add on top of previous child. + // Add on top of old child. _activeEntries.add(_currentEntry); } } From 11793f389729362c2638b10ff3a5a1195713cb23 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 4 Dec 2019 16:39:55 -0800 Subject: [PATCH 8/9] Update packages/animations/lib/src/page_transition_switcher.dart Co-Authored-By: Shi-Hao Hong --- packages/animations/lib/src/page_transition_switcher.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/animations/lib/src/page_transition_switcher.dart b/packages/animations/lib/src/page_transition_switcher.dart index 48cb60ef28e8..b7ca4a9bee20 100644 --- a/packages/animations/lib/src/page_transition_switcher.dart +++ b/packages/animations/lib/src/page_transition_switcher.dart @@ -78,7 +78,7 @@ typedef PageTransitionSwitcherTransitionBuilder = Widget Function( /// `primaryAnimation` runs forward and the value of its `secondaryAnimation` is /// usually fixed at 0.0. The widget returned by the [transitionBuilder] can /// incorporate both animations. It will use the primary animation to define how -/// its child appears, and the secondary animation to define how its child +/// its child appears, and the secondary animation to define how its child /// disappears. This is similar to the transition associated with pushing a new /// [PageRoute] on top of another. /// From c047a9231c4e056366256168892cc91a48c17abf Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 4 Dec 2019 16:40:09 -0800 Subject: [PATCH 9/9] Update packages/animations/lib/src/page_transition_switcher.dart Co-Authored-By: Shi-Hao Hong --- packages/animations/lib/src/page_transition_switcher.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/animations/lib/src/page_transition_switcher.dart b/packages/animations/lib/src/page_transition_switcher.dart index b7ca4a9bee20..c94a1ffe1fc9 100644 --- a/packages/animations/lib/src/page_transition_switcher.dart +++ b/packages/animations/lib/src/page_transition_switcher.dart @@ -175,7 +175,7 @@ class PageTransitionSwitcher extends StatefulWidget { /// transitions already in progress. final Duration duration; - /// Indicates whether the new [child] will visually appear on top or + /// Indicates whether the new [child] will visually appear on top of or /// underneath the old child. /// /// When this is false, the new child will transition in on top of the