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