1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/material.dart';
6///
7/// @docImport 'animated_switcher.dart';
8/// @docImport 'implicit_animations.dart';
9library;
10
11import 'package:flutter/rendering.dart';
12
13import 'animated_size.dart';
14import 'basic.dart';
15import 'focus_scope.dart';
16import 'framework.dart';
17import 'ticker_provider.dart';
18import 'transitions.dart';
19
20// Examples can assume:
21// bool _first = false;
22
23/// Specifies which of two children to show. See [AnimatedCrossFade].
24///
25/// The child that is shown will fade in, while the other will fade out.
26enum CrossFadeState {
27 /// Show the first child ([AnimatedCrossFade.firstChild]) and hide the second
28 /// ([AnimatedCrossFade.secondChild]]).
29 showFirst,
30
31 /// Show the second child ([AnimatedCrossFade.secondChild]) and hide the first
32 /// ([AnimatedCrossFade.firstChild]).
33 showSecond,
34}
35
36/// Signature for the [AnimatedCrossFade.layoutBuilder] callback.
37///
38/// The `topChild` is the child fading in, which is normally drawn on top. The
39/// `bottomChild` is the child fading out, normally drawn on the bottom.
40///
41/// For good performance, the returned widget tree should contain both the
42/// `topChild` and the `bottomChild`; the depth of the tree, and the types of
43/// the widgets in the tree, from the returned widget to each of the children
44/// should be the same; and where there is a widget with multiple children, the
45/// top child and the bottom child should be keyed using the provided
46/// `topChildKey` and `bottomChildKey` keys respectively.
47///
48/// {@tool snippet}
49///
50/// ```dart
51/// Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) {
52/// return Stack(
53/// children: <Widget>[
54/// Positioned(
55/// key: bottomChildKey,
56/// left: 0.0,
57/// top: 0.0,
58/// right: 0.0,
59/// child: bottomChild,
60/// ),
61/// Positioned(
62/// key: topChildKey,
63/// child: topChild,
64/// )
65/// ],
66/// );
67/// }
68/// ```
69/// {@end-tool}
70typedef AnimatedCrossFadeBuilder = Widget Function(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey);
71
72/// A widget that cross-fades between two given children and animates itself
73/// between their sizes.
74///
75/// {@youtube 560 315 https://www.youtube.com/watch?v=PGK2UUAyE54}
76///
77/// The animation is controlled through the [crossFadeState] parameter.
78/// [firstCurve] and [secondCurve] represent the opacity curves of the two
79/// children. The [firstCurve] is inverted, i.e. it fades out when providing a
80/// growing curve like [Curves.linear]. The [sizeCurve] is the curve used to
81/// animate between the size of the fading-out child and the size of the
82/// fading-in child.
83///
84/// This widget is intended to be used to fade a pair of widgets with the same
85/// width. In the case where the two children have different heights, the
86/// animation crops overflowing children during the animation by aligning their
87/// top edge, which means that the bottom will be clipped.
88///
89/// The animation is automatically triggered when an existing
90/// [AnimatedCrossFade] is rebuilt with a different value for the
91/// [crossFadeState] property.
92///
93/// {@tool snippet}
94///
95/// This code fades between two representations of the Flutter logo. It depends
96/// on a boolean field `_first`; when `_first` is true, the first logo is shown,
97/// otherwise the second logo is shown. When the field changes state, the
98/// [AnimatedCrossFade] widget cross-fades between the two forms of the logo
99/// over three seconds.
100///
101/// ```dart
102/// AnimatedCrossFade(
103/// duration: const Duration(seconds: 3),
104/// firstChild: const FlutterLogo(style: FlutterLogoStyle.horizontal, size: 100.0),
105/// secondChild: const FlutterLogo(style: FlutterLogoStyle.stacked, size: 100.0),
106/// crossFadeState: _first ? CrossFadeState.showFirst : CrossFadeState.showSecond,
107/// )
108/// ```
109/// {@end-tool}
110///
111/// See also:
112///
113/// * [AnimatedOpacity], which fades between nothing and a single child.
114/// * [AnimatedSwitcher], which switches out a child for a new one with a
115/// customizable transition, supporting multiple cross-fades at once.
116/// * [AnimatedSize], the lower-level widget which [AnimatedCrossFade] uses to
117/// automatically change size.
118class AnimatedCrossFade extends StatefulWidget {
119 /// Creates a cross-fade animation widget.
120 ///
121 /// The [duration] of the animation is the same for all components (fade in,
122 /// fade out, and size), and you can pass [Interval]s instead of [Curve]s in
123 /// order to have finer control, e.g., creating an overlap between the fades.
124 const AnimatedCrossFade({
125 super.key,
126 required this.firstChild,
127 required this.secondChild,
128 this.firstCurve = Curves.linear,
129 this.secondCurve = Curves.linear,
130 this.sizeCurve = Curves.linear,
131 this.alignment = Alignment.topCenter,
132 required this.crossFadeState,
133 required this.duration,
134 this.reverseDuration,
135 this.layoutBuilder = defaultLayoutBuilder,
136 this.excludeBottomFocus = true,
137 });
138
139 /// The child that is visible when [crossFadeState] is
140 /// [CrossFadeState.showFirst]. It fades out when transitioning
141 /// [crossFadeState] from [CrossFadeState.showFirst] to
142 /// [CrossFadeState.showSecond] and vice versa.
143 final Widget firstChild;
144
145 /// The child that is visible when [crossFadeState] is
146 /// [CrossFadeState.showSecond]. It fades in when transitioning
147 /// [crossFadeState] from [CrossFadeState.showFirst] to
148 /// [CrossFadeState.showSecond] and vice versa.
149 final Widget secondChild;
150
151 /// The child that will be shown when the animation has completed.
152 final CrossFadeState crossFadeState;
153
154 /// The duration of the whole orchestrated animation.
155 final Duration duration;
156
157 /// The duration of the whole orchestrated animation when running in reverse.
158 ///
159 /// If not supplied, this defaults to [duration].
160 final Duration? reverseDuration;
161
162 /// The fade curve of the first child.
163 ///
164 /// Defaults to [Curves.linear].
165 final Curve firstCurve;
166
167 /// The fade curve of the second child.
168 ///
169 /// Defaults to [Curves.linear].
170 final Curve secondCurve;
171
172 /// The curve of the animation between the two children's sizes.
173 ///
174 /// Defaults to [Curves.linear].
175 final Curve sizeCurve;
176
177 /// How the children should be aligned while the size is animating.
178 ///
179 /// Defaults to [Alignment.topCenter].
180 ///
181 /// See also:
182 ///
183 /// * [Alignment], a class with convenient constants typically used to
184 /// specify an [AlignmentGeometry].
185 /// * [AlignmentDirectional], like [Alignment] for specifying alignments
186 /// relative to text direction.
187 final AlignmentGeometry alignment;
188
189 /// A builder that positions the [firstChild] and [secondChild] widgets.
190 ///
191 /// The widget returned by this method is wrapped in an [AnimatedSize].
192 ///
193 /// By default, this uses [AnimatedCrossFade.defaultLayoutBuilder], which uses
194 /// a [Stack] and aligns the `bottomChild` to the top of the stack while
195 /// providing the `topChild` as the non-positioned child to fill the provided
196 /// constraints. This works well when the [AnimatedCrossFade] is in a position
197 /// to change size and when the children are not flexible. However, if the
198 /// children are less fussy about their sizes (for example a
199 /// [CircularProgressIndicator] inside a [Center]), or if the
200 /// [AnimatedCrossFade] is being forced to a particular size, then it can
201 /// result in the widgets jumping about when the cross-fade state is changed.
202 final AnimatedCrossFadeBuilder layoutBuilder;
203
204 /// When true, this is equivalent to wrapping the bottom widget with an [ExcludeFocus]
205 /// widget while it is at the bottom of the cross-fade stack.
206 ///
207 /// Defaults to true. When it is false, the bottom widget in the cross-fade stack
208 /// can remain in focus until the top widget requests focus. This is useful for
209 /// animating between different [TextField]s so the keyboard remains open during the
210 /// cross-fade animation.
211 final bool excludeBottomFocus;
212
213 /// The default layout algorithm used by [AnimatedCrossFade].
214 ///
215 /// The top child is placed in a stack that sizes itself to match the top
216 /// child. The bottom child is positioned at the top of the same stack, sized
217 /// to fit its width but without forcing the height. The stack is then
218 /// clipped.
219 ///
220 /// This is the default value for [layoutBuilder]. It implements
221 /// [AnimatedCrossFadeBuilder].
222 static Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) {
223 return Stack(
224 clipBehavior: Clip.none,
225 children: <Widget>[
226 Positioned(
227 key: bottomChildKey,
228 left: 0.0,
229 top: 0.0,
230 right: 0.0,
231 child: bottomChild,
232 ),
233 Positioned(
234 key: topChildKey,
235 child: topChild,
236 ),
237 ],
238 );
239 }
240
241 @override
242 State<AnimatedCrossFade> createState() => _AnimatedCrossFadeState();
243
244 @override
245 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
246 super.debugFillProperties(properties);
247 properties.add(EnumProperty<CrossFadeState>('crossFadeState', crossFadeState));
248 properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: Alignment.topCenter));
249 properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms'));
250 properties.add(IntProperty('reverseDuration', reverseDuration?.inMilliseconds, unit: 'ms', defaultValue: null));
251 }
252}
253
254class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProviderStateMixin {
255 late AnimationController _controller;
256 late Animation<double> _firstAnimation;
257 late Animation<double> _secondAnimation;
258
259 @override
260 void initState() {
261 super.initState();
262 _controller = AnimationController(
263 duration: widget.duration,
264 reverseDuration: widget.reverseDuration,
265 vsync: this,
266 );
267 if (widget.crossFadeState == CrossFadeState.showSecond) {
268 _controller.value = 1.0;
269 }
270 _firstAnimation = _initAnimation(widget.firstCurve, true);
271 _secondAnimation = _initAnimation(widget.secondCurve, false);
272 _controller.addStatusListener((AnimationStatus status) {
273 setState(() {
274 // Trigger a rebuild because it depends on _isTransitioning, which
275 // changes its value together with animation status.
276 });
277 });
278 }
279
280 Animation<double> _initAnimation(Curve curve, bool inverted) {
281 Animation<double> result = _controller.drive(CurveTween(curve: curve));
282 if (inverted) {
283 result = result.drive(Tween<double>(begin: 1.0, end: 0.0));
284 }
285 return result;
286 }
287
288 @override
289 void dispose() {
290 _controller.dispose();
291 super.dispose();
292 }
293
294 @override
295 void didUpdateWidget(AnimatedCrossFade oldWidget) {
296 super.didUpdateWidget(oldWidget);
297 if (widget.duration != oldWidget.duration) {
298 _controller.duration = widget.duration;
299 }
300 if (widget.reverseDuration != oldWidget.reverseDuration) {
301 _controller.reverseDuration = widget.reverseDuration;
302 }
303 if (widget.firstCurve != oldWidget.firstCurve) {
304 _firstAnimation = _initAnimation(widget.firstCurve, true);
305 }
306 if (widget.secondCurve != oldWidget.secondCurve) {
307 _secondAnimation = _initAnimation(widget.secondCurve, false);
308 }
309 if (widget.crossFadeState != oldWidget.crossFadeState) {
310 switch (widget.crossFadeState) {
311 case CrossFadeState.showFirst:
312 _controller.reverse();
313 case CrossFadeState.showSecond:
314 _controller.forward();
315 }
316 }
317 }
318
319 @override
320 Widget build(BuildContext context) {
321 const Key kFirstChildKey = ValueKey<CrossFadeState>(CrossFadeState.showFirst);
322 const Key kSecondChildKey = ValueKey<CrossFadeState>(CrossFadeState.showSecond);
323 final Key topKey;
324 Widget topChild;
325 final Animation<double> topAnimation;
326 final Key bottomKey;
327 Widget bottomChild;
328 final Animation<double> bottomAnimation;
329 if (_controller.isForwardOrCompleted) {
330 topKey = kSecondChildKey;
331 topChild = widget.secondChild;
332 topAnimation = _secondAnimation;
333 bottomKey = kFirstChildKey;
334 bottomChild = widget.firstChild;
335 bottomAnimation = _firstAnimation;
336 } else {
337 topKey = kFirstChildKey;
338 topChild = widget.firstChild;
339 topAnimation = _firstAnimation;
340 bottomKey = kSecondChildKey;
341 bottomChild = widget.secondChild;
342 bottomAnimation = _secondAnimation;
343 }
344
345 bottomChild = TickerMode(
346 key: bottomKey,
347 enabled: _controller.isAnimating,
348 child: IgnorePointer(
349 child: ExcludeSemantics( // Always exclude the semantics of the widget that's fading out.
350 child: ExcludeFocus(
351 excluding: widget.excludeBottomFocus,
352 child: FadeTransition(
353 opacity: bottomAnimation,
354 child: bottomChild,
355 ),
356 ),
357 ),
358 ),
359 );
360 topChild = TickerMode(
361 key: topKey,
362 enabled: true, // Top widget always has its animations enabled.
363 child: IgnorePointer(
364 ignoring: false,
365 child: ExcludeSemantics(
366 excluding: false, // Always publish semantics for the widget that's fading in.
367 child: ExcludeFocus(
368 excluding: false,
369 child: FadeTransition(
370 opacity: topAnimation,
371 child: topChild,
372 ),
373 ),
374 ),
375 ),
376 );
377 return ClipRect(
378 child: AnimatedSize(
379 alignment: widget.alignment,
380 duration: widget.duration,
381 reverseDuration: widget.reverseDuration,
382 curve: widget.sizeCurve,
383 child: widget.layoutBuilder(topChild, topKey, bottomChild, bottomKey),
384 ),
385 );
386 }
387
388 @override
389 void debugFillProperties(DiagnosticPropertiesBuilder description) {
390 super.debugFillProperties(description);
391 description.add(EnumProperty<CrossFadeState>('crossFadeState', widget.crossFadeState));
392 description.add(DiagnosticsProperty<AnimationController>('controller', _controller, showName: false));
393 description.add(DiagnosticsProperty<AlignmentGeometry>('alignment', widget.alignment, defaultValue: Alignment.topCenter));
394 }
395}
396