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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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