| // Copyright 2014 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 'basic.dart'; |
| import 'framework.dart'; |
| import 'ticker_provider.dart'; |
| import 'transitions.dart'; |
| |
| // Internal representation of a child that, now or in the past, was set on the |
| // AnimatedSwitcher.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 controller). |
| class _ChildEntry { |
| _ChildEntry({ |
| required this.controller, |
| required this.animation, |
| required this.transition, |
| required this.widgetChild, |
| }) : assert(animation != null), |
| assert(transition != null), |
| assert(controller != null); |
| |
| // The animation controller for the child's transition. |
| final AnimationController controller; |
| |
| // The (curved) animation being used to drive the transition. |
| final Animation<double> animation; |
| |
| // 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; |
| |
| @override |
| String toString() => 'Entry#${shortHash(this)}($widgetChild)'; |
| } |
| |
| /// Signature for builders used to generate custom transitions for |
| /// [AnimatedSwitcher]. |
| /// |
| /// The `child` should be transitioning in when the `animation` is running in |
| /// the forward direction. |
| /// |
| /// The function should return a widget which wraps the given `child`. It may |
| /// also use the `animation` to inform its transition. It must not return null. |
| typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation<double> animation); |
| |
| /// Signature for builders used to generate custom layouts for |
| /// [AnimatedSwitcher]. |
| /// |
| /// 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 `previousChildren`, or a null `currentChild`. |
| /// |
| /// The `previousChildren` list is an unmodifiable list, sorted with the oldest |
| /// at the beginning and the newest at the end. It does not include the |
| /// `currentChild`. |
| typedef AnimatedSwitcherLayoutBuilder = Widget Function(Widget? currentChild, List<Widget> previousChildren); |
| |
| /// A widget that by default does a cross-fade between a new widget and the |
| /// widget previously set on the [AnimatedSwitcher] as a child. |
| /// |
| /// {@youtube 560 315 https://www.youtube.com/watch?v=2W7POjFb88g} |
| /// |
| /// If they 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 [AnimatedSwitcher] 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.) |
| /// |
| /// The type of transition can be changed from a cross-fade to a custom |
| /// transition by setting the [transitionBuilder]. |
| /// |
| /// {@tool dartpad} |
| /// This sample shows a counter that animates the scale of a text widget |
| /// whenever the value changes. |
| /// |
| /// ** See code in examples/api/lib/widgets/animated_switcher/animated_switcher.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [AnimatedCrossFade], which only fades between two children, but also |
| /// interpolates their sizes, and is reversible. |
| /// * [AnimatedOpacity], which can be used to switch between nothingness and |
| /// a given child by fading the child in and out. |
| /// * [FadeTransition], which [AnimatedSwitcher] uses to perform the transition. |
| class AnimatedSwitcher extends StatefulWidget { |
| /// Creates an [AnimatedSwitcher]. |
| /// |
| /// The [duration], [transitionBuilder], [layoutBuilder], [switchInCurve], and |
| /// [switchOutCurve] parameters must not be null. |
| const AnimatedSwitcher({ |
| super.key, |
| this.child, |
| required this.duration, |
| this.reverseDuration, |
| this.switchInCurve = Curves.linear, |
| this.switchOutCurve = Curves.linear, |
| this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, |
| this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, |
| }) : assert(duration != null), |
| assert(switchInCurve != null), |
| assert(switchOutCurve != null), |
| assert(transitionBuilder != null), |
| assert(layoutBuilder != null); |
| |
| /// The current child widget to display. If there was a previous child, then |
| /// that child will be faded out using the [switchOutCurve], while the new |
| /// child is faded in with the [switchInCurve], over the [duration]. |
| /// |
| /// If there was no previous child, then this child will fade in using the |
| /// [switchInCurve] over the [duration]. |
| /// |
| /// The child is considered to be "new" if it has a different type or [Key] |
| /// (see [Widget.canUpdate]). |
| /// |
| /// To change the kind of transition used, see [transitionBuilder]. |
| 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. The same duration is used when fading out, unless |
| /// [reverseDuration] is set. Changing [duration] will not affect the |
| /// durations of transitions already in progress. |
| final Duration duration; |
| |
| /// The duration of the transition from the new [child] value to the old one. |
| /// |
| /// This duration is applied to the given [child] when that property is set to |
| /// a new child. Changing [reverseDuration] will not affect the durations of |
| /// transitions already in progress. |
| /// |
| /// If not set, then the value of [duration] is used by default. |
| final Duration? reverseDuration; |
| |
| /// The animation curve to use when transitioning in a new [child]. |
| /// |
| /// This curve is applied to the given [child] when that property is set to a |
| /// new child. Changing [switchInCurve] will not affect the curve of a |
| /// transition already in progress. |
| /// |
| /// The [switchOutCurve] is used when fading out, except that if [child] is |
| /// changed while the current child is in the middle of fading in, |
| /// [switchInCurve] will be run in reverse from that point instead of jumping |
| /// to the corresponding point on [switchOutCurve]. |
| final Curve switchInCurve; |
| |
| /// The animation curve to use when transitioning a previous [child] out. |
| /// |
| /// This curve is applied to the [child] when the child is faded in (or when |
| /// the widget is created, for the first child). Changing [switchOutCurve] |
| /// will not affect the curves of already-visible widgets, it only affects the |
| /// curves of future children. |
| /// |
| /// If [child] is changed while the current child is in the middle of fading |
| /// in, [switchInCurve] will be run in reverse from that point instead of |
| /// jumping to the corresponding point on [switchOutCurve]. |
| final Curve switchOutCurve; |
| |
| /// A function that wraps a new [child] with an animation that transitions |
| /// the [child] in when the animation runs in the forward direction and out |
| /// when the animation runs in the reverse direction. 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 default is [AnimatedSwitcher.defaultTransitionBuilder]. |
| /// |
| /// The animation provided to the builder has the [duration] and |
| /// [switchInCurve] or [switchOutCurve] applied as provided when the |
| /// corresponding [child] was first provided. |
| /// |
| /// See also: |
| /// |
| /// * [AnimatedSwitcherTransitionBuilder] for more information about |
| /// how a transition builder should function. |
| final AnimatedSwitcherTransitionBuilder 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 is [AnimatedSwitcher.defaultLayoutBuilder]. |
| /// |
| /// See also: |
| /// |
| /// * [AnimatedSwitcherLayoutBuilder] for more information about |
| /// how a layout builder should function. |
| final AnimatedSwitcherLayoutBuilder layoutBuilder; |
| |
| @override |
| State<AnimatedSwitcher> createState() => _AnimatedSwitcherState(); |
| |
| /// The transition builder used as the default value of [transitionBuilder]. |
| /// |
| /// The new child is given a [FadeTransition] which increases opacity as |
| /// the animation goes from 0.0 to 1.0, and decreases when the animation is |
| /// reversed. |
| /// |
| /// This is an [AnimatedSwitcherTransitionBuilder] function. |
| static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) { |
| return FadeTransition( |
| key: ValueKey<Key?>(child.key), |
| opacity: animation, |
| child: child, |
| ); |
| } |
| |
| /// The layout builder used as the default value of [layoutBuilder]. |
| /// |
| /// The new child is 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. |
| /// |
| /// This is an [AnimatedSwitcherLayoutBuilder] function. |
| static Widget defaultLayoutBuilder(Widget? currentChild, List<Widget> previousChildren) { |
| return Stack( |
| alignment: Alignment.center, |
| children: <Widget>[ |
| ...previousChildren, |
| if (currentChild != null) currentChild, |
| ], |
| ); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms')); |
| properties.add(IntProperty('reverseDuration', reverseDuration?.inMilliseconds, unit: 'ms', defaultValue: null)); |
| } |
| } |
| |
| class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProviderStateMixin { |
| _ChildEntry? _currentEntry; |
| final Set<_ChildEntry> _outgoingEntries = <_ChildEntry>{}; |
| List<Widget>? _outgoingWidgets = const <Widget>[]; |
| int _childNumber = 0; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _addEntryForNewChild(animate: false); |
| } |
| |
| @override |
| void didUpdateWidget(AnimatedSwitcher oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| |
| // If the transition builder changed, then update all of the previous |
| // transitions. |
| if (widget.transitionBuilder != oldWidget.transitionBuilder) { |
| _outgoingEntries.forEach(_updateTransitionForEntry); |
| if (_currentEntry != null) { |
| _updateTransitionForEntry(_currentEntry!); |
| } |
| _markChildWidgetCacheAsDirty(); |
| } |
| |
| 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 |
| _markChildWidgetCacheAsDirty(); |
| } |
| } |
| |
| void _addEntryForNewChild({ required bool animate }) { |
| assert(animate || _currentEntry == null); |
| if (_currentEntry != null) { |
| assert(animate); |
| assert(!_outgoingEntries.contains(_currentEntry)); |
| _outgoingEntries.add(_currentEntry!); |
| _currentEntry!.controller.reverse(); |
| _markChildWidgetCacheAsDirty(); |
| _currentEntry = null; |
| } |
| if (widget.child == null) { |
| return; |
| } |
| final AnimationController controller = AnimationController( |
| duration: widget.duration, |
| reverseDuration: widget.reverseDuration, |
| vsync: this, |
| ); |
| final Animation<double> animation = CurvedAnimation( |
| parent: controller, |
| curve: widget.switchInCurve, |
| reverseCurve: widget.switchOutCurve, |
| ); |
| _currentEntry = _newEntry( |
| child: widget.child!, |
| controller: controller, |
| animation: animation, |
| builder: widget.transitionBuilder, |
| ); |
| if (animate) { |
| controller.forward(); |
| } else { |
| assert(_outgoingEntries.isEmpty); |
| controller.value = 1.0; |
| } |
| } |
| |
| _ChildEntry _newEntry({ |
| required Widget child, |
| required AnimatedSwitcherTransitionBuilder builder, |
| required AnimationController controller, |
| required Animation<double> animation, |
| }) { |
| final _ChildEntry entry = _ChildEntry( |
| widgetChild: child, |
| transition: KeyedSubtree.wrap(builder(child, animation), _childNumber), |
| animation: animation, |
| controller: controller, |
| ); |
| animation.addStatusListener((AnimationStatus status) { |
| if (status == AnimationStatus.dismissed) { |
| setState(() { |
| assert(mounted); |
| assert(_outgoingEntries.contains(entry)); |
| _outgoingEntries.remove(entry); |
| _markChildWidgetCacheAsDirty(); |
| }); |
| controller.dispose(); |
| } |
| }); |
| return entry; |
| } |
| |
| void _markChildWidgetCacheAsDirty() { |
| _outgoingWidgets = null; |
| } |
| |
| void _updateTransitionForEntry(_ChildEntry entry) { |
| entry.transition = KeyedSubtree( |
| key: entry.transition.key, |
| child: widget.transitionBuilder(entry.widgetChild, entry.animation), |
| ); |
| } |
| |
| void _rebuildOutgoingWidgetsIfNeeded() { |
| _outgoingWidgets ??= List<Widget>.unmodifiable( |
| _outgoingEntries.map<Widget>((_ChildEntry entry) => entry.transition), |
| ); |
| assert(_outgoingEntries.length == _outgoingWidgets!.length); |
| assert(_outgoingEntries.isEmpty || _outgoingEntries.last.transition == _outgoingWidgets!.last); |
| } |
| |
| @override |
| void dispose() { |
| if (_currentEntry != null) { |
| _currentEntry!.controller.dispose(); |
| } |
| for (final _ChildEntry entry in _outgoingEntries) { |
| entry.controller.dispose(); |
| } |
| super.dispose(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| _rebuildOutgoingWidgetsIfNeeded(); |
| return widget.layoutBuilder(_currentEntry?.transition, _outgoingWidgets!.where((Widget outgoing) => outgoing.key != _currentEntry?.transition.key).toSet().toList()); |
| } |
| } |