Unverified Commit 1f5fcb74 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

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).
parent 8a4db32b
......@@ -302,13 +302,6 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat
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 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat
duration: _kFrontLayerSwitchDuration,
switchOutCurve: switchOutCurve,
switchInCurve: switchInCurve,
layoutBuilder: _animatedSwitcherLayoutBuilder,
child: _category == null
? const _FlutterLogo()
: new IconButton(
......@@ -358,7 +350,6 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat
duration: _kFrontLayerSwitchDuration,
switchOutCurve: switchOutCurve,
switchInCurve: switchInCurve,
layoutBuilder: _animatedSwitcherLayoutBuilder,
child: _category != null
? new _DemosPage(_category)
: new _CategoriesPage(
......
......@@ -1328,7 +1328,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
}
}
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 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
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 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
);
},
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,
),
),
),
),
);
......
......@@ -38,19 +38,24 @@ class _AnimatedSwitcherChildEntry {
/// 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 @@ typedef Widget AnimatedSwitcherLayoutBuilder(List<Widget> children);
/// 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 @@ typedef Widget AnimatedSwitcherLayoutBuilder(List<Widget> children);
///
/// @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: const Text('Increment'),
/// onPressed: () {
/// setState(() {
/// _count += 1;
/// });
/// },
/// ),
/// ),
/// new RaisedButton(
/// child: new Text('Click!'),
/// onPressed: () {
/// setState(() {
/// _count += 1;
/// });
/// },
/// ),
/// ],
/// ],
/// ),
/// ),
/// );
/// }
......@@ -156,9 +164,13 @@ class AnimatedSwitcher extends StatefulWidget {
/// 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 @@ class AnimatedSwitcher extends StatefulWidget {
/// 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 @@ class AnimatedSwitcher extends StatefulWidget {
@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 @@ class AnimatedSwitcher extends StatefulWidget {
);
}
/// 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 AnimatedSwitcher extends StatefulWidget {
}
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 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider
_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 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider
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 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider
);
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 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider
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);
}
}
......@@ -1029,10 +1029,10 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
// Check custom theme with disabled widget.
await tester.pumpWidget(buildApp(
theme: customTheme,
chipTheme: customTheme,
isSelectable: false,
isPressable: false,
isDeletable: true,
......
......@@ -19,6 +19,7 @@ void main() {
),
);
expect(find.byType(FadeTransition), findsOneWidget);
FadeTransition transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(1.0));
......@@ -32,6 +33,7 @@ void main() {
);
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 @@ void main() {
),
);
expect(find.byType(FadeTransition), findsOneWidget);
FadeTransition transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(1.0));
......@@ -77,7 +80,8 @@ void main() {
);
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 @@ void main() {
),
);
expect(find.byType(FadeTransition), findsOneWidget);
transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(1.0));
......@@ -163,9 +168,9 @@ void main() {
});
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 @@ void main() {
});
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 @@ void main() {
);
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();
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment