Skip to content
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

PageTransitionSwitcher #50

Merged
merged 9 commits into from
Dec 5, 2019
Merged

PageTransitionSwitcher #50

merged 9 commits into from
Dec 5, 2019

Conversation

goderbauer
Copy link
Member

@goderbauer goderbauer commented Dec 2, 2019

This is a variation of an AnimatedSwitcher [1], 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 goal is that transitions written for transitioning PageRoutes in and out can be re-used to switch between two widgets. This can be useful in combination with a BottomNavigationBar to switch between widgets when another BottomNavigationBarItem is activated using a PageRoute-like transition.

[1] https://master-api.flutter.dev/flutter/widgets/AnimatedSwitcher-class.html

@goderbauer
Copy link
Member Author

/cc @HansMuller @shihaohong

Copy link

@shihaohong shihaohong left a comment

Choose a reason for hiding this comment

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

I'm holding off on the rest because I feel like moving further without being 100% sure of what "previous", "old" and "new" child may mean would make reviewing the rest of the code difficult.

Copy link

@shihaohong shihaohong left a comment

Choose a reason for hiding this comment

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

The logic and tests look good! Most of my comments are suggestions to clarify the language of the code since it can be confusing to wrap your head around if you're unfamiliar with the primary/secondaryAnimation jargon and expectations.

@required this.secondaryController,
@required this.transition,
@required this.widgetChild,
}) : assert(primaryController != null),

Choose a reason for hiding this comment

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

nit:

Suggested change
}) : assert(primaryController != null),
}) : assert(primaryController != null),
assert(secondaryController != null),
assert(widgetChild != null),
assert(transition != null);

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that would be better! However, unfortunately, dartfmt is enabled for this repository giving me no control over the formatting here :(

packages/animations/lib/src/page_transition_switcher.dart Outdated Show resolved Hide resolved
/// 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

Choose a reason for hiding this comment

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

Suggested change
/// 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.)

Choose a reason for hiding this comment

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

Suggested change
/// indicator is fading in.)
/// indicator is fading in.

/// durations of transitions already in progress.
final Duration duration;

/// Indicates the direction of the animation when a new [child] is set.

Choose a reason for hiding this comment

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

Maybe "Determines whether or not the new [child] will replace the previous [child] from the top or underneath the previous [child]", or something similar to describe the actual transition visually.

expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.4));
expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.0));

// Container one is at the bottom.

Choose a reason for hiding this comment

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

Suggested change
// Container one is at the bottom.
// Container one is underneath container two.

_primaryAnimation = _getPrimaryAnimation(
<Key>[containerOne, containerTwo, containerThree], tester);
_secondaryAnimation = _getSecondaryAnimation(
<Key>[containerOne, containerTwo, containerThree], tester);

Choose a reason for hiding this comment

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

Maybe comment the expectation for this particular case since it's pretty unique. Like:

    // 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.

ScaleTransition scale = tester.firstWidget(find.byType(ScaleTransition));
expect(fade.opacity.value, moreOrLessEquals(1.0));
expect(scale.scale.value, moreOrLessEquals(0.4));
await tester.pumpAndSettle();

Choose a reason for hiding this comment

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

Maybe add a comment here saying that this pumpAndSettle call is meant to let the entering widget's transition continue to completion

});
}

class StatefulTest extends StatefulWidget {

Choose a reason for hiding this comment

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

Perhaps rename to make it clearer:

Suggested change
class StatefulTest extends StatefulWidget {
class StatefulTestWidget extends StatefulWidget {

expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsNothing);
await pumpChild(const Text('2'));
fade = tester.widget(find.byType(FadeTransition).first);

Choose a reason for hiding this comment

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

Shouldn't there only be one widget, since it's not animating and simply updating the value?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, you're right. removed.

Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

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

Mostly small stuff. Explaining how buildTransitions works is difficult!

@@ -51,8 +51,8 @@ typedef PageTransitionSwitcherTransitionBuilder = Widget Function(
Animation<double> 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
Copy link
Contributor

Choose a reason for hiding this comment

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

from the current child to a new 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
/// 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
Copy link
Contributor

Choose a reason for hiding this comment

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

In this case by "but with different parameters" I assume that you mean that the new child was configured differently. Developers may find this a little confusing. You might provide an example. Like: 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 AnimatedSwitcher class API doc makes this same point. Might make sense to sync this paragraph with that one.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added.

@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe: when the [child] is changed.

Widgets don't have properties that are set so much as constructor parameters whose values are provided.

/// 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
Copy link
Contributor

Choose a reason for hiding this comment

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

previously set child => previous child

I'll stop complaining about "child setting" etc.

/// specified, similar to how the enter and exit transitions of a [PageRoute]
/// are defined.
///
/// The transitions returned by the [transitionBuilder] are driven by two
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the difficult part of explaining route transitions. I'll take a crack at the whole thing:

When a new [child] is specified, the [transitionBuilder] is effectively applied twice, once to the old child and once to the new one. The old child's secondaryAnimation runs forward, and the value of its primaryAnimation is fixed at 1.0. The new child's primaryAnimation runs forward and the value of its secondary animation is 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 process is the same as the one used by [PageRoute.buildTransitions].

[And then include simple transitionBuilder example. There's a simple example here: https://api.flutter.dev/flutter/material/MaterialPageRoute/buildTransitions.html; prefer one that uses both animations]

Copy link
Member Author

Choose a reason for hiding this comment

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

Forgot to add the example, working on that now.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

final _ChildEntry entry = _ChildEntry(
widgetChild: child,
transition: KeyedSubtree.wrap(
builder(child, primaryController, secondaryController),
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe assert that the builder's return value is non-null here and provide a nice error message

/// [PageRoute] below it.
final bool reverse;

/// A function that wraps a new [child] with a primary and secondary animation
Copy link
Contributor

Choose a reason for hiding this comment

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

The new PageTransition child could be null, by the transitionBuilder's child is guaranteed to be non-null, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. added documentation.

///
/// 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Will a duration change affect in-progress transitions if transitionBuilder is also changed?

Copy link
Member Author

Choose a reason for hiding this comment

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

No.

_secondaryAnimation =
_getSecondaryAnimation(<Key>[containerOne, containerTwo], tester);
// Secondary is running for outgoing widget.
expect(_primaryAnimation[containerOne], moreOrLessEquals(1.0));
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe moreOrLessEquals isn't needed for cases were we believe the animation's value will have reached 0 or 1

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed where not needed.

import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets('transitions in a new child.', (WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should verify that transitioning to/from a null child works too

Choose a reason for hiding this comment

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

I believe there is a subsequent test for this called "handles null children"

Copy link
Member Author

@goderbauer goderbauer Dec 4, 2019

Choose a reason for hiding this comment

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

Yes, there is one later in this file :)

@goderbauer
Copy link
Member Author

PTAL and let me know if the descriptions are clearer now.

Copy link

@shihaohong shihaohong left a comment

Choose a reason for hiding this comment

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

LGTM, it's much clearer now :)

packages/animations/lib/src/page_transition_switcher.dart Outdated Show resolved Hide resolved
packages/animations/lib/src/page_transition_switcher.dart Outdated Show resolved Hide resolved
goderbauer and others added 2 commits December 4, 2019 16:39
Co-Authored-By: Shi-Hao Hong <shihaohong@google.com>
Co-Authored-By: Shi-Hao Hong <shihaohong@google.com>
Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

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

LGTM

/// Animation<double> primaryAnimation,
/// Animation<double> secondaryAnimation,
/// ) {
/// return SlideTransition(
Copy link
Contributor

Choose a reason for hiding this comment

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

NICE

@goderbauer goderbauer merged commit 4413ccf into flutter:master Dec 5, 2019
@goderbauer goderbauer deleted the switcher branch December 5, 2019 17:23
stuartmorgan pushed a commit that referenced this pull request Oct 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants