-
Notifications
You must be signed in to change notification settings - Fork 3.8k
PageTransitionSwitcher #50
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
Changes from 5 commits
0ab0817
1c95b86
782621f
d62a35b
b0c5f57
c7bd29b
ff1b55e
11793f3
c047a92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,4 @@ | |
| // found in the LICENSE file. | ||
|
|
||
| export 'src/open_container.dart'; | ||
| export 'src/page_transition_switcher.dart'; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,317 @@ | ||||||
| // 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)'; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If someone sees this string (maybe in an error message) - will they be confused by "Entry"?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to give more context. |
||||||
| } | ||||||
|
|
||||||
| /// Signature for builders used to generate custom transitions for | ||||||
| /// [PageTransitionSwitcher]. | ||||||
| /// | ||||||
| /// The function should return a widget which wraps the given `child`. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be worthwhile to reiterate how the animations run during a transition. This is the same text as I suggested below. When a PageTransitionSwitcher's child is replaced, |
||||||
| typedef PageTransitionSwitcherTransitionBuilder = Widget Function( | ||||||
| Widget child, | ||||||
| Animation<double> primaryAnimation, | ||||||
| Animation<double> secondaryAnimation, | ||||||
| ); | ||||||
|
|
||||||
| /// A widget that transitions from a previously set child to a newly set child | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. from the current child to a new 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 | ||||||
| /// specified, similar to how the enter and exit transitions of a [PageRoute] | ||||||
| /// are defined. | ||||||
| /// | ||||||
| /// The transitions returned by the [transitionBuilder] are driven by two | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 [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]
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Forgot to add the example, working on that now.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||
| /// animations: a primary one and a secondary one. When a new child is | ||||||
| /// transitioning in while [reverse] is false, the primary animation of the | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to add one example for when reverse is false? I was thinking maybe a few more for the other cases, like
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What kind of examples are you looking for?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking a simple default example of |
||||||
| /// 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 | ||||||
|
shihaohong marked this conversation as resolved.
Outdated
|
||||||
| /// 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 *previous* child, | ||||||
| /// but with different parameters, then [PageTransitionSwitcher] will *not* do a | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 The AnimatedSwitcher class API doc makes this same point. Might make sense to sync this paragraph with that one.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. |
||||||
| /// 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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// 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.) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| class PageTransitionSwitcher extends StatefulWidget { | ||||||
| /// Creates a [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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. transitioning => transitioned |
||||||
| /// 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. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. |
||||||
| final Duration duration; | ||||||
|
|
||||||
| /// Indicates the direction of the animation when a new [child] is set. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
| /// | ||||||
| /// When this is false, the new child will transition in on top of the | ||||||
| /// previously set child while its primary animation and the secondary | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
| /// 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 | ||||||
|
shihaohong marked this conversation as resolved.
|
||||||
| /// 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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. added documentation. |
||||||
| /// to transition between the previously set child and the new child. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a little confusing. The returned widget defines how [child] enters and leaves. It doesn't explicitly define a transition between two children. |
||||||
| /// | ||||||
| /// 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<PageTransitionSwitcher> | ||||||
| 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; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the transition duration was 1ms, then this value will overflow in about 292 million years. Probably a safe bet :-). |
||||||
| _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}) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe update the bool variable name to
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Conventionally
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wish, but dartfmt is tying my hands in this repo. |
||||||
| 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) { | ||||||
|
shihaohong marked this conversation as resolved.
|
||||||
| 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(); | ||||||
|
shihaohong marked this conversation as resolved.
|
||||||
| } 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( | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main purpose of
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. |
||||||
| builder(child, primaryController, secondaryController), | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
| _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( | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe assert that the child is non-null here and provide a nice error message |
||||||
| 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<Widget>((_ChildEntry entry) => entry.transition) | ||||||
| .toList(), | ||||||
| alignment: Alignment.center, | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
There was a problem hiding this comment.
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 :(