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