| // Copyright 2013 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/foundation.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| /// An internal representation of a child widget subtree that, now or in the past, |
| /// was set on the [PageTransitionSwitcher.child] field and is now in the process of |
| /// transitioning. |
| /// |
| /// The internal representation includes fields that we don't want to expose to |
| /// the public API (like the controllers). |
| class _ChildEntry { |
| /// Creates a [_ChildEntry]. |
| /// |
| /// The [primaryController], [secondaryController], [transition] and |
| /// [widgetChild] parameters must not be null. |
| _ChildEntry({ |
| required this.primaryController, |
| required this.secondaryController, |
| required this.transition, |
| required this.widgetChild, |
| }); |
| |
| /// The animation controller for the child's transition. |
| final AnimationController primaryController; |
| |
| /// The (curved) animation being used to drive the transition. |
| final AnimationController secondaryController; |
| |
| /// The currently built transition for this child. |
| Widget transition; |
| |
| /// The widget's child at the time this entry was created or updated. |
| /// Used to rebuild the transition if necessary. |
| Widget widgetChild; |
| |
| /// Release the resources used by this object. |
| /// |
| /// The object is no longer usable after this method is called. |
| void dispose() { |
| primaryController.dispose(); |
| secondaryController.dispose(); |
| } |
| |
| @override |
| String toString() { |
| return 'PageTransitionSwitcherEntry#${shortHash(this)}($widgetChild)'; |
| } |
| } |
| |
| /// Signature for builders used to generate custom layouts for |
| /// [PageTransitionSwitcher]. |
| /// |
| /// The builder should return a widget which contains the given children, laid |
| /// out as desired. It must not return null. The builder should be able to |
| /// handle an empty list of `entries`. |
| typedef PageTransitionSwitcherLayoutBuilder = Widget Function( |
| List<Widget> entries, |
| ); |
| |
| /// Signature for builders used to generate custom transitions for |
| /// [PageTransitionSwitcher]. |
| /// |
| /// 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<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( |
| /// 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 |
| /// 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. |
| /// |
| /// PageTransitionSwitcher uses the [layoutBuilder] property to lay out the |
| /// old and new child widgets. By default, [defaultLayoutBuilder] is used. |
| /// See the documentation for [layoutBuilder] for suggestions on how to |
| /// configure the layout of the incoming and outgoing child widgets if |
| /// [defaultLayoutBuilder] is not your desired layout. |
| class PageTransitionSwitcher extends StatefulWidget { |
| /// Creates a [PageTransitionSwitcher]. |
| /// |
| /// 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.layoutBuilder = defaultLayoutBuilder, |
| this.child, |
| }) : 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 |
| /// 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 |
| /// 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; |
| |
| /// A function that wraps all of the children that are transitioning out, and |
| /// the [child] that's transitioning in, with a widget that lays all of them |
| /// out. This is called every time this widget is built. The function must not |
| /// return null. |
| /// |
| /// The default [PageTransitionSwitcherLayoutBuilder] used is |
| /// [defaultLayoutBuilder]. |
| /// |
| /// The following example shows a [layoutBuilder] that places all entries in a |
| /// [Stack] that sizes itself to match the largest of the active entries. |
| /// All children are aligned on the top left corner of the [Stack]. |
| /// |
| /// ```dart |
| /// PageTransitionSwitcher( |
| /// duration: const Duration(milliseconds: 100), |
| /// child: Container(color: Colors.red), |
| /// layoutBuilder: ( |
| /// List<Widget> entries, |
| /// ) { |
| /// return Stack( |
| /// children: entries, |
| /// alignment: Alignment.topLeft, |
| /// ); |
| /// }, |
| /// ), |
| /// ``` |
| /// See [PageTransitionSwitcherLayoutBuilder] for more information about |
| /// how a layout builder should function. |
| final PageTransitionSwitcherLayoutBuilder layoutBuilder; |
| |
| /// The default layout builder for [PageTransitionSwitcher]. |
| /// |
| /// This function is the default way for how the new and old child widgets are placed |
| /// during the transition between the two widgets. All children are placed in a |
| /// [Stack] that sizes itself to match the largest of the child or a previous child. |
| /// The children are centered on each other. |
| /// |
| /// See [PageTransitionSwitcherTransitionBuilder] for more information on the function |
| /// signature. |
| static Widget defaultLayoutBuilder(List<Widget> entries) { |
| return Stack( |
| alignment: Alignment.center, |
| children: entries, |
| ); |
| } |
| |
| @override |
| State<PageTransitionSwitcher> 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; |
| _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) { |
| 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(); |
| } 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, |
| ); |
| final _ChildEntry entry = _ChildEntry( |
| widgetChild: child, |
| transition: KeyedSubtree.wrap( |
| 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, |
| ); |
| entry.transition = KeyedSubtree( |
| key: entry.transition.key, |
| child: transition, |
| ); |
| } |
| |
| @override |
| void dispose() { |
| for (final _ChildEntry entry in _activeEntries) { |
| entry.dispose(); |
| } |
| super.dispose(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return widget.layoutBuilder(_activeEntries |
| .map<Widget>((_ChildEntry entry) => entry.transition) |
| .toList()); |
| } |
| } |