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