@@ -10,39 +10,134 @@ import 'framework.dart';
10
10
import 'ticker_provider.dart' ;
11
11
import 'transitions.dart' ;
12
12
13
+ // Internal representation of a child that, now or in the past, was set on the
14
+ // AnimatedChildSwitcher.child field, but is now in the process of
15
+ // transitioning. The internal representation includes fields that we don't want
16
+ // to expose to the public API (like the controller).
13
17
class _AnimatedChildSwitcherChildEntry {
14
- _AnimatedChildSwitcherChildEntry (this .widget, this .controller, this .animation);
18
+ _AnimatedChildSwitcherChildEntry ({
19
+ @required this .animation,
20
+ @required this .transition,
21
+ @required this .controller,
22
+ @required this .widgetChild,
23
+ }) : assert (animation != null ),
24
+ assert (transition != null ),
25
+ assert (controller != null );
15
26
16
- Widget widget;
27
+ final Animation <double > animation;
28
+
29
+ // The currently built transition for this child.
30
+ Widget transition;
17
31
32
+ // The animation controller for the child's transition.
18
33
final AnimationController controller;
19
- final Animation <double > animation;
34
+
35
+ // The widget's child at the time this entry was created or updated.
36
+ Widget widgetChild;
20
37
}
21
38
22
- /// A widget that automatically does a [FadeTransition] between a new widget and
39
+ /// Signature for builders used to generate custom transitions for
40
+ /// [AnimatedChildSwitcher] .
41
+ ///
42
+ /// The [child] should be transitioning in when the [animation] is running in
43
+ /// the forward direction.
44
+ ///
45
+ /// The function should return a widget which wraps the given [child] . It may
46
+ /// also use the [animation] to inform its transition. It must not return null.
47
+ typedef Widget AnimatedChildSwitcherTransitionBuilder (Widget child, Animation <double > animation);
48
+
49
+ /// Signature for builders used to generate custom layouts for
50
+ /// [AnimatedChildSwitcher] .
51
+ ///
52
+ /// The function should return a widget which contains the given children, laid
53
+ /// out as desired. It must not return null.
54
+ typedef Widget AnimatedChildSwitcherLayoutBuilder (List <Widget > children);
55
+
56
+ /// A widget that by default does a [FadeTransition] between a new widget and
23
57
/// the widget previously set on the [AnimatedChildSwitcher] as a child.
24
58
///
25
- /// More than one previous child can exist and be fading out while the newest
26
- /// one is fading in if they are swapped fast enough (i.e. before [duration]
27
- /// elapses).
59
+ /// If they are swapped fast enough (i.e. before [duration] elapses), more than
60
+ /// one previous child can exist and be transitioning out while the newest one
61
+ /// is transitioning in.
62
+ ///
63
+ /// If the "new" child is the same widget type as the "old" child, but with
64
+ /// different parameters, then [AnimatedChildSwitcher] will *not* do a
65
+ /// transition between them, since as far as the framework is concerned, they
66
+ /// are the same widget, and the existing widget can be updated with the new
67
+ /// parameters. If you wish to force the transition to occur, set a [Key]
68
+ /// (typically a [ValueKey] taking any widget data that would change the visual
69
+ /// appearance of the widget) on each child widget that you wish to be
70
+ /// considered unique.
71
+ ///
72
+ /// ## Sample code
73
+ ///
74
+ /// ```dart
75
+ /// class ClickCounter extends StatefulWidget {
76
+ /// const ClickCounter({Key key}) : super(key: key);
77
+ ///
78
+ /// @override
79
+ /// _ClickCounterState createState() => new _ClickCounterState();
80
+ /// }
81
+ ///
82
+ /// class _ClickCounterState extends State<ClickCounter> {
83
+ /// int _count = 0;
84
+ ///
85
+ /// @override
86
+ /// Widget build(BuildContext context) {
87
+ /// return new Material(
88
+ /// child: Column(
89
+ /// mainAxisAlignment: MainAxisAlignment.center,
90
+ /// children: <Widget>[
91
+ /// new AnimatedChildSwitcher(
92
+ /// duration: const Duration(milliseconds: 200),
93
+ /// transitionBuilder: (Widget child, Animation<double> animation) {
94
+ /// return new ScaleTransition(child: child, scale: animation);
95
+ /// },
96
+ /// child: new Text(
97
+ /// '$_count',
98
+ /// // Must have this key to build a unique widget when _count changes.
99
+ /// key: new ValueKey<int>(_count),
100
+ /// textScaleFactor: 3.0,
101
+ /// ),
102
+ /// ),
103
+ /// new RaisedButton(
104
+ /// child: new Text('Click!'),
105
+ /// onPressed: () {
106
+ /// setState(() {
107
+ /// _count += 1;
108
+ /// });
109
+ /// },
110
+ /// ),
111
+ /// ],
112
+ /// ),
113
+ /// );
114
+ /// }
115
+ /// }
116
+ /// ```
28
117
///
29
118
/// See also:
30
119
///
31
120
/// * [AnimatedCrossFade] , which only fades between two children, but also
32
121
/// interpolates their sizes, and is reversible.
33
122
/// * [FadeTransition] which [AnimatedChildSwitcher] uses to perform the transition.
34
123
class AnimatedChildSwitcher extends StatefulWidget {
35
- /// The [duration] , [switchInCurve] , and [switchOutCurve] parameters must not
36
- /// be null.
124
+ /// Creates an [AnimatedChildSwitcher] .
125
+ ///
126
+ /// The [duration] , [transitionBuilder] , [layoutBuilder] , [switchInCurve] , and
127
+ /// [switchOutCurve] parameters must not be null.
37
128
const AnimatedChildSwitcher ({
38
129
Key key,
39
130
this .child,
131
+ @required this .duration,
40
132
this .switchInCurve: Curves .linear,
41
133
this .switchOutCurve: Curves .linear,
42
- @required this .duration,
43
- }) : assert (switchInCurve != null ),
134
+ this .transitionBuilder: AnimatedChildSwitcher .defaultTransitionBuilder,
135
+ this .layoutBuilder: AnimatedChildSwitcher .defaultLayoutBuilder,
136
+ }) : assert (duration != null ),
137
+ assert (switchInCurve != null ),
44
138
assert (switchOutCurve != null ),
45
- assert (duration != null ),
139
+ assert (transitionBuilder != null ),
140
+ assert (layoutBuilder != null ),
46
141
super (key: key);
47
142
48
143
/// The current child widget to display. If there was a previous child,
@@ -53,17 +148,71 @@ class AnimatedChildSwitcher extends StatefulWidget {
53
148
/// [duration] .
54
149
final Widget child;
55
150
56
- /// The animation curve to use when fading in the current widget.
151
+ /// The duration of the transition from the old [child] value to the new one.
152
+ final Duration duration;
153
+
154
+ /// The animation curve to use when transitioning in [child] .
57
155
final Curve switchInCurve;
58
156
59
- /// The animation curve to use when fading out the previous widgets .
157
+ /// The animation curve to use when transitioning the previous [child] out .
60
158
final Curve switchOutCurve;
61
159
62
- /// The duration over which to perform the cross fade using [FadeTransition] .
63
- final Duration duration;
160
+ /// A function that wraps the new [child] with an animation that transitions
161
+ /// the [child] in when the animation runs in the forward direction and out
162
+ /// when the animation runs in the reverse direction.
163
+ ///
164
+ /// The default is [AnimatedChildSwitcher.defaultTransitionBuilder] .
165
+ ///
166
+ /// See also:
167
+ ///
168
+ /// * [AnimatedChildSwitcherTransitionBuilder] for more information about
169
+ /// how a transition builder should function.
170
+ final AnimatedChildSwitcherTransitionBuilder transitionBuilder;
171
+
172
+ /// A function that wraps all of the children that are transitioning out, and
173
+ /// the [child] that's transitioning in, with a widget that lays all of them
174
+ /// out.
175
+ ///
176
+ /// The default is [AnimatedChildSwitcher.defaultLayoutBuilder] .
177
+ ///
178
+ /// See also:
179
+ ///
180
+ /// * [AnimatedChildSwitcherLayoutBuilder] for more information about
181
+ /// how a layout builder should function.
182
+ final AnimatedChildSwitcherLayoutBuilder layoutBuilder;
64
183
65
184
@override
66
185
_AnimatedChildSwitcherState createState () => new _AnimatedChildSwitcherState ();
186
+
187
+ /// The default transition algorithm used by [AnimatedChildSwitcher] .
188
+ ///
189
+ /// The new child is given a [FadeTransition] which increases opacity as
190
+ /// the animation goes from 0.0 to 1.0, and decreases when the animation is
191
+ /// reversed.
192
+ ///
193
+ /// The default value for the [transitionBuilder] , an
194
+ /// [AnimatedChildSwitcherTransitionBuilder] function.
195
+ static Widget defaultTransitionBuilder (Widget child, Animation <double > animation) {
196
+ return new FadeTransition (
197
+ opacity: animation,
198
+ child: child,
199
+ );
200
+ }
201
+
202
+ /// The default layout algorithm used by [AnimatedChildSwitcher] .
203
+ ///
204
+ /// The new child is placed in a [Stack] that sizes itself to match the
205
+ /// largest of the child or a previous child. The children are centered on
206
+ /// each other.
207
+ ///
208
+ /// This is the default value for [layoutBuilder] . It implements
209
+ /// [AnimatedChildSwitcherLayoutBuilder] .
210
+ static Widget defaultLayoutBuilder (List <Widget > children) {
211
+ return new Stack (
212
+ children: children,
213
+ alignment: Alignment .center,
214
+ );
215
+ }
67
216
}
68
217
69
218
class _AnimatedChildSwitcherState extends State <AnimatedChildSwitcher > with TickerProviderStateMixin {
@@ -73,17 +222,56 @@ class _AnimatedChildSwitcherState extends State<AnimatedChildSwitcher> with Tick
73
222
@override
74
223
void initState () {
75
224
super .initState ();
76
- addEntry (false );
225
+ _addEntry (animate: false );
226
+ }
227
+
228
+ Widget _generateTransition (Animation <double > animation) {
229
+ return new KeyedSubtree (
230
+ key: new UniqueKey (),
231
+ child: widget.transitionBuilder (widget.child, animation),
232
+ );
77
233
}
78
234
79
- void addEntry (bool animate) {
235
+ _AnimatedChildSwitcherChildEntry _newEntry ({
236
+ @required AnimationController controller,
237
+ @required Animation <double > animation,
238
+ }) {
239
+ final _AnimatedChildSwitcherChildEntry entry = new _AnimatedChildSwitcherChildEntry (
240
+ widgetChild: widget.child,
241
+ transition: _generateTransition (animation),
242
+ animation: animation,
243
+ controller: controller,
244
+ );
245
+ animation.addStatusListener ((AnimationStatus status) {
246
+ if (status == AnimationStatus .dismissed) {
247
+ assert (_children.contains (entry));
248
+ setState (() {
249
+ _children.remove (entry);
250
+ });
251
+ controller.dispose ();
252
+ }
253
+ });
254
+ return entry;
255
+ }
256
+
257
+ void _addEntry ({@required bool animate}) {
258
+ if (widget.child == null ) {
259
+ if (animate && _currentChild != null ) {
260
+ _currentChild.controller.reverse ();
261
+ assert (! _children.contains (_currentChild));
262
+ _children.add (_currentChild);
263
+ }
264
+ _currentChild = null ;
265
+ return ;
266
+ }
80
267
final AnimationController controller = new AnimationController (
81
268
duration: widget.duration,
82
269
vsync: this ,
83
270
);
84
271
if (animate) {
85
272
if (_currentChild != null ) {
86
273
_currentChild.controller.reverse ();
274
+ assert (! _children.contains (_currentChild));
87
275
_children.add (_currentChild);
88
276
}
89
277
controller.forward ();
@@ -97,21 +285,7 @@ class _AnimatedChildSwitcherState extends State<AnimatedChildSwitcher> with Tick
97
285
curve: widget.switchInCurve,
98
286
reverseCurve: widget.switchOutCurve,
99
287
);
100
- final _AnimatedChildSwitcherChildEntry entry = new _AnimatedChildSwitcherChildEntry (
101
- widget.child,
102
- controller,
103
- animation,
104
- );
105
- animation.addStatusListener ((AnimationStatus status) {
106
- if (status == AnimationStatus .dismissed) {
107
- assert (_children.contains (entry));
108
- setState (() {
109
- _children.remove (entry);
110
- });
111
- controller.dispose ();
112
- }
113
- });
114
- _currentChild = entry;
288
+ _currentChild = _newEntry (controller: controller, animation: animation);
115
289
}
116
290
117
291
@override
@@ -125,31 +299,33 @@ class _AnimatedChildSwitcherState extends State<AnimatedChildSwitcher> with Tick
125
299
super .dispose ();
126
300
}
127
301
302
+ bool get hasNewChild => widget.child != null ;
303
+ bool get hasOldChild => _currentChild != null ;
304
+
128
305
@override
129
- Widget build (BuildContext context) {
130
- if (widget.child != _currentChild.widget) {
131
- addEntry (true );
132
- }
133
- final List <Widget > children = < Widget > [];
134
- for (_AnimatedChildSwitcherChildEntry child in _children) {
135
- children.add (
136
- new FadeTransition (
137
- opacity: child.animation,
138
- child: child.widget,
139
- ),
140
- );
306
+ void didUpdateWidget (AnimatedChildSwitcher oldWidget) {
307
+ super .didUpdateWidget (oldWidget);
308
+ if (hasNewChild != hasOldChild || hasNewChild &&
309
+ ! Widget .canUpdate (widget.child, _currentChild.widgetChild)) {
310
+ _addEntry (animate: true );
311
+ } else {
312
+ if (_currentChild != null ) {
313
+ _currentChild.widgetChild = widget.child;
314
+ _currentChild.transition = _generateTransition (_currentChild.animation);
315
+ }
141
316
}
317
+ }
318
+
319
+ @override
320
+ Widget build (BuildContext context) {
321
+ final List <Widget > children = _children.map <Widget >(
322
+ (_AnimatedChildSwitcherChildEntry entry) {
323
+ return entry.transition;
324
+ },
325
+ ).toList ();
142
326
if (_currentChild != null ) {
143
- children.add (
144
- new FadeTransition (
145
- opacity: _currentChild.animation,
146
- child: _currentChild.widget,
147
- ),
148
- );
327
+ children.add (_currentChild.transition);
149
328
}
150
- return new Stack (
151
- children: children,
152
- alignment: Alignment .center,
153
- );
329
+ return widget.layoutBuilder (children);
154
330
}
155
331
}
0 commit comments