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 ...@@ -302,13 +302,6 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat
super.dispose(); super.dispose();
} }
static Widget _animatedSwitcherLayoutBuilder(List<Widget> children) {
return new Stack(
children: children,
alignment: Alignment.center,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
...@@ -338,7 +331,6 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat ...@@ -338,7 +331,6 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat
duration: _kFrontLayerSwitchDuration, duration: _kFrontLayerSwitchDuration,
switchOutCurve: switchOutCurve, switchOutCurve: switchOutCurve,
switchInCurve: switchInCurve, switchInCurve: switchInCurve,
layoutBuilder: _animatedSwitcherLayoutBuilder,
child: _category == null child: _category == null
? const _FlutterLogo() ? const _FlutterLogo()
: new IconButton( : new IconButton(
...@@ -358,7 +350,6 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat ...@@ -358,7 +350,6 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat
duration: _kFrontLayerSwitchDuration, duration: _kFrontLayerSwitchDuration,
switchOutCurve: switchOutCurve, switchOutCurve: switchOutCurve,
switchInCurve: switchInCurve, switchInCurve: switchInCurve,
layoutBuilder: _animatedSwitcherLayoutBuilder,
child: _category != null child: _category != null
? new _DemosPage(_category) ? new _DemosPage(_category)
: new _CategoriesPage( : new _CategoriesPage(
......
...@@ -1328,7 +1328,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -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) { if (child == null || callback == null || tooltip == null) {
return child; return child;
} }
...@@ -1343,17 +1343,17 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1343,17 +1343,17 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
return null; return null;
} }
return _wrapWithTooltip( return _wrapWithTooltip(
widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context)?.deleteButtonTooltip,
widget.onDeleted,
new InkResponse( new InkResponse(
onTap: widget.isEnabled ? widget.onDeleted : null, onTap: widget.isEnabled ? widget.onDeleted : null,
child: new IconTheme( child: new IconTheme(
data: theme.iconTheme.copyWith( data: theme.iconTheme.copyWith(
color: (widget.deleteIconColor ?? chipTheme.deleteIconColor) ?? theme.iconTheme.color, color: widget.deleteIconColor ?? chipTheme.deleteIconColor,
), ),
child: widget.deleteIcon, child: widget.deleteIcon,
), ),
), ),
widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context)?.deleteButtonTooltip,
widget.onDeleted,
); );
} }
...@@ -1388,42 +1388,43 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip ...@@ -1388,42 +1388,43 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
); );
}, },
child: _wrapWithTooltip( child: _wrapWithTooltip(
new _ChipRenderWidget( widget.tooltip,
theme: new _ChipRenderTheme( widget.onPressed,
label: new DefaultTextStyle( new _ChipRenderWidget(
overflow: TextOverflow.fade, theme: new _ChipRenderTheme(
textAlign: TextAlign.start, label: new DefaultTextStyle(
maxLines: 1, overflow: TextOverflow.fade,
softWrap: false, textAlign: TextAlign.start,
style: widget.labelStyle ?? chipTheme.labelStyle, maxLines: 1,
child: widget.label, softWrap: false,
), style: widget.labelStyle ?? chipTheme.labelStyle,
avatar: new AnimatedSwitcher( child: widget.label,
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,
), ),
value: widget.selected, avatar: new AnimatedSwitcher(
checkmarkAnimation: checkmarkAnimation, child: widget.avatar,
enableAnimation: enableAnimation, duration: _kDrawerDuration,
avatarDrawerAnimation: avatarDrawerAnimation, switchInCurve: Curves.fastOutSlowIn,
deleteDrawerAnimation: deleteDrawerAnimation, ),
isEnabled: widget.isEnabled, 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, value: widget.selected,
widget.onPressed), checkmarkAnimation: checkmarkAnimation,
enableAnimation: enableAnimation,
avatarDrawerAnimation: avatarDrawerAnimation,
deleteDrawerAnimation: deleteDrawerAnimation,
isEnabled: widget.isEnabled,
),
),
), ),
), ),
); );
......
...@@ -38,19 +38,24 @@ class _AnimatedSwitcherChildEntry { ...@@ -38,19 +38,24 @@ class _AnimatedSwitcherChildEntry {
/// Signature for builders used to generate custom transitions for /// Signature for builders used to generate custom transitions for
/// [AnimatedSwitcher]. /// [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 forward direction.
/// ///
/// The function should return a widget which wraps the given [child]. It may /// 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. /// also use the `animation` to inform its transition. It must not return null.
typedef Widget AnimatedSwitcherTransitionBuilder(Widget child, Animation<double> animation); typedef Widget AnimatedSwitcherTransitionBuilder(Widget child, Animation<double> animation);
/// Signature for builders used to generate custom layouts for /// Signature for builders used to generate custom layouts for
/// [AnimatedSwitcher]. /// [AnimatedSwitcher].
/// ///
/// The function should return a widget which contains the given children, laid /// The builder should return a widget which contains the given children, laid
/// out as desired. It must not return null. /// out as desired. It must not return null. The builder should be able to
typedef Widget AnimatedSwitcherLayoutBuilder(List<Widget> children); /// 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 /// A widget that by default does a [FadeTransition] between a new widget and
/// the widget previously set on the [AnimatedSwitcher] as a child. /// the widget previously set on the [AnimatedSwitcher] as a child.
...@@ -63,10 +68,9 @@ typedef Widget AnimatedSwitcherLayoutBuilder(List<Widget> children); ...@@ -63,10 +68,9 @@ typedef Widget AnimatedSwitcherLayoutBuilder(List<Widget> children);
/// different parameters, then [AnimatedSwitcher] will *not* do a /// different parameters, then [AnimatedSwitcher] will *not* do a
/// transition between them, since as far as the framework is concerned, they /// 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 /// 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] /// parameters. To force the transition to occur, set a [Key] (typically a
/// (typically a [ValueKey] taking any widget data that would change the visual /// [ValueKey] taking any widget data that would change the visual appearance
/// appearance of the widget) on each child widget that you wish to be /// of the widget) on each child widget that you wish to be considered unique.
/// considered unique.
/// ///
/// ## Sample code /// ## Sample code
/// ///
...@@ -83,31 +87,35 @@ typedef Widget AnimatedSwitcherLayoutBuilder(List<Widget> children); ...@@ -83,31 +87,35 @@ typedef Widget AnimatedSwitcherLayoutBuilder(List<Widget> children);
/// ///
/// @override /// @override
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// return new Material( /// return new MaterialApp(
/// child: Column( /// home: new Material(
/// mainAxisAlignment: MainAxisAlignment.center, /// child: Column(
/// children: <Widget>[ /// mainAxisAlignment: MainAxisAlignment.center,
/// new AnimatedSwitcher( /// children: <Widget>[
/// duration: const Duration(milliseconds: 200), /// new AnimatedSwitcher(
/// transitionBuilder: (Widget child, Animation<double> animation) { /// duration: const Duration(milliseconds: 500),
/// return new ScaleTransition(child: child, scale: animation); /// transitionBuilder: (Widget child, Animation<double> animation) {
/// }, /// return new ScaleTransition(child: child, scale: animation);
/// child: new Text( /// },
/// '$_count', /// child: new Text(
/// // Must have this key to build a unique widget when _count changes. /// '$_count',
/// key: new ValueKey<int>(_count), /// // This key causes the AnimatedSwitcher to interpret this as a "new"
/// textScaleFactor: 3.0, /// // 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 { ...@@ -156,9 +164,13 @@ class AnimatedSwitcher extends StatefulWidget {
/// The animation curve to use when transitioning the previous [child] out. /// The animation curve to use when transitioning the previous [child] out.
final Curve switchOutCurve; 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 /// 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]. /// The default is [AnimatedSwitcher.defaultTransitionBuilder].
/// ///
...@@ -170,7 +182,8 @@ class AnimatedSwitcher extends StatefulWidget { ...@@ -170,7 +182,8 @@ class AnimatedSwitcher extends StatefulWidget {
/// A function that wraps all of the children that are transitioning out, and /// 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 /// 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]. /// The default is [AnimatedSwitcher.defaultLayoutBuilder].
/// ///
...@@ -183,14 +196,13 @@ class AnimatedSwitcher extends StatefulWidget { ...@@ -183,14 +196,13 @@ class AnimatedSwitcher extends StatefulWidget {
@override @override
_AnimatedSwitcherState createState() => new _AnimatedSwitcherState(); _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 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 /// the animation goes from 0.0 to 1.0, and decreases when the animation is
/// reversed. /// reversed.
/// ///
/// The default value for the [transitionBuilder], an /// This is an [AnimatedSwitcherTransitionBuilder] function.
/// [AnimatedSwitcherTransitionBuilder] function.
static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) { static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return new FadeTransition( return new FadeTransition(
opacity: animation, opacity: animation,
...@@ -198,15 +210,18 @@ class AnimatedSwitcher extends StatefulWidget { ...@@ -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 /// 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 /// largest of the child or a previous child. The children are centered on
/// each other. /// each other.
/// ///
/// This is the default value for [layoutBuilder]. It implements /// This is an [AnimatedSwitcherLayoutBuilder] function.
/// [AnimatedSwitcherLayoutBuilder]. static Widget defaultLayoutBuilder(Widget currentChild, List<Widget> previousChildren) {
static Widget defaultLayoutBuilder(List<Widget> children) { List<Widget> children = previousChildren;
if (currentChild != null) {
children = children.toList()..add(currentChild);
}
return new Stack( return new Stack(
children: children, children: children,
alignment: Alignment.center, alignment: Alignment.center,
...@@ -215,8 +230,10 @@ class AnimatedSwitcher extends StatefulWidget { ...@@ -215,8 +230,10 @@ class AnimatedSwitcher extends StatefulWidget {
} }
class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProviderStateMixin { class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProviderStateMixin {
final Set<_AnimatedSwitcherChildEntry> _children = new Set<_AnimatedSwitcherChildEntry>(); final Set<_AnimatedSwitcherChildEntry> _previousChildren = new Set<_AnimatedSwitcherChildEntry>();
_AnimatedSwitcherChildEntry _currentChild; _AnimatedSwitcherChildEntry _currentChild;
List<Widget> _previousChildWidgetCache = const <Widget>[];
int serialNumber = 0;
@override @override
void initState() { void initState() {
...@@ -224,28 +241,26 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider ...@@ -224,28 +241,26 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider
_addEntry(animate: false); _addEntry(animate: false);
} }
Widget _generateTransition(Animation<double> animation) {
return new KeyedSubtree(
key: new UniqueKey(),
child: widget.transitionBuilder(widget.child, animation),
);
}
_AnimatedSwitcherChildEntry _newEntry({ _AnimatedSwitcherChildEntry _newEntry({
@required AnimationController controller, @required AnimationController controller,
@required Animation<double> animation, @required Animation<double> animation,
}) { }) {
final _AnimatedSwitcherChildEntry entry = new _AnimatedSwitcherChildEntry( final _AnimatedSwitcherChildEntry entry = new _AnimatedSwitcherChildEntry(
widgetChild: widget.child, widgetChild: widget.child,
transition: _generateTransition(animation), transition: KeyedSubtree.wrap(
widget.transitionBuilder(
widget.child,
animation,
),
serialNumber++,
),
animation: animation, animation: animation,
controller: controller, controller: controller,
); );
animation.addStatusListener((AnimationStatus status) { animation.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed) { if (status == AnimationStatus.dismissed) {
assert(_children.contains(entry));
setState(() { setState(() {
_children.remove(entry); _removeExpiredChild(entry);
}); });
controller.dispose(); controller.dispose();
} }
...@@ -253,12 +268,27 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider ...@@ -253,12 +268,27 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider
return entry; 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}) { void _addEntry({@required bool animate}) {
if (widget.child == null) { if (widget.child == null) {
if (animate && _currentChild != null) { if (animate && _currentChild != null) {
_currentChild.controller.reverse(); _retireCurrentChild();
assert(!_children.contains(_currentChild));
_children.add(_currentChild);
} }
_currentChild = null; _currentChild = null;
return; return;
...@@ -269,14 +299,12 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider ...@@ -269,14 +299,12 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider
); );
if (animate) { if (animate) {
if (_currentChild != null) { if (_currentChild != null) {
_currentChild.controller.reverse(); _retireCurrentChild();
assert(!_children.contains(_currentChild));
_children.add(_currentChild);
} }
controller.forward(); controller.forward();
} else { } else {
assert(_currentChild == null); assert(_currentChild == null);
assert(_children.isEmpty); assert(_previousChildren.isEmpty);
controller.value = 1.0; controller.value = 1.0;
} }
final Animation<double> animation = new CurvedAnimation( final Animation<double> animation = new CurvedAnimation(
...@@ -292,39 +320,63 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider ...@@ -292,39 +320,63 @@ class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProvider
if (_currentChild != null) { if (_currentChild != null) {
_currentChild.controller.dispose(); _currentChild.controller.dispose();
} }
for (_AnimatedSwitcherChildEntry child in _children) { for (_AnimatedSwitcherChildEntry child in _previousChildren) {
child.controller.dispose(); child.controller.dispose();
} }
super.dispose(); super.dispose();
} }
bool get hasNewChild => widget.child != null;
bool get hasOldChild => _currentChild != null;
@override @override
void didUpdateWidget(AnimatedSwitcher oldWidget) { void didUpdateWidget(AnimatedSwitcher oldWidget) {
super.didUpdateWidget(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); _addEntry(animate: true);
} else { } 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) { if (_currentChild != null) {
_currentChild.widgetChild = widget.child; _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<Widget> children = _children.map<Widget>( _rebuildChildWidgetCacheIfNeeded();
(_AnimatedSwitcherChildEntry entry) { return widget.layoutBuilder(_currentChild?.transition, _previousChildWidgetCache);
return entry.transition;
},
).toList();
if (_currentChild != null) {
children.add(_currentChild.transition);
}
return widget.layoutBuilder(children);
} }
} }
...@@ -1029,10 +1029,10 @@ void main() { ...@@ -1029,10 +1029,10 @@ void main() {
platform: TargetPlatform.android, platform: TargetPlatform.android,
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
); );
final ChipThemeData chipTheme = themeData.chipTheme; final ChipThemeData defaultChipTheme = themeData.chipTheme;
bool value = false; bool value = false;
Widget buildApp({ Widget buildApp({
ChipThemeData theme, ChipThemeData chipTheme,
Widget avatar, Widget avatar,
Widget deleteIcon, Widget deleteIcon,
bool isSelectable: true, bool isSelectable: true,
...@@ -1040,12 +1040,12 @@ void main() { ...@@ -1040,12 +1040,12 @@ void main() {
bool isDeletable: true, bool isDeletable: true,
bool showCheckmark: true, bool showCheckmark: true,
}) { }) {
theme ??= chipTheme; chipTheme ??= defaultChipTheme;
return _wrapForChip( return _wrapForChip(
child: new Theme( child: new Theme(
data: themeData, data: themeData,
child: new ChipTheme( child: new ChipTheme(
data: theme, data: chipTheme,
child: new StatefulBuilder(builder: (BuildContext context, StateSetter setState) { child: new StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return new RawChip( return new RawChip(
showCheckmark: showCheckmark, showCheckmark: showCheckmark,
...@@ -1054,7 +1054,7 @@ void main() { ...@@ -1054,7 +1054,7 @@ void main() {
avatar: avatar, avatar: avatar,
deleteIcon: deleteIcon, deleteIcon: deleteIcon,
isEnabled: isSelectable || isPressable, isEnabled: isSelectable || isPressable,
shape: theme.shape, shape: chipTheme.shape,
selected: isSelectable ? value : null, selected: isSelectable ? value : null,
label: new Text('$value'), label: new Text('$value'),
onSelected: isSelectable onSelected: isSelectable
...@@ -1085,13 +1085,13 @@ void main() { ...@@ -1085,13 +1085,13 @@ void main() {
DefaultTextStyle labelStyle = getLabelStyle(tester); DefaultTextStyle labelStyle = getLabelStyle(tester);
// Check default theme for enabled widget. // 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(iconData.color, equals(const Color(0xde000000)));
expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde))); expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde)));
await tester.tap(find.byType(RawChip)); await tester.tap(find.byType(RawChip));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
materialBox = getMaterialBox(tester); 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.tap(find.byType(RawChip));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -1100,7 +1100,7 @@ void main() { ...@@ -1100,7 +1100,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
materialBox = getMaterialBox(tester); materialBox = getMaterialBox(tester);
labelStyle = getLabelStyle(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))); expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde)));
// Apply a custom theme. // Apply a custom theme.
...@@ -1108,14 +1108,14 @@ void main() { ...@@ -1108,14 +1108,14 @@ void main() {
const Color customColor2 = const Color(0xdeadbeef); const Color customColor2 = const Color(0xdeadbeef);
const Color customColor3 = const Color(0xbeefcafe); const Color customColor3 = const Color(0xbeefcafe);
const Color customColor4 = const Color(0xaddedabe); const Color customColor4 = const Color(0xaddedabe);
final ChipThemeData customTheme = chipTheme.copyWith( final ChipThemeData customTheme = defaultChipTheme.copyWith(
brightness: Brightness.dark, brightness: Brightness.dark,
backgroundColor: customColor1, backgroundColor: customColor1,
disabledColor: customColor2, disabledColor: customColor2,
selectedColor: customColor3, selectedColor: customColor3,
deleteIconColor: customColor4, deleteIconColor: customColor4,
); );
await tester.pumpWidget(buildApp(theme: customTheme)); await tester.pumpWidget(buildApp(chipTheme: customTheme));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
materialBox = getMaterialBox(tester); materialBox = getMaterialBox(tester);
iconData = getIconData(tester); iconData = getIconData(tester);
...@@ -1134,7 +1134,7 @@ void main() { ...@@ -1134,7 +1134,7 @@ void main() {
// Check custom theme with disabled widget. // Check custom theme with disabled widget.
await tester.pumpWidget(buildApp( await tester.pumpWidget(buildApp(
theme: customTheme, chipTheme: customTheme,
isSelectable: false, isSelectable: false,
isPressable: false, isPressable: false,
isDeletable: true, isDeletable: true,
......
...@@ -19,6 +19,7 @@ void main() { ...@@ -19,6 +19,7 @@ void main() {
), ),
); );
expect(find.byType(FadeTransition), findsOneWidget);
FadeTransition transition = tester.firstWidget(find.byType(FadeTransition)); FadeTransition transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(1.0)); expect(transition.opacity.value, equals(1.0));
...@@ -32,6 +33,7 @@ void main() { ...@@ -32,6 +33,7 @@ void main() {
); );
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
expect(find.byType(FadeTransition), findsNWidgets(2));
transition = tester.firstWidget(find.byType(FadeTransition)); transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(0.5)); expect(transition.opacity.value, equals(0.5));
...@@ -64,6 +66,7 @@ void main() { ...@@ -64,6 +66,7 @@ void main() {
), ),
); );
expect(find.byType(FadeTransition), findsOneWidget);
FadeTransition transition = tester.firstWidget(find.byType(FadeTransition)); FadeTransition transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(1.0)); expect(transition.opacity.value, equals(1.0));
...@@ -77,7 +80,8 @@ void main() { ...@@ -77,7 +80,8 @@ void main() {
); );
await tester.pump(const Duration(milliseconds: 50)); 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)); expect(transition.opacity.value, equals(1.0));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
...@@ -117,6 +121,7 @@ void main() { ...@@ -117,6 +121,7 @@ void main() {
), ),
); );
expect(find.byType(FadeTransition), findsOneWidget);
transition = tester.firstWidget(find.byType(FadeTransition)); transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, equals(1.0)); expect(transition.opacity.value, equals(1.0));
...@@ -163,9 +168,9 @@ void main() { ...@@ -163,9 +168,9 @@ void main() {
}); });
testWidgets('AnimatedSwitcher uses custom layout.', (WidgetTester tester) async { testWidgets('AnimatedSwitcher uses custom layout.', (WidgetTester tester) async {
Widget newLayoutBuilder(List<Widget> children) { Widget newLayoutBuilder(Widget currentChild, List<Widget> previousChildren) {
return new Column( return new Column(
children: children, children: previousChildren + <Widget>[currentChild],
); );
} }
...@@ -182,12 +187,15 @@ void main() { ...@@ -182,12 +187,15 @@ void main() {
}); });
testWidgets('AnimatedSwitcher uses custom transitions.', (WidgetTester tester) async { testWidgets('AnimatedSwitcher uses custom transitions.', (WidgetTester tester) async {
final List<Widget> transitions = <Widget>[]; final List<Widget> foundChildren = <Widget>[];
Widget newLayoutBuilder(List<Widget> children) { Widget newLayoutBuilder(Widget currentChild, List<Widget> previousChildren) {
transitions.clear(); foundChildren.clear();
transitions.addAll(children); if (currentChild != null) {
foundChildren.add(currentChild);
}
foundChildren.addAll(previousChildren);
return new Column( return new Column(
children: children, children: foundChildren,
); );
} }
...@@ -212,12 +220,231 @@ void main() { ...@@ -212,12 +220,231 @@ void main() {
); );
expect(find.byType(Column), findsOneWidget); expect(find.byType(Column), findsOneWidget);
for (Widget transition in transitions) { for (Widget child in foundChildren) {
expect(transition, const isInstanceOf<KeyedSubtree>()); 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( expect(
find.descendant(of: find.byWidget(transition), matching: find.byType(SizeTransition)), find.descendant(of: find.byWidget(child), matching: find.byType(SizeTransition)),
findsOneWidget, 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