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
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7
import 'package:flutter/animation.dart';
8
import 'package:flutter/foundation.dart';
9 10 11 12 13 14

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

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

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

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

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

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

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

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

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

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

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

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

187 188 189 190 191 192 193 194 195
  /// 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;

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

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

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

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

  @override
254
  _AnimatedSwitcherState createState() => _AnimatedSwitcherState();
255

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

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

  @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));
  }
293 294
}

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

  @override
  void initState() {
    super.initState();
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
    _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();
    }
  }

340
  void _addEntryForNewChild({ @required bool animate }) {
341 342 343 344 345 346 347 348 349 350 351 352 353
    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,
354
      reverseDuration: widget.reverseDuration,
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
      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;
    }
374 375
  }

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

402
  void _markChildWidgetCacheAsDirty() {
403
    _outgoingWidgets = null;
404 405
  }

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

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

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

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