-
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 all 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,381 @@ | ||
| // 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() { | ||
| 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`. | ||
|
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, |
||
| /// | ||
| /// 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<double> primaryAnimation, | ||
| Animation<double> secondaryAnimation, | ||
| ); | ||
|
|
||
| /// 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. | ||
| /// | ||
| /// 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, 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]. | ||
| /// | ||
| /// 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<double> primaryAnimation, | ||
| /// Animation<double> secondaryAnimation, | ||
| /// ) { | ||
| /// return SlideTransition( | ||
|
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. NICE |
||
| /// position: Tween<Offset>( | ||
| /// begin: Offset.zero, | ||
| /// end: const Offset(1.5, 0.0), | ||
| /// ).animate(secondaryAnimation), | ||
| /// child: FadeTransition( | ||
| /// opacity: Tween<double>( | ||
| /// 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 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 *old* 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). 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 | ||
| /// 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 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 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 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] | ||
| /// (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 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 | ||
| /// 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 | ||
|
shihaohong marked this conversation as resolved.
|
||
| /// 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; | ||
|
|
||
| /// 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. |
||
| /// 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 old | ||
| /// children using the new [transitionBuilder]. The function must not return | ||
| /// null. | ||
| /// | ||
| /// The child provided to the transitionBuilder may be 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(shouldAnimate: false); | ||
| } | ||
|
|
||
| @override | ||
| void didUpdateWidget(PageTransitionSwitcher oldWidget) { | ||
| super.didUpdateWidget(oldWidget); | ||
|
|
||
| // If the transition builder changed, then update all of the old | ||
| // 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(shouldAnimate: 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 old transition so that we | ||
| // update the transition instead of replacing it. | ||
| _currentEntry.widgetChild = widget.child; | ||
| _updateTransitionForEntry(_currentEntry); // uses entry.widgetChild | ||
| } | ||
| } | ||
|
|
||
| void _addEntryForNewChild({@required bool shouldAnimate}) { | ||
| assert(shouldAnimate || _currentEntry == null); | ||
| if (_currentEntry != null) { | ||
| assert(shouldAnimate); | ||
| 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 (shouldAnimate) { | ||
| 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 old child. | ||
| _activeEntries.insert(_activeEntries.length - 1, _currentEntry); | ||
| } else { | ||
| // Add on top of old child. | ||
| _activeEntries.add(_currentEntry); | ||
| } | ||
| } | ||
|
|
||
| _ChildEntry _newEntry({ | ||
| @required Widget child, | ||
| @required PageTransitionSwitcherTransitionBuilder builder, | ||
| @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( | ||
|
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. |
||
| transition, | ||
| _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) { | ||
| 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: transition, | ||
| ); | ||
| } | ||
|
|
||
| @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 :(