From 1f5fcb7432603fc4399d72394efbc4d13a486d00 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Fri, 18 May 2018 16:27:19 -0700 Subject: [PATCH] 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). --- .../flutter_gallery/lib/gallery/home.dart | 9 - packages/flutter/lib/src/material/chip.dart | 77 +++--- .../lib/src/widgets/animated_switcher.dart | 208 +++++++++------ packages/flutter/test/material/chip_test.dart | 22 +- .../test/widgets/animated_switcher_test.dart | 249 +++++++++++++++++- 5 files changed, 418 insertions(+), 147 deletions(-) diff --git a/examples/flutter_gallery/lib/gallery/home.dart b/examples/flutter_gallery/lib/gallery/home.dart index 66a7885974..c3632bab3c 100644 --- a/examples/flutter_gallery/lib/gallery/home.dart +++ b/examples/flutter_gallery/lib/gallery/home.dart @@ -302,13 +302,6 @@ class _GalleryHomeState extends State with SingleTickerProviderStat super.dispose(); } - static Widget _animatedSwitcherLayoutBuilder(List 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 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 with SingleTickerProviderStat 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 ba8681e191..721fbb8c29 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -1328,7 +1328,7 @@ class _RawChipState extends State with TickerProviderStateMixin with TickerProviderStateMixin with TickerProviderStateMixin 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 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 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 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 children); /// /// @override /// Widget build(BuildContext context) { -/// return new Material( -/// child: Column( -/// mainAxisAlignment: MainAxisAlignment.center, -/// children: [ -/// new AnimatedSwitcher( -/// duration: const Duration(milliseconds: 200), -/// transitionBuilder: (Widget child, Animation 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(_count), -/// textScaleFactor: 3.0, +/// return new MaterialApp( +/// home: new Material( +/// child: Column( +/// mainAxisAlignment: MainAxisAlignment.center, +/// children: [ +/// new AnimatedSwitcher( +/// duration: const Duration(milliseconds: 500), +/// transitionBuilder: (Widget child, Animation 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(_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 @@ 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 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 children) { + /// This is an [AnimatedSwitcherLayoutBuilder] function. + static Widget defaultLayoutBuilder(Widget currentChild, List previousChildren) { + List 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 with TickerProviderStateMixin { - final Set<_AnimatedSwitcherChildEntry> _children = new Set<_AnimatedSwitcherChildEntry>(); + final Set<_AnimatedSwitcherChildEntry> _previousChildren = new Set<_AnimatedSwitcherChildEntry>(); _AnimatedSwitcherChildEntry _currentChild; + List _previousChildWidgetCache = const []; + int serialNumber = 0; @override void initState() { @@ -224,28 +241,26 @@ class _AnimatedSwitcherState extends State with TickerProvider _addEntry(animate: false); } - Widget _generateTransition(Animation animation) { - return new KeyedSubtree( - key: new UniqueKey(), - child: widget.transitionBuilder(widget.child, animation), - ); - } - _AnimatedSwitcherChildEntry _newEntry({ @required AnimationController controller, @required Animation 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 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 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 animation = new CurvedAnimation( @@ -292,39 +320,63 @@ class _AnimatedSwitcherState extends State 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.unmodifiable( + _previousChildren.map((_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 children = _children.map( - (_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 2bbbf1936c..02b33f7800 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -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, diff --git a/packages/flutter/test/widgets/animated_switcher_test.dart b/packages/flutter/test/widgets/animated_switcher_test.dart index 49aea4cc87..0b17710fdc 100644 --- a/packages/flutter/test/widgets/animated_switcher_test.dart +++ b/packages/flutter/test/widgets/animated_switcher_test.dart @@ -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 children) { + Widget newLayoutBuilder(Widget currentChild, List previousChildren) { return new Column( - children: children, + children: previousChildren + [currentChild], ); } @@ -182,12 +187,15 @@ void main() { }); testWidgets('AnimatedSwitcher uses custom transitions.', (WidgetTester tester) async { - final List transitions = []; - Widget newLayoutBuilder(List children) { - transitions.clear(); - transitions.addAll(children); + final List foundChildren = []; + Widget newLayoutBuilder(Widget currentChild, List 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()); + for (Widget child in foundChildren) { + expect(child, const isInstanceOf()); + } + + 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()); 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 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 foundChildren = []; + Widget newLayoutBuilder(Widget currentChild, List 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()); + expect( + find.descendant(of: find.byWidget(child), matching: find.byType(FadeTransition)), + findsOneWidget, + ); + } + + Widget newTransitionBuilder(Widget child, Animation 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()); + 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 { + StatefulTestState(); + static int generation = 0; + + @override + void initState() { + super.initState(); + generation++; + } + + @override + Widget build(BuildContext context) => new Container(); +}