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/foundation.dart';
6
7import 'basic.dart';
8import 'framework.dart';
9import 'ticker_provider.dart';
10import '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).
16class _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.
49typedef 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`.
61typedef 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.
104class 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
247class _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