animated_switcher.dart 16.2 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 66

/// A widget that by default does a [FadeTransition] between a new widget and
67
/// 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.
143 144 145
///  * [FadeTransition] which [AnimatedSwitcher] uses to perform the transition.
class AnimatedSwitcher extends StatefulWidget {
  /// Creates an [AnimatedSwitcher].
146 147 148
  ///
  /// The [duration], [transitionBuilder], [layoutBuilder], [switchInCurve], and
  /// [switchOutCurve] parameters must not be null.
149
  const AnimatedSwitcher({
150 151
    Key key,
    this.child,
152
    @required this.duration,
153
    this.reverseDuration,
154 155 156 157
    this.switchInCurve = Curves.linear,
    this.switchOutCurve = Curves.linear,
    this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,
    this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,
158 159 160 161 162 163
  }) : assert(duration != null),
       assert(switchInCurve != null),
       assert(switchOutCurve != null),
       assert(transitionBuilder != null),
       assert(layoutBuilder != null),
       super(key: key);
164

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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