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 | import 'package:flutter/foundation.dart'; |
6 | |
7 | import 'basic.dart'; |
8 | import 'framework.dart'; |
9 | import 'ticker_provider.dart'; |
10 | import 'transitions.dart'; |
11 | |
12 | // Internal representation of a child that, now or in the past, was set on the |
13 | // AnimatedSwitcher.child field, but is now in the process of |
14 | // transitioning. The internal representation includes fields that we don't want |
15 | // to expose to the public API (like the controller). |
16 | class _ChildEntry { |
17 | _ChildEntry({ |
18 | required this.controller, |
19 | required this.animation, |
20 | required this.transition, |
21 | required this.widgetChild, |
22 | }); |
23 | |
24 | // The animation controller for the child's transition. |
25 | final AnimationController controller; |
26 | |
27 | // The (curved) animation being used to drive the transition. |
28 | final Animation<double> animation; |
29 | |
30 | // The currently built transition for this child. |
31 | Widget transition; |
32 | |
33 | // The widget's child at the time this entry was created or updated. |
34 | // Used to rebuild the transition if necessary. |
35 | Widget widgetChild; |
36 | |
37 | @override |
38 | String toString() => 'Entry# ${shortHash(this)}( $widgetChild)' ; |
39 | } |
40 | |
41 | /// Signature for builders used to generate custom transitions for |
42 | /// [AnimatedSwitcher]. |
43 | /// |
44 | /// The `child` should be transitioning in when the `animation` is running in |
45 | /// the forward direction. |
46 | /// |
47 | /// The function should return a widget which wraps the given `child`. It may |
48 | /// also use the `animation` to inform its transition. It must not return null. |
49 | typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation<double> animation); |
50 | |
51 | /// Signature for builders used to generate custom layouts for |
52 | /// [AnimatedSwitcher]. |
53 | /// |
54 | /// The builder should return a widget which contains the given children, laid |
55 | /// out as desired. It must not return null. The builder should be able to |
56 | /// handle an empty list of `previousChildren`, or a null `currentChild`. |
57 | /// |
58 | /// The `previousChildren` list is an unmodifiable list, sorted with the oldest |
59 | /// at the beginning and the newest at the end. It does not include the |
60 | /// `currentChild`. |
61 | typedef AnimatedSwitcherLayoutBuilder = Widget Function(Widget? currentChild, List<Widget> previousChildren); |
62 | |
63 | /// A widget that by default does a cross-fade between a new widget and the |
64 | /// widget previously set on the [AnimatedSwitcher] as a child. |
65 | /// |
66 | /// {@youtube 560 315 https://www.youtube.com/watch?v=2W7POjFb88g} |
67 | /// |
68 | /// If they are swapped fast enough (i.e. before [duration] elapses), more than |
69 | /// one previous child can exist and be transitioning out while the newest one |
70 | /// is transitioning in. |
71 | /// |
72 | /// If the "new" child is the same widget type and key as the "old" child, but |
73 | /// with different parameters, then [AnimatedSwitcher] will *not* do a |
74 | /// transition between them, since as far as the framework is concerned, they |
75 | /// are the same widget and the existing widget can be updated with the new |
76 | /// parameters. To force the transition to occur, set a [Key] on each child |
77 | /// widget that you wish to be considered unique (typically a [ValueKey] on the |
78 | /// widget data that distinguishes this child from the others). |
79 | /// |
80 | /// The same key can be used for a new child as was used for an already-outgoing |
81 | /// child; the two will not be considered related. (For example, if a progress |
82 | /// indicator with key A is first shown, then an image with key B, then another |
83 | /// progress indicator with key A again, all in rapid succession, then the old |
84 | /// progress indicator and the image will be fading out while a new progress |
85 | /// indicator is fading in.) |
86 | /// |
87 | /// The type of transition can be changed from a cross-fade to a custom |
88 | /// transition by setting the [transitionBuilder]. |
89 | /// |
90 | /// {@tool dartpad} |
91 | /// This sample shows a counter that animates the scale of a text widget |
92 | /// whenever the value changes. |
93 | /// |
94 | /// ** See code in examples/api/lib/widgets/animated_switcher/animated_switcher.0.dart ** |
95 | /// {@end-tool} |
96 | /// |
97 | /// See also: |
98 | /// |
99 | /// * [AnimatedCrossFade], which only fades between two children, but also |
100 | /// interpolates their sizes, and is reversible. |
101 | /// * [AnimatedOpacity], which can be used to switch between nothingness and |
102 | /// a given child by fading the child in and out. |
103 | /// * [FadeTransition], which [AnimatedSwitcher] uses to perform the transition. |
104 | class AnimatedSwitcher extends StatefulWidget { |
105 | /// Creates an [AnimatedSwitcher]. |
106 | const AnimatedSwitcher({ |
107 | super.key, |
108 | this.child, |
109 | required this.duration, |
110 | this.reverseDuration, |
111 | this.switchInCurve = Curves.linear, |
112 | this.switchOutCurve = Curves.linear, |
113 | this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, |
114 | this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, |
115 | }); |
116 | |
117 | /// The current child widget to display. If there was a previous child, then |
118 | /// that child will be faded out using the [switchOutCurve], while the new |
119 | /// child is faded in with the [switchInCurve], over the [duration]. |
120 | /// |
121 | /// If there was no previous child, then this child will fade in using the |
122 | /// [switchInCurve] over the [duration]. |
123 | /// |
124 | /// The child is considered to be "new" if it has a different type or [Key] |
125 | /// (see [Widget.canUpdate]). |
126 | /// |
127 | /// To change the kind of transition used, see [transitionBuilder]. |
128 | final Widget? child; |
129 | |
130 | /// The duration of the transition from the old [child] value to the new one. |
131 | /// |
132 | /// This duration is applied to the given [child] when that property is set to |
133 | /// a new child. The same duration is used when fading out, unless |
134 | /// [reverseDuration] is set. Changing [duration] will not affect the |
135 | /// durations of transitions already in progress. |
136 | final Duration duration; |
137 | |
138 | /// The duration of the transition from the new [child] value to the old one. |
139 | /// |
140 | /// This duration is applied to the given [child] when that property is set to |
141 | /// a new child. Changing [reverseDuration] will not affect the durations of |
142 | /// transitions already in progress. |
143 | /// |
144 | /// If not set, then the value of [duration] is used by default. |
145 | final Duration? reverseDuration; |
146 | |
147 | /// The animation curve to use when transitioning in a new [child]. |
148 | /// |
149 | /// This curve is applied to the given [child] when that property is set to a |
150 | /// new child. Changing [switchInCurve] will not affect the curve of a |
151 | /// transition already in progress. |
152 | /// |
153 | /// The [switchOutCurve] is used when fading out, except that if [child] is |
154 | /// changed while the current child is in the middle of fading in, |
155 | /// [switchInCurve] will be run in reverse from that point instead of jumping |
156 | /// to the corresponding point on [switchOutCurve]. |
157 | final Curve switchInCurve; |
158 | |
159 | /// The animation curve to use when transitioning a previous [child] out. |
160 | /// |
161 | /// This curve is applied to the [child] when the child is faded in (or when |
162 | /// the widget is created, for the first child). Changing [switchOutCurve] |
163 | /// will not affect the curves of already-visible widgets, it only affects the |
164 | /// curves of future children. |
165 | /// |
166 | /// If [child] is changed while the current child is in the middle of fading |
167 | /// in, [switchInCurve] will be run in reverse from that point instead of |
168 | /// jumping to the corresponding point on [switchOutCurve]. |
169 | final Curve switchOutCurve; |
170 | |
171 | /// A function that wraps a new [child] with an animation that transitions |
172 | /// the [child] in when the animation runs in the forward direction and out |
173 | /// when the animation runs in the reverse direction. This is only called |
174 | /// when a new [child] is set (not for each build), or when a new |
175 | /// [transitionBuilder] is set. If a new [transitionBuilder] is set, then |
176 | /// the transition is rebuilt for the current child and all previous children |
177 | /// using the new [transitionBuilder]. The function must not return null. |
178 | /// |
179 | /// The default is [AnimatedSwitcher.defaultTransitionBuilder]. |
180 | /// |
181 | /// The animation provided to the builder has the [duration] and |
182 | /// [switchInCurve] or [switchOutCurve] applied as provided when the |
183 | /// corresponding [child] was first provided. |
184 | /// |
185 | /// See also: |
186 | /// |
187 | /// * [AnimatedSwitcherTransitionBuilder] for more information about |
188 | /// how a transition builder should function. |
189 | final AnimatedSwitcherTransitionBuilder transitionBuilder; |
190 | |
191 | /// A function that wraps all of the children that are transitioning out, and |
192 | /// the [child] that's transitioning in, with a widget that lays all of them |
193 | /// out. This is called every time this widget is built. The function must not |
194 | /// return null. |
195 | /// |
196 | /// The default is [AnimatedSwitcher.defaultLayoutBuilder]. |
197 | /// |
198 | /// See also: |
199 | /// |
200 | /// * [AnimatedSwitcherLayoutBuilder] for more information about |
201 | /// how a layout builder should function. |
202 | final AnimatedSwitcherLayoutBuilder layoutBuilder; |
203 | |
204 | @override |
205 | State<AnimatedSwitcher> createState() => _AnimatedSwitcherState(); |
206 | |
207 | /// The transition builder used as the default value of [transitionBuilder]. |
208 | /// |
209 | /// The new child is given a [FadeTransition] which increases opacity as |
210 | /// the animation goes from 0.0 to 1.0, and decreases when the animation is |
211 | /// reversed. |
212 | /// |
213 | /// This is an [AnimatedSwitcherTransitionBuilder] function. |
214 | static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) { |
215 | return FadeTransition( |
216 | key: ValueKey<Key?>(child.key), |
217 | opacity: animation, |
218 | child: child, |
219 | ); |
220 | } |
221 | |
222 | /// The layout builder used as the default value of [layoutBuilder]. |
223 | /// |
224 | /// The new child is placed in a [Stack] that sizes itself to match the |
225 | /// largest of the child or a previous child. The children are centered on |
226 | /// each other. |
227 | /// |
228 | /// This is an [AnimatedSwitcherLayoutBuilder] function. |
229 | static Widget defaultLayoutBuilder(Widget? currentChild, List<Widget> previousChildren) { |
230 | return Stack( |
231 | alignment: Alignment.center, |
232 | children: <Widget>[ |
233 | ...previousChildren, |
234 | if (currentChild != null) currentChild, |
235 | ], |
236 | ); |
237 | } |
238 | |
239 | @override |
240 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
241 | super.debugFillProperties(properties); |
242 | properties.add(IntProperty('duration' , duration.inMilliseconds, unit: 'ms' )); |
243 | properties.add(IntProperty('reverseDuration' , reverseDuration?.inMilliseconds, unit: 'ms' , defaultValue: null)); |
244 | } |
245 | } |
246 | |
247 | class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProviderStateMixin { |
248 | _ChildEntry? _currentEntry; |
249 | final Set<_ChildEntry> _outgoingEntries = <_ChildEntry>{}; |
250 | List<Widget>? _outgoingWidgets = const <Widget>[]; |
251 | int _childNumber = 0; |
252 | |
253 | @override |
254 | void initState() { |
255 | super.initState(); |
256 | _addEntryForNewChild(animate: false); |
257 | } |
258 | |
259 | @override |
260 | void didUpdateWidget(AnimatedSwitcher oldWidget) { |
261 | super.didUpdateWidget(oldWidget); |
262 | |
263 | // If the transition builder changed, then update all of the previous |
264 | // transitions. |
265 | if (widget.transitionBuilder != oldWidget.transitionBuilder) { |
266 | _outgoingEntries.forEach(_updateTransitionForEntry); |
267 | if (_currentEntry != null) { |
268 | _updateTransitionForEntry(_currentEntry!); |
269 | } |
270 | _markChildWidgetCacheAsDirty(); |
271 | } |
272 | |
273 | final bool hasNewChild = widget.child != null; |
274 | final bool hasOldChild = _currentEntry != null; |
275 | if (hasNewChild != hasOldChild || |
276 | hasNewChild && !Widget.canUpdate(widget.child!, _currentEntry!.widgetChild)) { |
277 | // Child has changed, fade current entry out and add new entry. |
278 | _childNumber += 1; |
279 | _addEntryForNewChild(animate: true); |
280 | } else if (_currentEntry != null) { |
281 | assert(hasOldChild && hasNewChild); |
282 | assert(Widget.canUpdate(widget.child!, _currentEntry!.widgetChild)); |
283 | // Child has been updated. Make sure we update the child widget and |
284 | // transition in _currentEntry even though we're not going to start a new |
285 | // animation, but keep the key from the previous transition so that we |
286 | // update the transition instead of replacing it. |
287 | _currentEntry!.widgetChild = widget.child!; |
288 | _updateTransitionForEntry(_currentEntry!); // uses entry.widgetChild |
289 | _markChildWidgetCacheAsDirty(); |
290 | } |
291 | } |
292 | |
293 | void _addEntryForNewChild({ required bool animate }) { |
294 | assert(animate || _currentEntry == null); |
295 | if (_currentEntry != null) { |
296 | assert(animate); |
297 | assert(!_outgoingEntries.contains(_currentEntry)); |
298 | _outgoingEntries.add(_currentEntry!); |
299 | _currentEntry!.controller.reverse(); |
300 | _markChildWidgetCacheAsDirty(); |
301 | _currentEntry = null; |
302 | } |
303 | if (widget.child == null) { |
304 | return; |
305 | } |
306 | final AnimationController controller = AnimationController( |
307 | duration: widget.duration, |
308 | reverseDuration: widget.reverseDuration, |
309 | vsync: this, |
310 | ); |
311 | final Animation<double> animation = CurvedAnimation( |
312 | parent: controller, |
313 | curve: widget.switchInCurve, |
314 | reverseCurve: widget.switchOutCurve, |
315 | ); |
316 | _currentEntry = _newEntry( |
317 | child: widget.child!, |
318 | controller: controller, |
319 | animation: animation, |
320 | builder: widget.transitionBuilder, |
321 | ); |
322 | if (animate) { |
323 | controller.forward(); |
324 | } else { |
325 | assert(_outgoingEntries.isEmpty); |
326 | controller.value = 1.0; |
327 | } |
328 | } |
329 | |
330 | _ChildEntry _newEntry({ |
331 | required Widget child, |
332 | required AnimatedSwitcherTransitionBuilder builder, |
333 | required AnimationController controller, |
334 | required Animation<double> animation, |
335 | }) { |
336 | final _ChildEntry entry = _ChildEntry( |
337 | widgetChild: child, |
338 | transition: KeyedSubtree.wrap(builder(child, animation), _childNumber), |
339 | animation: animation, |
340 | controller: controller, |
341 | ); |
342 | animation.addStatusListener((AnimationStatus status) { |
343 | if (status == AnimationStatus.dismissed) { |
344 | setState(() { |
345 | assert(mounted); |
346 | assert(_outgoingEntries.contains(entry)); |
347 | _outgoingEntries.remove(entry); |
348 | _markChildWidgetCacheAsDirty(); |
349 | }); |
350 | controller.dispose(); |
351 | } |
352 | }); |
353 | return entry; |
354 | } |
355 | |
356 | void _markChildWidgetCacheAsDirty() { |
357 | _outgoingWidgets = null; |
358 | } |
359 | |
360 | void _updateTransitionForEntry(_ChildEntry entry) { |
361 | entry.transition = KeyedSubtree( |
362 | key: entry.transition.key, |
363 | child: widget.transitionBuilder(entry.widgetChild, entry.animation), |
364 | ); |
365 | } |
366 | |
367 | void _rebuildOutgoingWidgetsIfNeeded() { |
368 | _outgoingWidgets ??= List<Widget>.unmodifiable( |
369 | _outgoingEntries.map<Widget>((_ChildEntry entry) => entry.transition), |
370 | ); |
371 | assert(_outgoingEntries.length == _outgoingWidgets!.length); |
372 | assert(_outgoingEntries.isEmpty || _outgoingEntries.last.transition == _outgoingWidgets!.last); |
373 | } |
374 | |
375 | @override |
376 | void dispose() { |
377 | if (_currentEntry != null) { |
378 | _currentEntry!.controller.dispose(); |
379 | } |
380 | for (final _ChildEntry entry in _outgoingEntries) { |
381 | entry.controller.dispose(); |
382 | } |
383 | super.dispose(); |
384 | } |
385 | |
386 | @override |
387 | Widget build(BuildContext context) { |
388 | _rebuildOutgoingWidgetsIfNeeded(); |
389 | return widget.layoutBuilder(_currentEntry?.transition, _outgoingWidgets!.where((Widget outgoing) => outgoing.key != _currentEntry?.transition.key).toSet().toList()); |
390 | } |
391 | } |
392 | |