Speed up AnimatedSwitcher. (#17265)
This optimizes the AnimatedSwitcher so that it tags the right widget with its keyed subtree, and avoids rebuilding the transition unnecessarily.
This significantly improves the performance of Chips (which uses AnimatedSwitcher to swap out it's avatar and delete icon children).
diff --git a/examples/flutter_gallery/lib/gallery/home.dart b/examples/flutter_gallery/lib/gallery/home.dart
index 66a7885..c3632ba 100644
--- a/examples/flutter_gallery/lib/gallery/home.dart
+++ b/examples/flutter_gallery/lib/gallery/home.dart
@@ -302,13 +302,6 @@
super.dispose();
}
- static Widget _animatedSwitcherLayoutBuilder(List<Widget> children) {
- return new Stack(
- children: children,
- alignment: Alignment.center,
- );
- }
-
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
@@ -338,7 +331,6 @@
duration: _kFrontLayerSwitchDuration,
switchOutCurve: switchOutCurve,
switchInCurve: switchInCurve,
- layoutBuilder: _animatedSwitcherLayoutBuilder,
child: _category == null
? const _FlutterLogo()
: new IconButton(
@@ -358,7 +350,6 @@
duration: _kFrontLayerSwitchDuration,
switchOutCurve: switchOutCurve,
switchInCurve: switchInCurve,
- layoutBuilder: _animatedSwitcherLayoutBuilder,
child: _category != null
? new _DemosPage(_category)
: new _CategoriesPage(
diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart
index ba8681e..721fbb8 100644
--- a/packages/flutter/lib/src/material/chip.dart
+++ b/packages/flutter/lib/src/material/chip.dart
@@ -1328,7 +1328,7 @@
}
}
- Widget _wrapWithTooltip(Widget child, String tooltip, VoidCallback callback) {
+ Widget _wrapWithTooltip(String tooltip, VoidCallback callback, Widget child) {
if (child == null || callback == null || tooltip == null) {
return child;
}
@@ -1343,17 +1343,17 @@
return null;
}
return _wrapWithTooltip(
+ widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context)?.deleteButtonTooltip,
+ widget.onDeleted,
new InkResponse(
onTap: widget.isEnabled ? widget.onDeleted : null,
child: new IconTheme(
data: theme.iconTheme.copyWith(
- color: (widget.deleteIconColor ?? chipTheme.deleteIconColor) ?? theme.iconTheme.color,
+ color: widget.deleteIconColor ?? chipTheme.deleteIconColor,
),
child: widget.deleteIcon,
),
),
- widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context)?.deleteButtonTooltip,
- widget.onDeleted,
);
}
@@ -1388,42 +1388,43 @@
);
},
child: _wrapWithTooltip(
- new _ChipRenderWidget(
- theme: new _ChipRenderTheme(
- label: new DefaultTextStyle(
- overflow: TextOverflow.fade,
- textAlign: TextAlign.start,
- maxLines: 1,
- softWrap: false,
- style: widget.labelStyle ?? chipTheme.labelStyle,
- child: widget.label,
- ),
- avatar: new AnimatedSwitcher(
- child: widget.avatar,
- duration: _kDrawerDuration,
- switchInCurve: Curves.fastOutSlowIn,
- ),
- deleteIcon: new AnimatedSwitcher(
- child: _buildDeleteIcon(context, theme, chipTheme),
- duration: _kDrawerDuration,
- switchInCurve: Curves.fastOutSlowIn,
- ),
- brightness: chipTheme.brightness,
- padding: (widget.padding ?? chipTheme.padding).resolve(textDirection),
- labelPadding: (widget.labelPadding ?? chipTheme.labelPadding).resolve(textDirection),
- showAvatar: hasAvatar,
- showCheckmark: widget.showCheckmark,
- canTapBody: canTap,
+ widget.tooltip,
+ widget.onPressed,
+ new _ChipRenderWidget(
+ theme: new _ChipRenderTheme(
+ label: new DefaultTextStyle(
+ overflow: TextOverflow.fade,
+ textAlign: TextAlign.start,
+ maxLines: 1,
+ softWrap: false,
+ style: widget.labelStyle ?? chipTheme.labelStyle,
+ child: widget.label,
),
- value: widget.selected,
- checkmarkAnimation: checkmarkAnimation,
- enableAnimation: enableAnimation,
- avatarDrawerAnimation: avatarDrawerAnimation,
- deleteDrawerAnimation: deleteDrawerAnimation,
- isEnabled: widget.isEnabled,
+ avatar: new AnimatedSwitcher(
+ child: widget.avatar,
+ duration: _kDrawerDuration,
+ switchInCurve: Curves.fastOutSlowIn,
+ ),
+ deleteIcon: new AnimatedSwitcher(
+ child: _buildDeleteIcon(context, theme, chipTheme),
+ duration: _kDrawerDuration,
+ switchInCurve: Curves.fastOutSlowIn,
+ ),
+ brightness: chipTheme.brightness,
+ padding: (widget.padding ?? chipTheme.padding).resolve(textDirection),
+ labelPadding: (widget.labelPadding ?? chipTheme.labelPadding).resolve(textDirection),
+ showAvatar: hasAvatar,
+ showCheckmark: widget.showCheckmark,
+ canTapBody: canTap,
),
- widget.tooltip,
- widget.onPressed),
+ value: widget.selected,
+ checkmarkAnimation: checkmarkAnimation,
+ enableAnimation: enableAnimation,
+ avatarDrawerAnimation: avatarDrawerAnimation,
+ deleteDrawerAnimation: deleteDrawerAnimation,
+ isEnabled: widget.isEnabled,
+ ),
+ ),
),
),
);
diff --git a/packages/flutter/lib/src/widgets/animated_switcher.dart b/packages/flutter/lib/src/widgets/animated_switcher.dart
index 8aba05e..917210c 100644
--- a/packages/flutter/lib/src/widgets/animated_switcher.dart
+++ b/packages/flutter/lib/src/widgets/animated_switcher.dart
@@ -38,19 +38,24 @@
/// Signature for builders used to generate custom transitions for
/// [AnimatedSwitcher].
///
-/// The [child] should be transitioning in when the [animation] is running in
+/// The `child` should be transitioning in when the `animation` is running in
/// the forward direction.
///
-/// The function should return a widget which wraps the given [child]. It may
-/// also use the [animation] to inform its transition. It must not return null.
+/// The function should return a widget which wraps the given `child`. It may
+/// also use the `animation` to inform its transition. It must not return null.
typedef Widget AnimatedSwitcherTransitionBuilder(Widget child, Animation<double> animation);
/// Signature for builders used to generate custom layouts for
/// [AnimatedSwitcher].
///
-/// The function should return a widget which contains the given children, laid
-/// out as desired. It must not return null.
-typedef Widget AnimatedSwitcherLayoutBuilder(List<Widget> children);
+/// The builder should return a widget which contains the given children, laid
+/// out as desired. It must not return null. The builder should be able to
+/// handle an empty list of `previousChildren`, or a null `currentChild`.
+///
+/// The `previousChildren` list is an unmodifiable list, sorted with the oldest
+/// at the beginning and the newest at the end. It does not include the
+/// `currentChild`.
+typedef Widget AnimatedSwitcherLayoutBuilder(Widget currentChild, List<Widget> previousChildren);
/// A widget that by default does a [FadeTransition] between a new widget and
/// the widget previously set on the [AnimatedSwitcher] as a child.
@@ -63,10 +68,9 @@
/// different parameters, then [AnimatedSwitcher] will *not* do a
/// transition between them, since as far as the framework is concerned, they
/// are the same widget, and the existing widget can be updated with the new
-/// parameters. If you wish to force the transition to occur, set a [Key]
-/// (typically a [ValueKey] taking any widget data that would change the visual
-/// appearance of the widget) on each child widget that you wish to be
-/// considered unique.
+/// parameters. To force the transition to occur, set a [Key] (typically a
+/// [ValueKey] taking any widget data that would change the visual appearance
+/// of the widget) on each child widget that you wish to be considered unique.
///
/// ## Sample code
///
@@ -83,31 +87,35 @@
///
/// @override
/// Widget build(BuildContext context) {
-/// return new Material(
-/// child: Column(
-/// mainAxisAlignment: MainAxisAlignment.center,
-/// children: <Widget>[
-/// new AnimatedSwitcher(
-/// duration: const Duration(milliseconds: 200),
-/// transitionBuilder: (Widget child, Animation<double> animation) {
-/// return new ScaleTransition(child: child, scale: animation);
-/// },
-/// child: new Text(
-/// '$_count',
-/// // Must have this key to build a unique widget when _count changes.
-/// key: new ValueKey<int>(_count),
-/// textScaleFactor: 3.0,
+/// return new MaterialApp(
+/// home: new Material(
+/// child: Column(
+/// mainAxisAlignment: MainAxisAlignment.center,
+/// children: <Widget>[
+/// new AnimatedSwitcher(
+/// duration: const Duration(milliseconds: 500),
+/// transitionBuilder: (Widget child, Animation<double> animation) {
+/// return new ScaleTransition(child: child, scale: animation);
+/// },
+/// child: new Text(
+/// '$_count',
+/// // This key causes the AnimatedSwitcher to interpret this as a "new"
+/// // child each time the count changes, so that it will begin its animation
+/// // when the count changes.
+/// key: new ValueKey<int>(_count),
+/// style: Theme.of(context).textTheme.display1,
+/// ),
/// ),
-/// ),
-/// new RaisedButton(
-/// child: new Text('Click!'),
-/// onPressed: () {
-/// setState(() {
-/// _count += 1;
-/// });
-/// },
-/// ),
-/// ],
+/// new RaisedButton(
+/// child: const Text('Increment'),
+/// onPressed: () {
+/// setState(() {
+/// _count += 1;
+/// });
+/// },
+/// ),
+/// ],
+/// ),
/// ),
/// );
/// }
@@ -156,9 +164,13 @@
/// The animation curve to use when transitioning the previous [child] out.
final Curve switchOutCurve;
- /// A function that wraps the new [child] with an animation that transitions
+ /// A function that wraps a new [child] with an animation that transitions
/// the [child] in when the animation runs in the forward direction and out
- /// when the animation runs in the reverse direction.
+ /// when the animation runs in the reverse direction. This is only called
+ /// when a new [child] is set (not for each build), or when a new
+ /// [transitionBuilder] is set. If a new [transitionBuilder] is set, then
+ /// the transition is rebuilt for the current child and all previous children
+ /// using the new [transitionBuilder]. The function must not return null.
///
/// The default is [AnimatedSwitcher.defaultTransitionBuilder].
///
@@ -170,7 +182,8 @@
/// A function that wraps all of the children that are transitioning out, and
/// the [child] that's transitioning in, with a widget that lays all of them
- /// out.
+ /// out. This is called every time this widget is built. The function must not
+ /// return null.
///
/// The default is [AnimatedSwitcher.defaultLayoutBuilder].
///
@@ -183,14 +196,13 @@
@override
_AnimatedSwitcherState createState() => new _AnimatedSwitcherState();
- /// The default transition algorithm used by [AnimatedSwitcher].
+ /// The transition builder used as the default value of [transitionBuilder].
///
/// The new child is given a [FadeTransition] which increases opacity as
/// the animation goes from 0.0 to 1.0, and decreases when the animation is
/// reversed.
///
- /// The default value for the [transitionBuilder], an
- /// [AnimatedSwitcherTransitionBuilder] function.
+ /// This is an [AnimatedSwitcherTransitionBuilder] function.
static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return new FadeTransition(
opacity: animation,
@@ -198,15 +210,18 @@
);
}
- /// The default layout algorithm used by [AnimatedSwitcher].
+ /// The layout builder used as the default value of [layoutBuilder].
///
/// The new child is placed in a [Stack] that sizes itself to match the
/// largest of the child or a previous child. The children are centered on
/// each other.
///
- /// This is the default value for [layoutBuilder]. It implements
- /// [AnimatedSwitcherLayoutBuilder].
- static Widget defaultLayoutBuilder(List<Widget> children) {
+ /// This is an [AnimatedSwitcherLayoutBuilder] function.
+ static Widget defaultLayoutBuilder(Widget currentChild, List<Widget> previousChildren) {
+ List<Widget> children = previousChildren;
+ if (currentChild != null) {
+ children = children.toList()..add(currentChild);
+ }
return new Stack(
children: children,
alignment: Alignment.center,
@@ -215,8 +230,10 @@
}
class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProviderStateMixin {
- final Set<_AnimatedSwitcherChildEntry> _children = new Set<_AnimatedSwitcherChildEntry>();
+ final Set<_AnimatedSwitcherChildEntry> _previousChildren = new Set<_AnimatedSwitcherChildEntry>();
_AnimatedSwitcherChildEntry _currentChild;
+ List<Widget> _previousChildWidgetCache = const <Widget>[];
+ int serialNumber = 0;
@override
void initState() {
@@ -224,28 +241,26 @@
_addEntry(animate: false);
}
- Widget _generateTransition(Animation<double> animation) {
- return new KeyedSubtree(
- key: new UniqueKey(),
- child: widget.transitionBuilder(widget.child, animation),
- );
- }
-
_AnimatedSwitcherChildEntry _newEntry({
@required AnimationController controller,
@required Animation<double> animation,
}) {
final _AnimatedSwitcherChildEntry entry = new _AnimatedSwitcherChildEntry(
widgetChild: widget.child,
- transition: _generateTransition(animation),
+ transition: KeyedSubtree.wrap(
+ widget.transitionBuilder(
+ widget.child,
+ animation,
+ ),
+ serialNumber++,
+ ),
animation: animation,
controller: controller,
);
animation.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed) {
- assert(_children.contains(entry));
setState(() {
- _children.remove(entry);
+ _removeExpiredChild(entry);
});
controller.dispose();
}
@@ -253,12 +268,27 @@
return entry;
}
+ void _removeExpiredChild(_AnimatedSwitcherChildEntry child) {
+ assert(_previousChildren.contains(child));
+ _previousChildren.remove(child);
+ _markChildWidgetCacheAsDirty();
+ }
+
+ void _retireCurrentChild() {
+ assert(!_previousChildren.contains(_currentChild));
+ _currentChild.controller.reverse();
+ _previousChildren.add(_currentChild);
+ _markChildWidgetCacheAsDirty();
+ }
+
+ void _markChildWidgetCacheAsDirty() {
+ _previousChildWidgetCache = null;
+ }
+
void _addEntry({@required bool animate}) {
if (widget.child == null) {
if (animate && _currentChild != null) {
- _currentChild.controller.reverse();
- assert(!_children.contains(_currentChild));
- _children.add(_currentChild);
+ _retireCurrentChild();
}
_currentChild = null;
return;
@@ -269,14 +299,12 @@
);
if (animate) {
if (_currentChild != null) {
- _currentChild.controller.reverse();
- assert(!_children.contains(_currentChild));
- _children.add(_currentChild);
+ _retireCurrentChild();
}
controller.forward();
} else {
assert(_currentChild == null);
- assert(_children.isEmpty);
+ assert(_previousChildren.isEmpty);
controller.value = 1.0;
}
final Animation<double> animation = new CurvedAnimation(
@@ -292,39 +320,63 @@
if (_currentChild != null) {
_currentChild.controller.dispose();
}
- for (_AnimatedSwitcherChildEntry child in _children) {
+ for (_AnimatedSwitcherChildEntry child in _previousChildren) {
child.controller.dispose();
}
super.dispose();
}
- bool get hasNewChild => widget.child != null;
- bool get hasOldChild => _currentChild != null;
-
@override
void didUpdateWidget(AnimatedSwitcher oldWidget) {
super.didUpdateWidget(oldWidget);
- if (hasNewChild != hasOldChild || hasNewChild &&
- !Widget.canUpdate(widget.child, _currentChild.widgetChild)) {
+
+ void updateTransition(_AnimatedSwitcherChildEntry entry) {
+ entry.transition = new KeyedSubtree(
+ key: entry.transition.key,
+ child: widget.transitionBuilder(entry.widgetChild, entry.animation),
+ );
+ }
+
+ // If the transition builder changed, then update all of the previous transitions
+ if (widget.transitionBuilder != oldWidget.transitionBuilder) {
+ _previousChildren.forEach(updateTransition);
+ if (_currentChild != null) {
+ updateTransition(_currentChild);
+ }
+ _markChildWidgetCacheAsDirty();
+ }
+
+ final bool hasNewChild = widget.child != null;
+ final bool hasOldChild = _currentChild != null;
+ if (hasNewChild != hasOldChild ||
+ hasNewChild && !Widget.canUpdate(widget.child, _currentChild.widgetChild)) {
_addEntry(animate: true);
} else {
+ // Make sure we update the child widget and transition in _currentChild
+ // even if we're not going to start a new animation, but keep the key from
+ // the previous transition so that we update the transition instead of
+ // replacing it.
if (_currentChild != null) {
_currentChild.widgetChild = widget.child;
- _currentChild.transition = _generateTransition(_currentChild.animation);
+ updateTransition(_currentChild);
+ _markChildWidgetCacheAsDirty();
}
}
}
+ void _rebuildChildWidgetCacheIfNeeded() {
+ _previousChildWidgetCache ??= new List<Widget>.unmodifiable(
+ _previousChildren.map<Widget>((_AnimatedSwitcherChildEntry child) {
+ return child.transition;
+ }),
+ );
+ assert(_previousChildren.length == _previousChildWidgetCache.length);
+ assert(_previousChildren.isEmpty || _previousChildren.last.transition == _previousChildWidgetCache.last);
+ }
+
@override
Widget build(BuildContext context) {
- final List<Widget> children = _children.map<Widget>(
- (_AnimatedSwitcherChildEntry entry) {
- return entry.transition;
- },
- ).toList();
- if (_currentChild != null) {
- children.add(_currentChild.transition);
- }
- return widget.layoutBuilder(children);
+ _rebuildChildWidgetCacheIfNeeded();
+ return widget.layoutBuilder(_currentChild?.transition, _previousChildWidgetCache);
}
}
diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart
index 2bbbf19..02b33f7 100644
--- a/packages/flutter/test/material/chip_test.dart
+++ b/packages/flutter/test/material/chip_test.dart
@@ -1029,10 +1029,10 @@
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
);
- final ChipThemeData chipTheme = themeData.chipTheme;
+ final ChipThemeData defaultChipTheme = themeData.chipTheme;
bool value = false;
Widget buildApp({
- ChipThemeData theme,
+ ChipThemeData chipTheme,
Widget avatar,
Widget deleteIcon,
bool isSelectable: true,
@@ -1040,12 +1040,12 @@
bool isDeletable: true,
bool showCheckmark: true,
}) {
- theme ??= chipTheme;
+ chipTheme ??= defaultChipTheme;
return _wrapForChip(
child: new Theme(
data: themeData,
child: new ChipTheme(
- data: theme,
+ data: chipTheme,
child: new StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return new RawChip(
showCheckmark: showCheckmark,
@@ -1054,7 +1054,7 @@
avatar: avatar,
deleteIcon: deleteIcon,
isEnabled: isSelectable || isPressable,
- shape: theme.shape,
+ shape: chipTheme.shape,
selected: isSelectable ? value : null,
label: new Text('$value'),
onSelected: isSelectable
@@ -1085,13 +1085,13 @@
DefaultTextStyle labelStyle = getLabelStyle(tester);
// Check default theme for enabled widget.
- expect(materialBox, paints..path(color: chipTheme.backgroundColor));
+ expect(materialBox, paints..path(color: defaultChipTheme.backgroundColor));
expect(iconData.color, equals(const Color(0xde000000)));
expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde)));
await tester.tap(find.byType(RawChip));
await tester.pumpAndSettle();
materialBox = getMaterialBox(tester);
- expect(materialBox, paints..path(color: chipTheme.selectedColor));
+ expect(materialBox, paints..path(color: defaultChipTheme.selectedColor));
await tester.tap(find.byType(RawChip));
await tester.pumpAndSettle();
@@ -1100,7 +1100,7 @@
await tester.pumpAndSettle();
materialBox = getMaterialBox(tester);
labelStyle = getLabelStyle(tester);
- expect(materialBox, paints..path(color: chipTheme.disabledColor));
+ expect(materialBox, paints..path(color: defaultChipTheme.disabledColor));
expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde)));
// Apply a custom theme.
@@ -1108,14 +1108,14 @@
const Color customColor2 = const Color(0xdeadbeef);
const Color customColor3 = const Color(0xbeefcafe);
const Color customColor4 = const Color(0xaddedabe);
- final ChipThemeData customTheme = chipTheme.copyWith(
+ final ChipThemeData customTheme = defaultChipTheme.copyWith(
brightness: Brightness.dark,
backgroundColor: customColor1,
disabledColor: customColor2,
selectedColor: customColor3,
deleteIconColor: customColor4,
);
- await tester.pumpWidget(buildApp(theme: customTheme));
+ await tester.pumpWidget(buildApp(chipTheme: customTheme));
await tester.pumpAndSettle();
materialBox = getMaterialBox(tester);
iconData = getIconData(tester);
@@ -1134,7 +1134,7 @@
// Check custom theme with disabled widget.
await tester.pumpWidget(buildApp(
- theme: customTheme,
+ chipTheme: customTheme,
isSelectable: false,
isPressable: false,
isDeletable: true,
diff --git a/packages/flutter/test/widgets/animated_switcher_test.dart b/packages/flutter/test/widgets/animated_switcher_test.dart
index 49aea4c..0b17710 100644
--- a/packages/flutter/test/widgets/animated_switcher_test.dart
+++ b/packages/flutter/test/widgets/animated_switcher_test.dart
@@ -19,6 +19,7 @@
),
);
+ expect(find.byType(FadeTransition), findsOneWidget);
FadeTransition transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(1.0));
@@ -32,6 +33,7 @@
);
await tester.pump(const Duration(milliseconds: 50));
+ expect(find.byType(FadeTransition), findsNWidgets(2));
transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(0.5));
@@ -64,6 +66,7 @@
),
);
+ expect(find.byType(FadeTransition), findsOneWidget);
FadeTransition transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(1.0));
@@ -77,7 +80,8 @@
);
await tester.pump(const Duration(milliseconds: 50));
- transition = tester.widget(find.byType(FadeTransition));
+ expect(find.byType(FadeTransition), findsOneWidget);
+ transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(1.0));
await tester.pumpAndSettle();
});
@@ -117,6 +121,7 @@
),
);
+ expect(find.byType(FadeTransition), findsOneWidget);
transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(1.0));
@@ -163,9 +168,9 @@
});
testWidgets('AnimatedSwitcher uses custom layout.', (WidgetTester tester) async {
- Widget newLayoutBuilder(List<Widget> children) {
+ Widget newLayoutBuilder(Widget currentChild, List<Widget> previousChildren) {
return new Column(
- children: children,
+ children: previousChildren + <Widget>[currentChild],
);
}
@@ -182,12 +187,15 @@
});
testWidgets('AnimatedSwitcher uses custom transitions.', (WidgetTester tester) async {
- final List<Widget> transitions = <Widget>[];
- Widget newLayoutBuilder(List<Widget> children) {
- transitions.clear();
- transitions.addAll(children);
+ final List<Widget> foundChildren = <Widget>[];
+ Widget newLayoutBuilder(Widget currentChild, List<Widget> previousChildren) {
+ foundChildren.clear();
+ if (currentChild != null) {
+ foundChildren.add(currentChild);
+ }
+ foundChildren.addAll(previousChildren);
return new Column(
- children: children,
+ children: foundChildren,
);
}
@@ -212,12 +220,231 @@
);
expect(find.byType(Column), findsOneWidget);
- for (Widget transition in transitions) {
- expect(transition, const isInstanceOf<KeyedSubtree>());
+ for (Widget child in foundChildren) {
+ expect(child, const isInstanceOf<KeyedSubtree>());
+ }
+
+ await tester.pumpWidget(
+ new Directionality(
+ textDirection: TextDirection.rtl,
+ child: new AnimatedSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: null,
+ switchInCurve: Curves.linear,
+ layoutBuilder: newLayoutBuilder,
+ transitionBuilder: newTransitionBuilder,
+ ),
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 50));
+
+ for (Widget child in foundChildren) {
+ expect(child, const isInstanceOf<KeyedSubtree>());
expect(
- find.descendant(of: find.byWidget(transition), matching: find.byType(SizeTransition)),
+ find.descendant(of: find.byWidget(child), matching: find.byType(SizeTransition)),
findsOneWidget,
);
}
});
+
+ testWidgets("AnimatedSwitcher doesn't reset state of the children in transitions.", (WidgetTester tester) async {
+ final UniqueKey statefulOne = new UniqueKey();
+ final UniqueKey statefulTwo = new UniqueKey();
+ final UniqueKey statefulThree = new UniqueKey();
+
+ StatefulTestState.generation = 0;
+
+ await tester.pumpWidget(
+ new AnimatedSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: new StatefulTest(key: statefulOne),
+ switchInCurve: Curves.linear,
+ switchOutCurve: Curves.linear,
+ ),
+ );
+
+ expect(find.byType(FadeTransition), findsOneWidget);
+ FadeTransition transition = tester.firstWidget(find.byType(FadeTransition));
+ expect(transition.opacity.value, equals(1.0));
+ expect(StatefulTestState.generation, equals(1));
+
+ await tester.pumpWidget(
+ new AnimatedSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: new StatefulTest(key: statefulTwo),
+ switchInCurve: Curves.linear,
+ switchOutCurve: Curves.linear,
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 50));
+ expect(find.byType(FadeTransition), findsNWidgets(2));
+ transition = tester.firstWidget(find.byType(FadeTransition));
+ expect(transition.opacity.value, equals(0.5));
+ expect(StatefulTestState.generation, equals(2));
+
+ await tester.pumpWidget(
+ new AnimatedSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: new StatefulTest(key: statefulThree),
+ switchInCurve: Curves.linear,
+ switchOutCurve: Curves.linear,
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 10));
+ expect(StatefulTestState.generation, equals(3));
+ transition = tester.widget(find.byType(FadeTransition).at(0));
+ expect(transition.opacity.value, closeTo(0.4, 0.01));
+ transition = tester.widget(find.byType(FadeTransition).at(1));
+ expect(transition.opacity.value, closeTo(0.4, 0.01));
+ transition = tester.widget(find.byType(FadeTransition).at(2));
+ expect(transition.opacity.value, closeTo(0.1, 0.01));
+ await tester.pumpAndSettle();
+ expect(StatefulTestState.generation, equals(3));
+ });
+
+ testWidgets('AnimatedSwitcher updates widgets without animating if they are isomorphic.', (WidgetTester tester) async {
+ Future<Null> pumpChild(Widget child) async {
+ return tester.pumpWidget(
+ new Directionality(
+ textDirection: TextDirection.rtl,
+ child: new AnimatedSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: child,
+ switchInCurve: Curves.linear,
+ switchOutCurve: Curves.linear,
+ ),
+ ),
+ );
+ }
+
+ await pumpChild(const Text('1'));
+ await tester.pump(const Duration(milliseconds: 10));
+ FadeTransition transition = tester.widget(find.byType(FadeTransition).first);
+ expect(transition.opacity.value, equals(1.0));
+ expect(find.text('1'), findsOneWidget);
+ expect(find.text('2'), findsNothing);
+ await pumpChild(const Text('2'));
+ transition = tester.widget(find.byType(FadeTransition).first);
+ await tester.pump(const Duration(milliseconds: 20));
+ expect(transition.opacity.value, equals(1.0));
+ expect(find.text('1'), findsNothing);
+ expect(find.text('2'), findsOneWidget);
+ });
+
+ testWidgets('AnimatedSwitcher updates previous child transitions if the transitionBuilder changes.', (WidgetTester tester) async {
+ final UniqueKey containerOne = new UniqueKey();
+ final UniqueKey containerTwo = new UniqueKey();
+ final UniqueKey containerThree = new UniqueKey();
+
+ final List<Widget> foundChildren = <Widget>[];
+ Widget newLayoutBuilder(Widget currentChild, List<Widget> previousChildren) {
+ foundChildren.clear();
+ if (currentChild != null) {
+ foundChildren.add(currentChild);
+ }
+ foundChildren.addAll(previousChildren);
+ return new Column(
+ children: foundChildren,
+ );
+ }
+
+ // Insert three unique children so that we have some previous children.
+ await tester.pumpWidget(
+ new AnimatedSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: new Container(key: containerOne, color: const Color(0xFFFF0000)),
+ switchInCurve: Curves.linear,
+ switchOutCurve: Curves.linear,
+ layoutBuilder: newLayoutBuilder,
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 10));
+
+ await tester.pumpWidget(
+ new AnimatedSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: new Container(key: containerTwo, color: const Color(0xFF00FF00)),
+ switchInCurve: Curves.linear,
+ switchOutCurve: Curves.linear,
+ layoutBuilder: newLayoutBuilder,
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 10));
+
+ await tester.pumpWidget(
+ new AnimatedSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: new Container(key: containerThree, color: const Color(0xFF0000FF)),
+ switchInCurve: Curves.linear,
+ switchOutCurve: Curves.linear,
+ layoutBuilder: newLayoutBuilder,
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 10));
+
+ expect(foundChildren.length, equals(3));
+ for (Widget child in foundChildren) {
+ expect(child, const isInstanceOf<KeyedSubtree>());
+ expect(
+ find.descendant(of: find.byWidget(child), matching: find.byType(FadeTransition)),
+ findsOneWidget,
+ );
+ }
+
+ Widget newTransitionBuilder(Widget child, Animation<double> animation) {
+ return new ScaleTransition(
+ scale: animation,
+ child: child,
+ );
+ }
+
+ // Now set a new transition builder and make sure all the previous
+ // transitions are replaced.
+ await tester.pumpWidget(
+ new AnimatedSwitcher(
+ duration: const Duration(milliseconds: 100),
+ child: new Container(color: const Color(0x00000000)),
+ switchInCurve: Curves.linear,
+ layoutBuilder: newLayoutBuilder,
+ transitionBuilder: newTransitionBuilder,
+ ),
+ );
+
+ await tester.pump(const Duration(milliseconds: 10));
+
+ expect(foundChildren.length, equals(3));
+ for (Widget child in foundChildren) {
+ expect(child, const isInstanceOf<KeyedSubtree>());
+ expect(
+ find.descendant(of: find.byWidget(child), matching: find.byType(ScaleTransition)),
+ findsOneWidget,
+ );
+ }
+ });
+}
+
+class StatefulTest extends StatefulWidget {
+ const StatefulTest({Key key}) : super(key: key);
+
+ @override
+ StatefulTestState createState() => new StatefulTestState();
+}
+
+class StatefulTestState extends State<StatefulTest> {
+ StatefulTestState();
+ static int generation = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ generation++;
+ }
+
+ @override
+ Widget build(BuildContext context) => new Container();
}