animated_switcher.dart 16.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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';
6
import 'package:flutter/foundation.dart';
7 8 9 10 11 12

import 'basic.dart';
import 'framework.dart';
import 'ticker_provider.dart';
import 'transitions.dart';

13
// Internal representation of a child that, now or in the past, was set on the
14
// AnimatedSwitcher.child field, but is now in the process of
15 16
// transitioning. The internal representation includes fields that we don't want
// to expose to the public API (like the controller).
17 18
class _ChildEntry {
  _ChildEntry({
19 20 21 22
    required this.controller,
    required this.animation,
    required this.transition,
    required this.widgetChild,
23 24 25
  }) : assert(animation != null),
       assert(transition != null),
       assert(controller != null);
26

27 28 29 30
  // The animation controller for the child's transition.
  final AnimationController controller;

  // The (curved) animation being used to drive the transition.
31 32 33 34
  final Animation<double> animation;

  // The currently built transition for this child.
  Widget transition;
35

36
  // The widget's child at the time this entry was created or updated.
37
  // Used to rebuild the transition if necessary.
38
  Widget widgetChild;
39 40 41

  @override
  String toString() => 'Entry#${shortHash(this)}($widgetChild)';
42 43
}

44
/// Signature for builders used to generate custom transitions for
45
/// [AnimatedSwitcher].
46
///
47
/// The `child` should be transitioning in when the `animation` is running in
48 49
/// the forward direction.
///
50 51
/// 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.
52
typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation<double> animation);
53 54

/// Signature for builders used to generate custom layouts for
55
/// [AnimatedSwitcher].
56
///
57 58 59 60 61 62 63
/// 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`.
64
typedef AnimatedSwitcherLayoutBuilder = Widget Function(Widget? currentChild, List<Widget> previousChildren);
65

Ian Hickson's avatar
Ian Hickson committed
66 67
/// A widget that by default does a cross-fade between a new widget and the
/// widget previously set on the [AnimatedSwitcher] as a child.
68
///
69 70
/// {@youtube 560 315 https://www.youtube.com/watch?v=2W7POjFb88g}
///
71 72 73 74
/// 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.
///
75 76
/// 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
77
/// transition between them, since as far as the framework is concerned, they
78 79 80 81 82 83 84 85 86 87 88
/// 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.)
89
///
90 91
/// The type of transition can be changed from a cross-fade to a custom
/// transition by setting the [transitionBuilder].
92
///
93 94 95
/// {@tool dartpad --template=stateful_widget_material}
/// This sample shows a counter that animates the scale of a text widget
/// whenever the value changes.
96
///
97 98
/// ```dart
/// int _count = 0;
99
///
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
/// @override
/// Widget build(BuildContext context) {
///   return Container(
///     color: Colors.white,
///     child: Column(
///       mainAxisAlignment: MainAxisAlignment.center,
///       children: <Widget>[
///         AnimatedSwitcher(
///           duration: const Duration(milliseconds: 500),
///           transitionBuilder: (Widget child, Animation<double> animation) {
///             return ScaleTransition(child: child, scale: animation);
///           },
///           child: Text(
///             '$_count',
///             // This key causes the AnimatedSwitcher to interpret this as a "new"
///             // child each time the count changes, so that it will begin its animation
///             // when the count changes.
///             key: ValueKey<int>(_count),
///             style: Theme.of(context).textTheme.headline4,
///           ),
///         ),
121
///         ElevatedButton(
122 123 124 125 126 127
///           child: const Text('Increment'),
///           onPressed: () {
///             setState(() {
///               _count += 1;
///             });
///           },
128
///         ),
129 130 131
///       ],
///     ),
///   );
132 133
/// }
/// ```
134
/// {@end-tool}
135 136 137 138 139
///
/// See also:
///
///  * [AnimatedCrossFade], which only fades between two children, but also
///    interpolates their sizes, and is reversible.
Ian Hickson's avatar
Ian Hickson committed
140 141 142
///  * [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.
143 144
class AnimatedSwitcher extends StatefulWidget {
  /// Creates an [AnimatedSwitcher].
145 146 147
  ///
  /// The [duration], [transitionBuilder], [layoutBuilder], [switchInCurve], and
  /// [switchOutCurve] parameters must not be null.
148
  const AnimatedSwitcher({
149
    Key? key,
150
    this.child,
151
    required this.duration,
152
    this.reverseDuration,
153 154 155 156
    this.switchInCurve = Curves.linear,
    this.switchOutCurve = Curves.linear,
    this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,
    this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,
157 158 159 160 161 162
  }) : assert(duration != null),
       assert(switchInCurve != null),
       assert(switchOutCurve != null),
       assert(transitionBuilder != null),
       assert(layoutBuilder != null),
       super(key: key);
163

164 165 166 167 168 169
  /// 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].
170
  ///
171 172 173 174
  /// 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].
175
  final Widget? child;
176

177
  /// The duration of the transition from the old [child] value to the new one.
178 179
  ///
  /// This duration is applied to the given [child] when that property is set to
180 181 182
  /// 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.
183 184
  final Duration duration;

185 186 187 188 189 190 191
  /// 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.
192
  final Duration? reverseDuration;
193

194 195 196 197 198 199 200 201 202 203
  /// 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].
204 205
  final Curve switchInCurve;

206 207 208 209 210 211 212 213 214 215
  /// 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].
216 217
  final Curve switchOutCurve;

218
  /// A function that wraps a new [child] with an animation that transitions
219
  /// the [child] in when the animation runs in the forward direction and out
220 221 222 223 224
  /// 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.
225
  ///
226
  /// The default is [AnimatedSwitcher.defaultTransitionBuilder].
227
  ///
228 229 230 231
  /// The animation provided to the builder has the [duration] and
  /// [switchInCurve] or [switchOutCurve] applied as provided when the
  /// corresponding [child] was first provided.
  ///
232 233
  /// See also:
  ///
234
  ///  * [AnimatedSwitcherTransitionBuilder] for more information about
235
  ///    how a transition builder should function.
236
  final AnimatedSwitcherTransitionBuilder transitionBuilder;
237 238 239

  /// 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
240 241
  /// out. This is called every time this widget is built. The function must not
  /// return null.
242
  ///
243
  /// The default is [AnimatedSwitcher.defaultLayoutBuilder].
244 245 246
  ///
  /// See also:
  ///
247
  ///  * [AnimatedSwitcherLayoutBuilder] for more information about
248
  ///    how a layout builder should function.
249
  final AnimatedSwitcherLayoutBuilder layoutBuilder;
250 251

  @override
252
  _AnimatedSwitcherState createState() => _AnimatedSwitcherState();
253

254
  /// The transition builder used as the default value of [transitionBuilder].
255 256 257 258 259
  ///
  /// 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.
  ///
260
  /// This is an [AnimatedSwitcherTransitionBuilder] function.
261
  static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
262
    return FadeTransition(
263 264 265 266 267
      opacity: animation,
      child: child,
    );
  }

268
  /// The layout builder used as the default value of [layoutBuilder].
269 270 271 272 273
  ///
  /// 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.
  ///
274
  /// This is an [AnimatedSwitcherLayoutBuilder] function.
275
  static Widget defaultLayoutBuilder(Widget? currentChild, List<Widget> previousChildren) {
276
    return Stack(
277 278 279 280
      children: <Widget>[
        ...previousChildren,
        if (currentChild != null) currentChild,
      ],
281 282 283
      alignment: Alignment.center,
    );
  }
284 285 286 287 288 289 290

  @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));
  }
291 292
}

293
class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProviderStateMixin {
294
  _ChildEntry? _currentEntry;
295
  final Set<_ChildEntry> _outgoingEntries = <_ChildEntry>{};
296
  List<Widget>? _outgoingWidgets = const <Widget>[];
297
  int _childNumber = 0;
298 299 300 301

  @override
  void initState() {
    super.initState();
302 303 304 305 306 307 308 309 310 311 312 313
    _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)
314
        _updateTransitionForEntry(_currentEntry!);
315 316 317 318 319 320
      _markChildWidgetCacheAsDirty();
    }

    final bool hasNewChild = widget.child != null;
    final bool hasOldChild = _currentEntry != null;
    if (hasNewChild != hasOldChild ||
321
        hasNewChild && !Widget.canUpdate(widget.child!, _currentEntry!.widgetChild)) {
322 323 324 325 326
      // Child has changed, fade current entry out and add new entry.
      _childNumber += 1;
      _addEntryForNewChild(animate: true);
    } else if (_currentEntry != null) {
      assert(hasOldChild && hasNewChild);
327
      assert(Widget.canUpdate(widget.child!, _currentEntry!.widgetChild));
328 329 330 331
      // 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.
332 333
      _currentEntry!.widgetChild = widget.child!;
      _updateTransitionForEntry(_currentEntry!); // uses entry.widgetChild
334 335 336 337
      _markChildWidgetCacheAsDirty();
    }
  }

338
  void _addEntryForNewChild({ required bool animate }) {
339 340 341 342
    assert(animate || _currentEntry == null);
    if (_currentEntry != null) {
      assert(animate);
      assert(!_outgoingEntries.contains(_currentEntry));
343 344
      _outgoingEntries.add(_currentEntry!);
      _currentEntry!.controller.reverse();
345 346 347 348 349 350 351
      _markChildWidgetCacheAsDirty();
      _currentEntry = null;
    }
    if (widget.child == null)
      return;
    final AnimationController controller = AnimationController(
      duration: widget.duration,
352
      reverseDuration: widget.reverseDuration,
353 354 355 356 357 358 359 360
      vsync: this,
    );
    final Animation<double> animation = CurvedAnimation(
      parent: controller,
      curve: widget.switchInCurve,
      reverseCurve: widget.switchOutCurve,
    );
    _currentEntry = _newEntry(
361
      child: widget.child!,
362 363 364 365 366 367 368 369 370 371
      controller: controller,
      animation: animation,
      builder: widget.transitionBuilder,
    );
    if (animate) {
      controller.forward();
    } else {
      assert(_outgoingEntries.isEmpty);
      controller.value = 1.0;
    }
372 373
  }

374
  _ChildEntry _newEntry({
375 376 377 378
    required Widget child,
    required AnimatedSwitcherTransitionBuilder builder,
    required AnimationController controller,
    required Animation<double> animation,
379
  }) {
380 381 382
    final _ChildEntry entry = _ChildEntry(
      widgetChild: child,
      transition: KeyedSubtree.wrap(builder(child, animation), _childNumber),
383 384 385 386 387 388
      animation: animation,
      controller: controller,
    );
    animation.addStatusListener((AnimationStatus status) {
      if (status == AnimationStatus.dismissed) {
        setState(() {
389 390 391 392
          assert(mounted);
          assert(_outgoingEntries.contains(entry));
          _outgoingEntries.remove(entry);
          _markChildWidgetCacheAsDirty();
393 394 395 396 397 398 399
        });
        controller.dispose();
      }
    });
    return entry;
  }

400
  void _markChildWidgetCacheAsDirty() {
401
    _outgoingWidgets = null;
402 403
  }

404 405 406 407
  void _updateTransitionForEntry(_ChildEntry entry) {
    entry.transition = KeyedSubtree(
      key: entry.transition.key,
      child: widget.transitionBuilder(entry.widgetChild, entry.animation),
408
    );
409 410 411 412 413
  }

  void _rebuildOutgoingWidgetsIfNeeded() {
    _outgoingWidgets ??= List<Widget>.unmodifiable(
      _outgoingEntries.map<Widget>((_ChildEntry entry) => entry.transition),
414
    );
415 416
    assert(_outgoingEntries.length == _outgoingWidgets!.length);
    assert(_outgoingEntries.isEmpty || _outgoingEntries.last.transition == _outgoingWidgets!.last);
417 418 419 420
  }

  @override
  void dispose() {
421
    if (_currentEntry != null)
422
      _currentEntry!.controller.dispose();
423
    for (final _ChildEntry entry in _outgoingEntries)
424
      entry.controller.dispose();
425 426 427
    super.dispose();
  }

428 429
  @override
  Widget build(BuildContext context) {
430
    _rebuildOutgoingWidgetsIfNeeded();
431
    return widget.layoutBuilder(_currentEntry?.transition, _outgoingWidgets!);
432 433
  }
}