Skip to content

Commit c73b8a7

Browse files
authored
Fix a bug in the AnimatedChildSwitcher, add builders. (#16250)
This fixes a rendering problem in the AnimatedChildSwitcher where it would add a new "previous" child each time it rebuilt, and if you did it fast enough, all of them would disappear from the page. It also expands the API for AnimatedChildSwitcher to allow you to specify your own transition and/or layout builder for the transition. Fixes #16226
1 parent 7a78741 commit c73b8a7

File tree

5 files changed

+427
-57
lines changed

5 files changed

+427
-57
lines changed

packages/flutter/lib/src/rendering/stack.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,11 @@ class RenderStack extends RenderBox
480480
assert(_resolvedAlignment != null);
481481
_hasVisualOverflow = false;
482482
bool hasNonPositionedChildren = false;
483+
if (childCount == 0) {
484+
size = constraints.biggest;
485+
assert(size.isFinite);
486+
return;
487+
}
483488

484489
double width = constraints.minWidth;
485490
double height = constraints.minHeight;

packages/flutter/lib/src/widgets/animated_child_switcher.dart

Lines changed: 231 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,134 @@ import 'framework.dart';
1010
import 'ticker_provider.dart';
1111
import 'transitions.dart';
1212

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).
1317
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);
1526

16-
Widget widget;
27+
final Animation<double> animation;
28+
29+
// The currently built transition for this child.
30+
Widget transition;
1731

32+
// The animation controller for the child's transition.
1833
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;
2037
}
2138

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
2357
/// the widget previously set on the [AnimatedChildSwitcher] as a child.
2458
///
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+
/// ```
28117
///
29118
/// See also:
30119
///
31120
/// * [AnimatedCrossFade], which only fades between two children, but also
32121
/// interpolates their sizes, and is reversible.
33122
/// * [FadeTransition] which [AnimatedChildSwitcher] uses to perform the transition.
34123
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.
37128
const AnimatedChildSwitcher({
38129
Key key,
39130
this.child,
131+
@required this.duration,
40132
this.switchInCurve: Curves.linear,
41133
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),
44138
assert(switchOutCurve != null),
45-
assert(duration != null),
139+
assert(transitionBuilder != null),
140+
assert(layoutBuilder != null),
46141
super(key: key);
47142

48143
/// The current child widget to display. If there was a previous child,
@@ -53,17 +148,71 @@ class AnimatedChildSwitcher extends StatefulWidget {
53148
/// [duration].
54149
final Widget child;
55150

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].
57155
final Curve switchInCurve;
58156

59-
/// The animation curve to use when fading out the previous widgets.
157+
/// The animation curve to use when transitioning the previous [child] out.
60158
final Curve switchOutCurve;
61159

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;
64183

65184
@override
66185
_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+
}
67216
}
68217

69218
class _AnimatedChildSwitcherState extends State<AnimatedChildSwitcher> with TickerProviderStateMixin {
@@ -73,17 +222,56 @@ class _AnimatedChildSwitcherState extends State<AnimatedChildSwitcher> with Tick
73222
@override
74223
void initState() {
75224
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+
);
77233
}
78234

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+
}
80267
final AnimationController controller = new AnimationController(
81268
duration: widget.duration,
82269
vsync: this,
83270
);
84271
if (animate) {
85272
if (_currentChild != null) {
86273
_currentChild.controller.reverse();
274+
assert(!_children.contains(_currentChild));
87275
_children.add(_currentChild);
88276
}
89277
controller.forward();
@@ -97,21 +285,7 @@ class _AnimatedChildSwitcherState extends State<AnimatedChildSwitcher> with Tick
97285
curve: widget.switchInCurve,
98286
reverseCurve: widget.switchOutCurve,
99287
);
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);
115289
}
116290

117291
@override
@@ -125,31 +299,33 @@ class _AnimatedChildSwitcherState extends State<AnimatedChildSwitcher> with Tick
125299
super.dispose();
126300
}
127301

302+
bool get hasNewChild => widget.child != null;
303+
bool get hasOldChild => _currentChild != null;
304+
128305
@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+
}
141316
}
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();
142326
if (_currentChild != null) {
143-
children.add(
144-
new FadeTransition(
145-
opacity: _currentChild.animation,
146-
child: _currentChild.widget,
147-
),
148-
);
327+
children.add(_currentChild.transition);
149328
}
150-
return new Stack(
151-
children: children,
152-
alignment: Alignment.center,
153-
);
329+
return widget.layoutBuilder(children);
154330
}
155331
}

packages/flutter/lib/src/widgets/animated_cross_fade.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ typedef Widget AnimatedCrossFadeBuilder(Widget topChild, Key topChildKey, Widget
100100
///
101101
/// * [AnimatedSize], the lower-level widget which [AnimatedCrossFade] uses to
102102
/// automatically change size.
103+
/// * [AnimatedChildSwitcher], which switches out a child for a new one with a
104+
/// customizable transition.
103105
class AnimatedCrossFade extends StatefulWidget {
104106
/// Creates a cross-fade animation widget.
105107
///

packages/flutter/test/rendering/stack_test.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ void main() {
4949
expect(green.size.height, equals(100.0));
5050
});
5151

52+
test('Stack can layout with no children', () {
53+
final RenderBox stack = new RenderStack(
54+
textDirection: TextDirection.ltr,
55+
children: <RenderBox>[],
56+
);
57+
58+
layout(stack, constraints: new BoxConstraints.tight(const Size(100.0, 100.0)));
59+
60+
expect(stack.size.width, equals(100.0));
61+
expect(stack.size.height, equals(100.0));
62+
});
5263

5364
group('RenderIndexedStack', () {
5465
test('visitChildrenForSemantics only visits displayed child', () {

0 commit comments

Comments
 (0)