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(); +}