| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'animated_cross_fade.dart'; |
| 6 | /// @docImport 'implicit_animations.dart'; |
| 7 | library; |
| 8 | |
| 9 | import 'package:flutter/foundation.dart'; |
| 10 | |
| 11 | import 'basic.dart'; |
| 12 | import 'framework.dart'; |
| 13 | import 'ticker_provider.dart'; |
| 14 | import 'transitions.dart'; |
| 15 | |
| 16 | // Internal representation of a child that, now or in the past, was set on the |
| 17 | // AnimatedSwitcher.child field, but is now in the process of |
| 18 | // transitioning. The internal representation includes fields that we don't want |
| 19 | // to expose to the public API (like the controller). |
| 20 | class _ChildEntry { |
| 21 | _ChildEntry({ |
| 22 | required this.controller, |
| 23 | required this.animation, |
| 24 | required this.transition, |
| 25 | required this.widgetChild, |
| 26 | }); |
| 27 | |
| 28 | // The animation controller for the child's transition. |
| 29 | final AnimationController controller; |
| 30 | |
| 31 | // The (curved) animation being used to drive the transition. |
| 32 | final CurvedAnimation animation; |
| 33 | |
| 34 | // The currently built transition for this child. |
| 35 | Widget transition; |
| 36 | |
| 37 | // The widget's child at the time this entry was created or updated. |
| 38 | // Used to rebuild the transition if necessary. |
| 39 | Widget widgetChild; |
| 40 | |
| 41 | @override |
| 42 | String toString() => 'Entry# ${shortHash(this)}( $widgetChild)' ; |
| 43 | } |
| 44 | |
| 45 | /// Signature for builders used to generate custom transitions for |
| 46 | /// [AnimatedSwitcher]. |
| 47 | /// |
| 48 | /// The `child` should be transitioning in when the `animation` is running in |
| 49 | /// the forward direction. |
| 50 | /// |
| 51 | /// The function should return a widget which wraps the given `child`. It may |
| 52 | /// also use the `animation` to inform its transition. It must not return null. |
| 53 | typedef AnimatedSwitcherTransitionBuilder = |
| 54 | Widget Function(Widget child, Animation<double> animation); |
| 55 | |
| 56 | /// Signature for builders used to generate custom layouts for |
| 57 | /// [AnimatedSwitcher]. |
| 58 | /// |
| 59 | /// The builder should return a widget which contains the given children, laid |
| 60 | /// out as desired. It must not return null. The builder should be able to |
| 61 | /// handle an empty list of `previousChildren`, or a null `currentChild`. |
| 62 | /// |
| 63 | /// The `previousChildren` list is an unmodifiable list, sorted with the oldest |
| 64 | /// at the beginning and the newest at the end. It does not include the |
| 65 | /// `currentChild`. |
| 66 | typedef AnimatedSwitcherLayoutBuilder = |
| 67 | Widget Function(Widget? currentChild, List<Widget> previousChildren); |
| 68 | |
| 69 | /// A widget that by default does a cross-fade between a new widget and the |
| 70 | /// widget previously set on the [AnimatedSwitcher] as a child. |
| 71 | /// |
| 72 | /// {@youtube 560 315 https://www.youtube.com/watch?v=2W7POjFb88g} |
| 73 | /// |
| 74 | /// If they are swapped fast enough (i.e. before [duration] elapses), more than |
| 75 | /// one previous child can exist and be transitioning out while the newest one |
| 76 | /// is transitioning in. |
| 77 | /// |
| 78 | /// If the "new" child is the same widget type and key as the "old" child, but |
| 79 | /// with different parameters, then [AnimatedSwitcher] will *not* do a |
| 80 | /// transition between them, since as far as the framework is concerned, they |
| 81 | /// are the same widget and the existing widget can be updated with the new |
| 82 | /// parameters. To force the transition to occur, set a [Key] on each child |
| 83 | /// widget that you wish to be considered unique (typically a [ValueKey] on the |
| 84 | /// widget data that distinguishes this child from the others). |
| 85 | /// |
| 86 | /// The same key can be used for a new child as was used for an already-outgoing |
| 87 | /// child; the two will not be considered related. (For example, if a progress |
| 88 | /// indicator with key A is first shown, then an image with key B, then another |
| 89 | /// progress indicator with key A again, all in rapid succession, then the old |
| 90 | /// progress indicator and the image will be fading out while a new progress |
| 91 | /// indicator is fading in.) |
| 92 | /// |
| 93 | /// The type of transition can be changed from a cross-fade to a custom |
| 94 | /// transition by setting the [transitionBuilder]. |
| 95 | /// |
| 96 | /// {@tool dartpad} |
| 97 | /// This sample shows a counter that animates the scale of a text widget |
| 98 | /// whenever the value changes. |
| 99 | /// |
| 100 | /// ** See code in examples/api/lib/widgets/animated_switcher/animated_switcher.0.dart ** |
| 101 | /// {@end-tool} |
| 102 | /// |
| 103 | /// See also: |
| 104 | /// |
| 105 | /// * [AnimatedCrossFade], which only fades between two children, but also |
| 106 | /// interpolates their sizes, and is reversible. |
| 107 | /// * [AnimatedOpacity], which can be used to switch between nothingness and |
| 108 | /// a given child by fading the child in and out. |
| 109 | /// * [FadeTransition], which [AnimatedSwitcher] uses to perform the transition. |
| 110 | class AnimatedSwitcher extends StatefulWidget { |
| 111 | /// Creates an [AnimatedSwitcher]. |
| 112 | const AnimatedSwitcher({ |
| 113 | super.key, |
| 114 | this.child, |
| 115 | required this.duration, |
| 116 | this.reverseDuration, |
| 117 | this.switchInCurve = Curves.linear, |
| 118 | this.switchOutCurve = Curves.linear, |
| 119 | this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, |
| 120 | this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, |
| 121 | }); |
| 122 | |
| 123 | /// The current child widget to display. If there was a previous child, then |
| 124 | /// that child will be faded out using the [switchOutCurve], while the new |
| 125 | /// child is faded in with the [switchInCurve], over the [duration]. |
| 126 | /// |
| 127 | /// If there was no previous child, then this child will fade in using the |
| 128 | /// [switchInCurve] over the [duration]. |
| 129 | /// |
| 130 | /// The child is considered to be "new" if it has a different type or [Key] |
| 131 | /// (see [Widget.canUpdate]). |
| 132 | /// |
| 133 | /// To change the kind of transition used, see [transitionBuilder]. |
| 134 | final Widget? child; |
| 135 | |
| 136 | /// The duration of the transition from the old [child] value to the new one. |
| 137 | /// |
| 138 | /// This duration is applied to the given [child] when that property is set to |
| 139 | /// a new child. The same duration is used when fading out, unless |
| 140 | /// [reverseDuration] is set. Changing [duration] will not affect the |
| 141 | /// durations of transitions already in progress. |
| 142 | final Duration duration; |
| 143 | |
| 144 | /// The duration of the transition from the new [child] value to the old one. |
| 145 | /// |
| 146 | /// This duration is applied to the given [child] when that property is set to |
| 147 | /// a new child. Changing [reverseDuration] will not affect the durations of |
| 148 | /// transitions already in progress. |
| 149 | /// |
| 150 | /// If not set, then the value of [duration] is used by default. |
| 151 | final Duration? reverseDuration; |
| 152 | |
| 153 | /// The animation curve to use when transitioning in a new [child]. |
| 154 | /// |
| 155 | /// This curve is applied to the given [child] when that property is set to a |
| 156 | /// new child. Changing [switchInCurve] will not affect the curve of a |
| 157 | /// transition already in progress. |
| 158 | /// |
| 159 | /// The [switchOutCurve] is used when fading out, except that if [child] is |
| 160 | /// changed while the current child is in the middle of fading in, |
| 161 | /// [switchInCurve] will be run in reverse from that point instead of jumping |
| 162 | /// to the corresponding point on [switchOutCurve]. |
| 163 | final Curve switchInCurve; |
| 164 | |
| 165 | /// The animation curve to use when transitioning a previous [child] out. |
| 166 | /// |
| 167 | /// This curve is applied to the [child] when the child is faded in (or when |
| 168 | /// the widget is created, for the first child). Changing [switchOutCurve] |
| 169 | /// will not affect the curves of already-visible widgets, it only affects the |
| 170 | /// curves of future children. |
| 171 | /// |
| 172 | /// If [child] is changed while the current child is in the middle of fading |
| 173 | /// in, [switchInCurve] will be run in reverse from that point instead of |
| 174 | /// jumping to the corresponding point on [switchOutCurve]. |
| 175 | final Curve switchOutCurve; |
| 176 | |
| 177 | /// A function that wraps a new [child] with an animation that transitions |
| 178 | /// the [child] in when the animation runs in the forward direction and out |
| 179 | /// when the animation runs in the reverse direction. This is only called |
| 180 | /// when a new [child] is set (not for each build), or when a new |
| 181 | /// [transitionBuilder] is set. If a new [transitionBuilder] is set, then |
| 182 | /// the transition is rebuilt for the current child and all previous children |
| 183 | /// using the new [transitionBuilder]. The function must not return null. |
| 184 | /// |
| 185 | /// The default is [AnimatedSwitcher.defaultTransitionBuilder]. |
| 186 | /// |
| 187 | /// The animation provided to the builder has the [duration] and |
| 188 | /// [switchInCurve] or [switchOutCurve] applied as provided when the |
| 189 | /// corresponding [child] was first provided. |
| 190 | /// |
| 191 | /// See also: |
| 192 | /// |
| 193 | /// * [AnimatedSwitcherTransitionBuilder] for more information about |
| 194 | /// how a transition builder should function. |
| 195 | final AnimatedSwitcherTransitionBuilder transitionBuilder; |
| 196 | |
| 197 | /// A function that wraps all of the children that are transitioning out, and |
| 198 | /// the [child] that's transitioning in, with a widget that lays all of them |
| 199 | /// out. This is called every time this widget is built. The function must not |
| 200 | /// return null. |
| 201 | /// |
| 202 | /// The default is [AnimatedSwitcher.defaultLayoutBuilder]. |
| 203 | /// |
| 204 | /// See also: |
| 205 | /// |
| 206 | /// * [AnimatedSwitcherLayoutBuilder] for more information about |
| 207 | /// how a layout builder should function. |
| 208 | final AnimatedSwitcherLayoutBuilder layoutBuilder; |
| 209 | |
| 210 | @override |
| 211 | State<AnimatedSwitcher> createState() => _AnimatedSwitcherState(); |
| 212 | |
| 213 | /// The transition builder used as the default value of [transitionBuilder]. |
| 214 | /// |
| 215 | /// The new child is given a [FadeTransition] which increases opacity as |
| 216 | /// the animation goes from 0.0 to 1.0, and decreases when the animation is |
| 217 | /// reversed. |
| 218 | /// |
| 219 | /// This is an [AnimatedSwitcherTransitionBuilder] function. |
| 220 | static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) { |
| 221 | return FadeTransition(key: ValueKey<Key?>(child.key), opacity: animation, child: child); |
| 222 | } |
| 223 | |
| 224 | /// The layout builder used as the default value of [layoutBuilder]. |
| 225 | /// |
| 226 | /// The new child is placed in a [Stack] that sizes itself to match the |
| 227 | /// largest of the child or a previous child. The children are centered on |
| 228 | /// each other. |
| 229 | /// |
| 230 | /// This is an [AnimatedSwitcherLayoutBuilder] function. |
| 231 | static Widget defaultLayoutBuilder(Widget? currentChild, List<Widget> previousChildren) { |
| 232 | return Stack( |
| 233 | alignment: Alignment.center, |
| 234 | children: <Widget>[...previousChildren, if (currentChild != null) currentChild], |
| 235 | ); |
| 236 | } |
| 237 | |
| 238 | @override |
| 239 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 240 | super.debugFillProperties(properties); |
| 241 | properties.add(IntProperty('duration' , duration.inMilliseconds, unit: 'ms' )); |
| 242 | properties.add( |
| 243 | IntProperty( |
| 244 | 'reverseDuration' , |
| 245 | reverseDuration?.inMilliseconds, |
| 246 | unit: 'ms' , |
| 247 | defaultValue: null, |
| 248 | ), |
| 249 | ); |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProviderStateMixin { |
| 254 | _ChildEntry? _currentEntry; |
| 255 | final Set<_ChildEntry> _outgoingEntries = <_ChildEntry>{}; |
| 256 | List<Widget>? _outgoingWidgets = const <Widget>[]; |
| 257 | int _childNumber = 0; |
| 258 | |
| 259 | @override |
| 260 | void initState() { |
| 261 | super.initState(); |
| 262 | _addEntryForNewChild(animate: false); |
| 263 | } |
| 264 | |
| 265 | @override |
| 266 | void didUpdateWidget(AnimatedSwitcher oldWidget) { |
| 267 | super.didUpdateWidget(oldWidget); |
| 268 | |
| 269 | // If the transition builder changed, then update all of the previous |
| 270 | // transitions. |
| 271 | if (widget.transitionBuilder != oldWidget.transitionBuilder) { |
| 272 | _outgoingEntries.forEach(_updateTransitionForEntry); |
| 273 | if (_currentEntry != null) { |
| 274 | _updateTransitionForEntry(_currentEntry!); |
| 275 | } |
| 276 | _markChildWidgetCacheAsDirty(); |
| 277 | } |
| 278 | |
| 279 | final bool hasNewChild = widget.child != null; |
| 280 | final bool hasOldChild = _currentEntry != null; |
| 281 | if (hasNewChild != hasOldChild || |
| 282 | hasNewChild && !Widget.canUpdate(widget.child!, _currentEntry!.widgetChild)) { |
| 283 | // Child has changed, fade current entry out and add new entry. |
| 284 | _childNumber += 1; |
| 285 | _addEntryForNewChild(animate: true); |
| 286 | } else if (_currentEntry != null) { |
| 287 | assert(hasOldChild && hasNewChild); |
| 288 | assert(Widget.canUpdate(widget.child!, _currentEntry!.widgetChild)); |
| 289 | // Child has been updated. Make sure we update the child widget and |
| 290 | // transition in _currentEntry even though we're not going to start a new |
| 291 | // animation, but keep the key from the previous transition so that we |
| 292 | // update the transition instead of replacing it. |
| 293 | _currentEntry!.widgetChild = widget.child!; |
| 294 | _updateTransitionForEntry(_currentEntry!); // uses entry.widgetChild |
| 295 | _markChildWidgetCacheAsDirty(); |
| 296 | } |
| 297 | } |
| 298 | |
| 299 | void _addEntryForNewChild({required bool animate}) { |
| 300 | assert(animate || _currentEntry == null); |
| 301 | if (_currentEntry != null) { |
| 302 | assert(animate); |
| 303 | assert(!_outgoingEntries.contains(_currentEntry)); |
| 304 | _outgoingEntries.add(_currentEntry!); |
| 305 | _currentEntry!.controller.reverse(); |
| 306 | _markChildWidgetCacheAsDirty(); |
| 307 | _currentEntry = null; |
| 308 | } |
| 309 | if (widget.child == null) { |
| 310 | return; |
| 311 | } |
| 312 | final AnimationController controller = AnimationController( |
| 313 | duration: widget.duration, |
| 314 | reverseDuration: widget.reverseDuration, |
| 315 | vsync: this, |
| 316 | ); |
| 317 | final CurvedAnimation animation = CurvedAnimation( |
| 318 | parent: controller, |
| 319 | curve: widget.switchInCurve, |
| 320 | reverseCurve: widget.switchOutCurve, |
| 321 | ); |
| 322 | _currentEntry = _newEntry( |
| 323 | child: widget.child!, |
| 324 | controller: controller, |
| 325 | animation: animation, |
| 326 | builder: widget.transitionBuilder, |
| 327 | ); |
| 328 | if (animate) { |
| 329 | controller.forward(); |
| 330 | } else { |
| 331 | assert(_outgoingEntries.isEmpty); |
| 332 | controller.value = 1.0; |
| 333 | } |
| 334 | } |
| 335 | |
| 336 | _ChildEntry _newEntry({ |
| 337 | required Widget child, |
| 338 | required AnimatedSwitcherTransitionBuilder builder, |
| 339 | required AnimationController controller, |
| 340 | required CurvedAnimation animation, |
| 341 | }) { |
| 342 | final _ChildEntry entry = _ChildEntry( |
| 343 | widgetChild: child, |
| 344 | transition: KeyedSubtree.wrap(builder(child, animation), _childNumber), |
| 345 | animation: animation, |
| 346 | controller: controller, |
| 347 | ); |
| 348 | animation.addStatusListener((AnimationStatus status) { |
| 349 | if (status.isDismissed) { |
| 350 | setState(() { |
| 351 | assert(mounted); |
| 352 | assert(_outgoingEntries.contains(entry)); |
| 353 | _outgoingEntries.remove(entry); |
| 354 | _markChildWidgetCacheAsDirty(); |
| 355 | }); |
| 356 | controller.dispose(); |
| 357 | animation.dispose(); |
| 358 | } |
| 359 | }); |
| 360 | return entry; |
| 361 | } |
| 362 | |
| 363 | void _markChildWidgetCacheAsDirty() { |
| 364 | _outgoingWidgets = null; |
| 365 | } |
| 366 | |
| 367 | void _updateTransitionForEntry(_ChildEntry entry) { |
| 368 | entry.transition = KeyedSubtree( |
| 369 | key: entry.transition.key, |
| 370 | child: widget.transitionBuilder(entry.widgetChild, entry.animation), |
| 371 | ); |
| 372 | } |
| 373 | |
| 374 | void _rebuildOutgoingWidgetsIfNeeded() { |
| 375 | _outgoingWidgets ??= List<Widget>.unmodifiable( |
| 376 | _outgoingEntries.map<Widget>((_ChildEntry entry) => entry.transition), |
| 377 | ); |
| 378 | assert(_outgoingEntries.length == _outgoingWidgets!.length); |
| 379 | assert(_outgoingEntries.isEmpty || _outgoingEntries.last.transition == _outgoingWidgets!.last); |
| 380 | } |
| 381 | |
| 382 | @override |
| 383 | void dispose() { |
| 384 | _currentEntry?.controller.dispose(); |
| 385 | _currentEntry?.animation.dispose(); |
| 386 | for (final _ChildEntry entry in _outgoingEntries) { |
| 387 | entry.controller.dispose(); |
| 388 | entry.animation.dispose(); |
| 389 | } |
| 390 | super.dispose(); |
| 391 | } |
| 392 | |
| 393 | @override |
| 394 | Widget build(BuildContext context) { |
| 395 | _rebuildOutgoingWidgetsIfNeeded(); |
| 396 | return widget.layoutBuilder( |
| 397 | _currentEntry?.transition, |
| 398 | _outgoingWidgets! |
| 399 | .where((Widget outgoing) => outgoing.key != _currentEntry?.transition.key) |
| 400 | .toSet() |
| 401 | .toList(), |
| 402 | ); |
| 403 | } |
| 404 | } |
| 405 | |