animated_cross_fade.dart 14.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/rendering.dart';

import 'animated_size.dart';
import 'basic.dart';
9
import 'focus_scope.dart';
10
import 'framework.dart';
11
import 'ticker_provider.dart';
12 13
import 'transitions.dart';

14
// Examples can assume:
15
// bool _first = false;
16

17
/// Specifies which of two children to show. See [AnimatedCrossFade].
18 19 20
///
/// The child that is shown will fade in, while the other will fade out.
enum CrossFadeState {
21 22
  /// Show the first child ([AnimatedCrossFade.firstChild]) and hide the second
  /// ([AnimatedCrossFade.secondChild]]).
23
  showFirst,
24

25 26 27
  /// Show the second child ([AnimatedCrossFade.secondChild]) and hide the first
  /// ([AnimatedCrossFade.firstChild]).
  showSecond,
28 29
}

30 31 32 33 34 35 36 37 38 39 40 41
/// Signature for the [AnimatedCrossFade.layoutBuilder] callback.
///
/// The `topChild` is the child fading in, which is normally drawn on top. The
/// `bottomChild` is the child fading out, normally drawn on the bottom.
///
/// For good performance, the returned widget tree should contain both the
/// `topChild` and the `bottomChild`; the depth of the tree, and the types of
/// the widgets in the tree, from the returned widget to each of the children
/// should be the same; and where there is a widget with multiple children, the
/// top child and the bottom child should be keyed using the provided
/// `topChildKey` and `bottomChildKey` keys respectively.
///
42
/// {@tool snippet}
43 44 45
///
/// ```dart
/// Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) {
46
///   return Stack(
47
///     children: <Widget>[
48
///       Positioned(
49 50 51 52 53 54
///         key: bottomChildKey,
///         left: 0.0,
///         top: 0.0,
///         right: 0.0,
///         child: bottomChild,
///       ),
55
///       Positioned(
56 57 58 59 60 61 62
///         key: topChildKey,
///         child: topChild,
///       )
///     ],
///   );
/// }
/// ```
63
/// {@end-tool}
64
typedef AnimatedCrossFadeBuilder = Widget Function(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey);
65

66 67 68
/// A widget that cross-fades between two given children and animates itself
/// between their sizes.
///
69 70
/// {@youtube 560 315 https://www.youtube.com/watch?v=PGK2UUAyE54}
///
71 72
/// The animation is controlled through the [crossFadeState] parameter.
/// [firstCurve] and [secondCurve] represent the opacity curves of the two
73 74
/// children. The [firstCurve] is inverted, i.e. it fades out when providing a
/// growing curve like [Curves.linear]. The [sizeCurve] is the curve used to
75 76
/// animate between the size of the fading-out child and the size of the
/// fading-in child.
77 78 79 80 81
///
/// This widget is intended to be used to fade a pair of widgets with the same
/// width. In the case where the two children have different heights, the
/// animation crops overflowing children during the animation by aligning their
/// top edge, which means that the bottom will be clipped.
82
///
83 84 85 86
/// The animation is automatically triggered when an existing
/// [AnimatedCrossFade] is rebuilt with a different value for the
/// [crossFadeState] property.
///
87
/// {@tool snippet}
88 89
///
/// This code fades between two representations of the Flutter logo. It depends
90
/// on a boolean field `_first`; when `_first` is true, the first logo is shown,
91 92 93
/// otherwise the second logo is shown. When the field changes state, the
/// [AnimatedCrossFade] widget cross-fades between the two forms of the logo
/// over three seconds.
94 95
///
/// ```dart
96
/// AnimatedCrossFade(
97 98 99
///   duration: const Duration(seconds: 3),
///   firstChild: const FlutterLogo(style: FlutterLogoStyle.horizontal, size: 100.0),
///   secondChild: const FlutterLogo(style: FlutterLogoStyle.stacked, size: 100.0),
100
///   crossFadeState: _first ? CrossFadeState.showFirst : CrossFadeState.showSecond,
101 102
/// )
/// ```
103
/// {@end-tool}
104 105 106
///
/// See also:
///
Ian Hickson's avatar
Ian Hickson committed
107 108 109
///  * [AnimatedOpacity], which fades between nothing and a single child.
///  * [AnimatedSwitcher], which switches out a child for a new one with a
///    customizable transition, supporting multiple cross-fades at once.
110 111
///  * [AnimatedSize], the lower-level widget which [AnimatedCrossFade] uses to
///    automatically change size.
112
class AnimatedCrossFade extends StatefulWidget {
113
  /// Creates a cross-fade animation widget.
114 115 116 117
  ///
  /// The [duration] of the animation is the same for all components (fade in,
  /// fade out, and size), and you can pass [Interval]s instead of [Curve]s in
  /// order to have finer control, e.g., creating an overlap between the fades.
118 119
  ///
  /// All the arguments other than [key] must be non-null.
120
  const AnimatedCrossFade({
121
    super.key,
122 123
    required this.firstChild,
    required this.secondChild,
124 125 126 127
    this.firstCurve = Curves.linear,
    this.secondCurve = Curves.linear,
    this.sizeCurve = Curves.linear,
    this.alignment = Alignment.topCenter,
128 129
    required this.crossFadeState,
    required this.duration,
130
    this.reverseDuration,
131
    this.layoutBuilder = defaultLayoutBuilder,
132
    this.excludeBottomFocus = true,
133 134 135
  }) : assert(firstChild != null),
       assert(secondChild != null),
       assert(firstCurve != null),
136 137
       assert(secondCurve != null),
       assert(sizeCurve != null),
138 139 140 141
       assert(alignment != null),
       assert(crossFadeState != null),
       assert(duration != null),
       assert(layoutBuilder != null),
142
       assert(excludeBottomFocus != null);
143

Ian Hickson's avatar
Ian Hickson committed
144 145 146
  /// The child that is visible when [crossFadeState] is
  /// [CrossFadeState.showFirst]. It fades out when transitioning
  /// [crossFadeState] from [CrossFadeState.showFirst] to
147
  /// [CrossFadeState.showSecond] and vice versa.
148 149
  final Widget firstChild;

Ian Hickson's avatar
Ian Hickson committed
150 151 152
  /// The child that is visible when [crossFadeState] is
  /// [CrossFadeState.showSecond]. It fades in when transitioning
  /// [crossFadeState] from [CrossFadeState.showFirst] to
153
  /// [CrossFadeState.showSecond] and vice versa.
154 155
  final Widget secondChild;

156
  /// The child that will be shown when the animation has completed.
157 158 159 160 161
  final CrossFadeState crossFadeState;

  /// The duration of the whole orchestrated animation.
  final Duration duration;

162 163 164
  /// The duration of the whole orchestrated animation when running in reverse.
  ///
  /// If not supplied, this defaults to [duration].
165
  final Duration? reverseDuration;
166

167
  /// The fade curve of the first child.
168 169
  ///
  /// Defaults to [Curves.linear].
170 171 172
  final Curve firstCurve;

  /// The fade curve of the second child.
173 174
  ///
  /// Defaults to [Curves.linear].
175 176 177
  final Curve secondCurve;

  /// The curve of the animation between the two children's sizes.
178 179
  ///
  /// Defaults to [Curves.linear].
180 181
  final Curve sizeCurve;

182 183
  /// How the children should be aligned while the size is animating.
  ///
184
  /// Defaults to [Alignment.topCenter].
185 186 187 188 189 190 191
  ///
  /// See also:
  ///
  ///  * [Alignment], a class with convenient constants typically used to
  ///    specify an [AlignmentGeometry].
  ///  * [AlignmentDirectional], like [Alignment] for specifying alignments
  ///    relative to text direction.
192
  final AlignmentGeometry alignment;
193

194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
  /// A builder that positions the [firstChild] and [secondChild] widgets.
  ///
  /// The widget returned by this method is wrapped in an [AnimatedSize].
  ///
  /// By default, this uses [AnimatedCrossFade.defaultLayoutBuilder], which uses
  /// a [Stack] and aligns the `bottomChild` to the top of the stack while
  /// providing the `topChild` as the non-positioned child to fill the provided
  /// constraints. This works well when the [AnimatedCrossFade] is in a position
  /// to change size and when the children are not flexible. However, if the
  /// children are less fussy about their sizes (for example a
  /// [CircularProgressIndicator] inside a [Center]), or if the
  /// [AnimatedCrossFade] is being forced to a particular size, then it can
  /// result in the widgets jumping about when the cross-fade state is changed.
  final AnimatedCrossFadeBuilder layoutBuilder;

209 210 211 212 213 214 215 216 217
  /// When true, this is equivalent to wrapping the bottom widget with an [ExcludeFocus]
  /// widget while it is at the bottom of the cross-fade stack.
  ///
  /// Defaults to true. When it is false, the bottom widget in the cross-fade stack
  /// can remain in focus until the top widget requests focus. This is useful for
  /// animating between different [TextField]s so the keyboard remains open during the
  /// cross-fade animation.
  final bool excludeBottomFocus;

218 219 220 221 222 223 224 225 226 227
  /// The default layout algorithm used by [AnimatedCrossFade].
  ///
  /// The top child is placed in a stack that sizes itself to match the top
  /// child. The bottom child is positioned at the top of the same stack, sized
  /// to fit its width but without forcing the height. The stack is then
  /// clipped.
  ///
  /// This is the default value for [layoutBuilder]. It implements
  /// [AnimatedCrossFadeBuilder].
  static Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) {
228
    return Stack(
229
      clipBehavior: Clip.none,
230
      children: <Widget>[
231
        Positioned(
232 233 234 235 236 237
          key: bottomChildKey,
          left: 0.0,
          top: 0.0,
          right: 0.0,
          child: bottomChild,
        ),
238
        Positioned(
239 240
          key: topChildKey,
          child: topChild,
241
        ),
242 243 244 245
      ],
    );
  }

246
  @override
247
  State<AnimatedCrossFade> createState() => _AnimatedCrossFadeState();
248 249

  @override
250 251
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
252 253
    properties.add(EnumProperty<CrossFadeState>('crossFadeState', crossFadeState));
    properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: Alignment.topCenter));
254 255
    properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms'));
    properties.add(IntProperty('reverseDuration', reverseDuration?.inMilliseconds, unit: 'ms', defaultValue: null));
256
  }
257 258
}

259
class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProviderStateMixin {
260
  late AnimationController _controller;
261 262
  late Animation<double> _firstAnimation;
  late Animation<double> _secondAnimation;
263

264 265 266
  @override
  void initState() {
    super.initState();
267 268 269 270 271
    _controller = AnimationController(
      duration: widget.duration,
      reverseDuration: widget.reverseDuration,
      vsync: this,
    );
272
    if (widget.crossFadeState == CrossFadeState.showSecond) {
273
      _controller.value = 1.0;
274
    }
275 276
    _firstAnimation = _initAnimation(widget.firstCurve, true);
    _secondAnimation = _initAnimation(widget.secondCurve, false);
277
    _controller.addStatusListener((AnimationStatus status) {
278 279 280 281 282
      setState(() {
        // Trigger a rebuild because it depends on _isTransitioning, which
        // changes its value together with animation status.
      });
    });
283
  }
284

285
  Animation<double> _initAnimation(Curve curve, bool inverted) {
286
    Animation<double> result = _controller.drive(CurveTween(curve: curve));
287
    if (inverted) {
288
      result = result.drive(Tween<double>(begin: 1.0, end: 0.0));
289
    }
290
    return result;
291 292 293 294
  }

  @override
  void dispose() {
295
    _controller.dispose();
296 297 298 299
    super.dispose();
  }

  @override
300 301
  void didUpdateWidget(AnimatedCrossFade oldWidget) {
    super.didUpdateWidget(oldWidget);
302
    if (widget.duration != oldWidget.duration) {
303
      _controller.duration = widget.duration;
304 305
    }
    if (widget.reverseDuration != oldWidget.reverseDuration) {
306
      _controller.reverseDuration = widget.reverseDuration;
307 308
    }
    if (widget.firstCurve != oldWidget.firstCurve) {
309
      _firstAnimation = _initAnimation(widget.firstCurve, true);
310 311
    }
    if (widget.secondCurve != oldWidget.secondCurve) {
312
      _secondAnimation = _initAnimation(widget.secondCurve, false);
313
    }
314 315
    if (widget.crossFadeState != oldWidget.crossFadeState) {
      switch (widget.crossFadeState) {
316
        case CrossFadeState.showFirst:
317
          _controller.reverse();
318 319
          break;
        case CrossFadeState.showSecond:
320
          _controller.forward();
321 322 323 324 325
          break;
      }
    }
  }

326
  /// Whether we're in the middle of cross-fading this frame.
327
  bool get _isTransitioning => _controller.status == AnimationStatus.forward || _controller.status == AnimationStatus.reverse;
328

329 330
  @override
  Widget build(BuildContext context) {
331 332
    const Key kFirstChildKey = ValueKey<CrossFadeState>(CrossFadeState.showFirst);
    const Key kSecondChildKey = ValueKey<CrossFadeState>(CrossFadeState.showSecond);
333 334
    final bool transitioningForwards = _controller.status == AnimationStatus.completed ||
                                       _controller.status == AnimationStatus.forward;
335
    final Key topKey;
336
    Widget topChild;
337 338
    final Animation<double> topAnimation;
    final Key bottomKey;
339
    Widget bottomChild;
340
    final Animation<double> bottomAnimation;
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
    if (transitioningForwards) {
      topKey = kSecondChildKey;
      topChild = widget.secondChild;
      topAnimation = _secondAnimation;
      bottomKey = kFirstChildKey;
      bottomChild = widget.firstChild;
      bottomAnimation = _firstAnimation;
    } else {
      topKey = kFirstChildKey;
      topChild = widget.firstChild;
      topAnimation = _firstAnimation;
      bottomKey = kSecondChildKey;
      bottomChild = widget.secondChild;
      bottomAnimation = _secondAnimation;
    }

357
    bottomChild = TickerMode(
358 359
      key: bottomKey,
      enabled: _isTransitioning,
360 361 362
      child: IgnorePointer(
        child: ExcludeSemantics( // Always exclude the semantics of the widget that's fading out.
          child: ExcludeFocus(
363
            excluding: widget.excludeBottomFocus,
364 365 366 367
            child: FadeTransition(
              opacity: bottomAnimation,
              child: bottomChild,
            ),
368
          ),
369
        ),
370
      ),
371
    );
372
    topChild = TickerMode(
373 374
      key: topKey,
      enabled: true, // Top widget always has its animations enabled.
375 376 377 378 379 380 381 382 383 384
      child: IgnorePointer(
        ignoring: false,
        child: ExcludeSemantics(
          excluding: false, // Always publish semantics for the widget that's fading in.
          child: ExcludeFocus(
            excluding: false,
            child: FadeTransition(
              opacity: topAnimation,
              child: topChild,
            ),
385
          ),
386
        ),
387
      ),
388
    );
389 390
    return ClipRect(
      child: AnimatedSize(
391
        alignment: widget.alignment,
392
        duration: widget.duration,
393
        reverseDuration: widget.reverseDuration,
394
        curve: widget.sizeCurve,
395
        child: widget.layoutBuilder(topChild, topKey, bottomChild, bottomKey),
396
      ),
397 398
    );
  }
399 400

  @override
401
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
402
    super.debugFillProperties(description);
403 404 405
    description.add(EnumProperty<CrossFadeState>('crossFadeState', widget.crossFadeState));
    description.add(DiagnosticsProperty<AnimationController>('controller', _controller, showName: false));
    description.add(DiagnosticsProperty<AlignmentGeometry>('alignment', widget.alignment, defaultValue: Alignment.topCenter));
406
  }
407
}