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'; |
9 | library; |
10 | |
11 | import 'package:flutter/rendering.dart'; |
12 | |
13 | import 'animated_size.dart'; |
14 | import 'basic.dart'; |
15 | import 'focus_scope.dart'; |
16 | import 'framework.dart'; |
17 | import 'ticker_provider.dart'; |
18 | import '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. |
26 | enum 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} |
70 | typedef 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. |
119 | class 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 | |
264 | class _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 | |