animated_switcher.dart 16.4 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 19
class _ChildEntry {
  _ChildEntry({
    @required this.controller,
20 21 22
    @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
/// {@tool sample}
91 92 93 94 95 96
///
/// ```dart
/// class ClickCounter extends StatefulWidget {
///   const ClickCounter({Key key}) : super(key: key);
///
///   @override
97
///   _ClickCounterState createState() => _ClickCounterState();
98 99 100 101 102 103 104
/// }
///
/// class _ClickCounterState extends State<ClickCounter> {
///   int _count = 0;
///
///   @override
///   Widget build(BuildContext context) {
105 106
///     return MaterialApp(
///       home: Material(
107 108 109
///         child: Column(
///           mainAxisAlignment: MainAxisAlignment.center,
///           children: <Widget>[
110
///             AnimatedSwitcher(
111 112
///               duration: const Duration(milliseconds: 500),
///               transitionBuilder: (Widget child, Animation<double> animation) {
113
///                 return ScaleTransition(child: child, scale: animation);
114
///               },
115
///               child: Text(
116 117 118 119
///                 '$_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.
120
///                 key: ValueKey<int>(_count),
121 122 123
///                 style: Theme.of(context).textTheme.display1,
///               ),
///             ),
124
///             RaisedButton(
125 126 127 128 129 130
///               child: const Text('Increment'),
///               onPressed: () {
///                 setState(() {
///                   _count += 1;
///                 });
///               },
131
///             ),
132 133
///           ],
///         ),
134 135 136 137 138
///       ),
///     );
///   }
/// }
/// ```
139
/// {@end-tool}
140 141 142 143 144
///
/// See also:
///
///  * [AnimatedCrossFade], which only fades between two children, but also
///    interpolates their sizes, and is reversible.
Ian Hickson's avatar
Ian Hickson committed
145 146 147
///  * [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.
148 149
class AnimatedSwitcher extends StatefulWidget {
  /// Creates an [AnimatedSwitcher].
150 151 152
  ///
  /// The [duration], [transitionBuilder], [layoutBuilder], [switchInCurve], and
  /// [switchOutCurve] parameters must not be null.
153
  const AnimatedSwitcher({
154 155
    Key key,
    this.child,
156
    @required this.duration,
157
    this.reverseDuration,
158 159 160 161
    this.switchInCurve = Curves.linear,
    this.switchOutCurve = Curves.linear,
    this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,
    this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,
162 163 164 165 166 167
  }) : assert(duration != null),
       assert(switchInCurve != null),
       assert(switchOutCurve != null),
       assert(transitionBuilder != null),
       assert(layoutBuilder != null),
       super(key: key);
168

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

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

190 191 192 193 194 195 196 197 198
  /// 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;

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

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

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

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

  @override
257
  _AnimatedSwitcherState createState() => _AnimatedSwitcherState();
258

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

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

  @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));
  }
296 297
}

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

  @override
  void initState() {
    super.initState();
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 340 341 342
    _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();
    }
  }

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

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

405
  void _markChildWidgetCacheAsDirty() {
406
    _outgoingWidgets = null;
407 408
  }

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

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

  @override
  void dispose() {
426 427 428 429
    if (_currentEntry != null)
      _currentEntry.controller.dispose();
    for (_ChildEntry entry in _outgoingEntries)
      entry.controller.dispose();
430 431 432
    super.dispose();
  }

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