// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('toString control test', (WidgetTester tester) async { const Widget widget = FadeTransition( opacity: kAlwaysCompleteAnimation, child: Text('Ready', textDirection: TextDirection.ltr), ); expect(widget.toString, isNot(throwsException)); }); group('DecoratedBoxTransition test', () { final DecorationTween decorationTween = DecorationTween( begin: BoxDecoration( color: const Color(0xFFFFFFFF), border: Border.all( width: 4.0, ), borderRadius: BorderRadius.zero, boxShadow: const <BoxShadow>[ BoxShadow( color: Color(0x66000000), blurRadius: 10.0, spreadRadius: 4.0, ), ], ), end: BoxDecoration( color: const Color(0xFF000000), border: Border.all( color: const Color(0xFF202020), ), borderRadius: const BorderRadius.all(Radius.circular(10.0)), // No shadow. ), ); late AnimationController controller; setUp(() { controller = AnimationController(vsync: const TestVSync()); }); testWidgets('decoration test', (WidgetTester tester) async { final DecoratedBoxTransition transitionUnderTest = DecoratedBoxTransition( decoration: decorationTween.animate(controller), child: const Text( "Doesn't matter", textDirection: TextDirection.ltr, ), ); await tester.pumpWidget(transitionUnderTest); RenderDecoratedBox actualBox = tester.renderObject(find.byType(DecoratedBox)); BoxDecoration actualDecoration = actualBox.decoration as BoxDecoration; expect(actualDecoration.color, const Color(0xFFFFFFFF)); expect(actualDecoration.boxShadow![0].blurRadius, 10.0); expect(actualDecoration.boxShadow![0].spreadRadius, 4.0); expect(actualDecoration.boxShadow![0].color, const Color(0x66000000)); controller.value = 0.5; await tester.pump(); actualBox = tester.renderObject(find.byType(DecoratedBox)); actualDecoration = actualBox.decoration as BoxDecoration; expect(actualDecoration.color, const Color(0xFF7F7F7F)); expect(actualDecoration.border, isA<Border>()); final Border border = actualDecoration.border! as Border; expect(border.left.width, 2.5); expect(border.left.style, BorderStyle.solid); expect(border.left.color, const Color(0xFF101010)); expect(actualDecoration.borderRadius, const BorderRadius.all(Radius.circular(5.0))); expect(actualDecoration.shape, BoxShape.rectangle); expect(actualDecoration.boxShadow![0].blurRadius, 5.0); expect(actualDecoration.boxShadow![0].spreadRadius, 2.0); // Scaling a shadow doesn't change the color. expect(actualDecoration.boxShadow![0].color, const Color(0x66000000)); controller.value = 1.0; await tester.pump(); actualBox = tester.renderObject(find.byType(DecoratedBox)); actualDecoration = actualBox.decoration as BoxDecoration; expect(actualDecoration.color, const Color(0xFF000000)); expect(actualDecoration.boxShadow, null); }); testWidgets('animations work with curves test', (WidgetTester tester) async { final Animation<Decoration> curvedDecorationAnimation = decorationTween.animate(CurvedAnimation( parent: controller, curve: Curves.easeOut, )); final DecoratedBoxTransition transitionUnderTest = DecoratedBoxTransition( decoration: curvedDecorationAnimation, position: DecorationPosition.foreground, child: const Text( "Doesn't matter", textDirection: TextDirection.ltr, ), ); await tester.pumpWidget(transitionUnderTest); RenderDecoratedBox actualBox = tester.renderObject(find.byType(DecoratedBox)); BoxDecoration actualDecoration = actualBox.decoration as BoxDecoration; expect(actualDecoration.color, const Color(0xFFFFFFFF)); expect(actualDecoration.boxShadow![0].blurRadius, 10.0); expect(actualDecoration.boxShadow![0].spreadRadius, 4.0); expect(actualDecoration.boxShadow![0].color, const Color(0x66000000)); controller.value = 0.5; await tester.pump(); actualBox = tester.renderObject(find.byType(DecoratedBox)); actualDecoration = actualBox.decoration as BoxDecoration; // Same as the test above but the values should be much closer to the // tween's end values given the easeOut curve. expect(actualDecoration.color, const Color(0xFF505050)); expect(actualDecoration.border, isA<Border>()); final Border border = actualDecoration.border! as Border; expect(border.left.width, moreOrLessEquals(1.9, epsilon: 0.1)); expect(border.left.style, BorderStyle.solid); expect(border.left.color, const Color(0xFF151515)); expect(actualDecoration.borderRadius!.resolve(TextDirection.ltr).topLeft.x, moreOrLessEquals(6.8, epsilon: 0.1)); expect(actualDecoration.shape, BoxShape.rectangle); expect(actualDecoration.boxShadow![0].blurRadius, moreOrLessEquals(3.1, epsilon: 0.1)); expect(actualDecoration.boxShadow![0].spreadRadius, moreOrLessEquals(1.2, epsilon: 0.1)); // Scaling a shadow doesn't change the color. expect(actualDecoration.boxShadow![0].color, const Color(0x66000000)); }); }); testWidgets('AlignTransition animates', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<Alignment> alignmentTween = AlignmentTween( begin: Alignment.centerLeft, end: Alignment.bottomRight, ).animate(controller); final Widget widget = AlignTransition( alignment: alignmentTween, child: const Text('Ready', textDirection: TextDirection.ltr), ); await tester.pumpWidget(widget); final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align)); Alignment actualAlignment = actualPositionedBox.alignment as Alignment; expect(actualAlignment, Alignment.centerLeft); controller.value = 0.5; await tester.pump(); actualAlignment = actualPositionedBox.alignment as Alignment; expect(actualAlignment, const Alignment(0.0, 0.5)); }); testWidgets('RelativePositionedTransition animates', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<Rect?> rectTween = RectTween( begin: const Rect.fromLTWH(0, 0, 30, 40), end: const Rect.fromLTWH(100, 200, 100, 200), ).animate(controller); final Widget widget = Directionality( textDirection: TextDirection.rtl, child: Stack( alignment: Alignment.centerLeft, children: <Widget>[ RelativePositionedTransition( size: const Size(200, 300), rect: rectTween, child: const Placeholder(), ), ], ), ); await tester.pumpWidget(widget); final Positioned actualPositioned = tester.widget(find.byType(Positioned)); final RenderBox renderBox = tester.renderObject(find.byType(Placeholder)); Rect actualRect = Rect.fromLTRB( actualPositioned.left!, actualPositioned.top!, actualPositioned.right ?? 0.0, actualPositioned.bottom ?? 0.0, ); expect(actualRect, equals(const Rect.fromLTRB(0, 0, 170, 260))); expect(renderBox.size, equals(const Size(630, 340))); controller.value = 0.5; await tester.pump(); actualRect = Rect.fromLTRB( actualPositioned.left!, actualPositioned.top!, actualPositioned.right ?? 0.0, actualPositioned.bottom ?? 0.0, ); expect(actualRect, equals(const Rect.fromLTWH(0, 0, 170, 260))); expect(renderBox.size, equals(const Size(665, 420))); }); testWidgets('AlignTransition keeps width and height factors', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<Alignment> alignmentTween = AlignmentTween( begin: Alignment.centerLeft, end: Alignment.bottomRight, ).animate(controller); final Widget widget = AlignTransition( alignment: alignmentTween, widthFactor: 0.3, heightFactor: 0.4, child: const Text('Ready', textDirection: TextDirection.ltr), ); await tester.pumpWidget(widget); final Align actualAlign = tester.widget(find.byType(Align)); expect(actualAlign.widthFactor, 0.3); expect(actualAlign.heightFactor, 0.4); }); testWidgets('SizeTransition clamps negative size factors - vertical axis', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<double> animation = Tween<double>(begin: -1.0, end: 1.0).animate(controller); final Widget widget = Directionality( textDirection: TextDirection.ltr, child: SizeTransition( sizeFactor: animation, fixedCrossAxisSizeFactor: 2.0, child: const Text('Ready'), ), ); await tester.pumpWidget(widget); final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align)); expect(actualPositionedBox.heightFactor, 0.0); expect(actualPositionedBox.widthFactor, 2.0); controller.value = 0.0; await tester.pump(); expect(actualPositionedBox.heightFactor, 0.0); expect(actualPositionedBox.widthFactor, 2.0); controller.value = 0.75; await tester.pump(); expect(actualPositionedBox.heightFactor, 0.5); expect(actualPositionedBox.widthFactor, 2.0); controller.value = 1.0; await tester.pump(); expect(actualPositionedBox.heightFactor, 1.0); expect(actualPositionedBox.widthFactor, 2.0); }); testWidgets('SizeTransition clamps negative size factors - horizontal axis', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<double> animation = Tween<double>(begin: -1.0, end: 1.0).animate(controller); final Widget widget = Directionality( textDirection: TextDirection.ltr, child: SizeTransition( axis: Axis.horizontal, sizeFactor: animation, fixedCrossAxisSizeFactor: 1.0, child: const Text('Ready'), ), ); await tester.pumpWidget(widget); final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align)); expect(actualPositionedBox.widthFactor, 0.0); expect(actualPositionedBox.heightFactor, 1.0); controller.value = 0.0; await tester.pump(); expect(actualPositionedBox.widthFactor, 0.0); expect(actualPositionedBox.heightFactor, 1.0); controller.value = 0.75; await tester.pump(); expect(actualPositionedBox.widthFactor, 0.5); expect(actualPositionedBox.heightFactor, 1.0); controller.value = 1.0; await tester.pump(); expect(actualPositionedBox.widthFactor, 1.0); expect(actualPositionedBox.heightFactor, 1.0); }); testWidgets('SizeTransition with fixedCrossAxisSizeFactor should size its cross axis from its children - vertical axis', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<double> animation = Tween<double>(begin: 0, end: 1.0).animate(controller); const Key key = Key('key'); final Widget widget = Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( key: key, child: SizeTransition( sizeFactor: animation, fixedCrossAxisSizeFactor: 1.0, child: const SizedBox.square(dimension: 100), ), ), ), ); await tester.pumpWidget(widget); final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align)); expect(actualPositionedBox.heightFactor, 0.0); expect(actualPositionedBox.widthFactor, 1.0); expect(tester.getSize(find.byKey(key)), const Size(100, 0)); controller.value = 0.0; await tester.pump(); expect(actualPositionedBox.heightFactor, 0.0); expect(actualPositionedBox.widthFactor, 1.0); expect(tester.getSize(find.byKey(key)), const Size(100, 0)); controller.value = 0.5; await tester.pump(); expect(actualPositionedBox.heightFactor, 0.5); expect(actualPositionedBox.widthFactor, 1.0); expect(tester.getSize(find.byKey(key)), const Size(100, 50)); controller.value = 1.0; await tester.pump(); expect(actualPositionedBox.heightFactor, 1.0); expect(actualPositionedBox.widthFactor, 1.0); expect(tester.getSize(find.byKey(key)), const Size.square(100)); controller.value = 0.5; await tester.pump(); expect(actualPositionedBox.heightFactor, 0.5); expect(actualPositionedBox.widthFactor, 1.0); expect(tester.getSize(find.byKey(key)), const Size(100, 50)); controller.value = 0.0; await tester.pump(); expect(actualPositionedBox.heightFactor, 0.0); expect(actualPositionedBox.widthFactor, 1.0); expect(tester.getSize(find.byKey(key)), const Size(100, 0)); }); testWidgets('SizeTransition with fixedCrossAxisSizeFactor should size its cross axis from its children - horizontal axis', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); const Key key = Key('key'); final Widget widget = Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( key: key, child: SizeTransition( axis: Axis.horizontal, sizeFactor: animation, fixedCrossAxisSizeFactor: 1.0, child: const SizedBox.square(dimension: 100), ), ), ), ); await tester.pumpWidget(widget); final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align)); expect(actualPositionedBox.heightFactor, 1.0); expect(actualPositionedBox.widthFactor, 0.0); expect(tester.getSize(find.byKey(key)), const Size(0, 100)); controller.value = 0.0; await tester.pump(); expect(actualPositionedBox.heightFactor, 1.0); expect(actualPositionedBox.widthFactor, 0.0); expect(tester.getSize(find.byKey(key)), const Size(0, 100)); controller.value = 0.5; await tester.pump(); expect(actualPositionedBox.heightFactor, 1.0); expect(actualPositionedBox.widthFactor, 0.5); expect(tester.getSize(find.byKey(key)), const Size(50, 100)); controller.value = 1.0; await tester.pump(); expect(actualPositionedBox.heightFactor, 1.0); expect(actualPositionedBox.widthFactor, 1.0); expect(tester.getSize(find.byKey(key)), const Size.square(100)); controller.value = 0.5; await tester.pump(); expect(actualPositionedBox.heightFactor, 1.0); expect(actualPositionedBox.widthFactor, 0.5); expect(tester.getSize(find.byKey(key)), const Size(50, 100)); controller.value = 0.0; await tester.pump(); expect(actualPositionedBox.heightFactor, 1.0); expect(actualPositionedBox.widthFactor, 0.0); expect(tester.getSize(find.byKey(key)), const Size(0, 100)); }); testWidgets('MatrixTransition animates', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Widget widget = MatrixTransition( alignment: Alignment.topRight, onTransform: (double value) => Matrix4.translationValues(value, value, value), animation: controller, child: const Text( 'Matrix', textDirection: TextDirection.ltr, ), ); await tester.pumpWidget(widget); Transform actualTransformedBox = tester.widget(find.byType(Transform)); Matrix4 actualTransform = actualTransformedBox.transform; expect(actualTransform, equals(Matrix4.rotationZ(0.0))); controller.value = 0.5; await tester.pump(); actualTransformedBox = tester.widget(find.byType(Transform)); actualTransform = actualTransformedBox.transform; expect(actualTransform, Matrix4.fromList(<double>[ 1.0, 0.0, 0.0, 0.5, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 1.0, 0.5, 0.0, 0.0, 0.0, 1.0, ])..transpose()); controller.value = 0.75; await tester.pump(); actualTransformedBox = tester.widget(find.byType(Transform)); actualTransform = actualTransformedBox.transform; expect(actualTransform, Matrix4.fromList(<double>[ 1.0, 0.0, 0.0, 0.75, 0.0, 1.0, 0.0, 0.75, 0.0, 0.0, 1.0, 0.75, 0.0, 0.0, 0.0, 1.0, ])..transpose()); }); testWidgets('MatrixTransition maintains chosen alignment during animation', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Widget widget = MatrixTransition( alignment: Alignment.topRight, onTransform: (double value) => Matrix4.identity(), animation: controller, child: const Text('Matrix', textDirection: TextDirection.ltr), ); await tester.pumpWidget(widget); MatrixTransition actualTransformedBox = tester.widget(find.byType(MatrixTransition)); Alignment actualAlignment = actualTransformedBox.alignment; expect(actualAlignment, Alignment.topRight); controller.value = 0.5; await tester.pump(); actualTransformedBox = tester.widget(find.byType(MatrixTransition)); actualAlignment = actualTransformedBox.alignment; expect(actualAlignment, Alignment.topRight); }); testWidgets('RotationTransition animates', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Widget widget = RotationTransition( alignment: Alignment.topRight, turns: controller, child: const Text( 'Rotation', textDirection: TextDirection.ltr, ), ); await tester.pumpWidget(widget); Transform actualRotatedBox = tester.widget(find.byType(Transform)); Matrix4 actualTurns = actualRotatedBox.transform; expect(actualTurns, equals(Matrix4.rotationZ(0.0))); controller.value = 0.5; await tester.pump(); actualRotatedBox = tester.widget(find.byType(Transform)); actualTurns = actualRotatedBox.transform; expect(actualTurns, matrixMoreOrLessEquals(Matrix4.fromList(<double>[ -1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ])..transpose())); controller.value = 0.75; await tester.pump(); actualRotatedBox = tester.widget(find.byType(Transform)); actualTurns = actualRotatedBox.transform; expect(actualTurns, matrixMoreOrLessEquals(Matrix4.fromList(<double>[ 0.0, 1.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ])..transpose())); }); testWidgets('RotationTransition maintains chosen alignment during animation', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Widget widget = RotationTransition( alignment: Alignment.topRight, turns: controller, child: const Text('Rotation', textDirection: TextDirection.ltr), ); await tester.pumpWidget(widget); RotationTransition actualRotatedBox = tester.widget(find.byType(RotationTransition)); Alignment actualAlignment = actualRotatedBox.alignment; expect(actualAlignment, Alignment.topRight); controller.value = 0.5; await tester.pump(); actualRotatedBox = tester.widget(find.byType(RotationTransition)); actualAlignment = actualRotatedBox.alignment; expect(actualAlignment, Alignment.topRight); }); group('FadeTransition', () { double getOpacity(WidgetTester tester, String textValue) { final FadeTransition opacityWidget = tester.widget<FadeTransition>( find.ancestor( of: find.text(textValue), matching: find.byType(FadeTransition), ).first, ); return opacityWidget.opacity.value; } testWidgets('animates', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); final Widget widget = Directionality( textDirection: TextDirection.ltr, child: FadeTransition( opacity: animation, child: const Text('Fade In'), ), ); await tester.pumpWidget(widget); expect(getOpacity(tester, 'Fade In'), 0.0); controller.value = 0.25; await tester.pump(); expect(getOpacity(tester, 'Fade In'), 0.25); controller.value = 0.5; await tester.pump(); expect(getOpacity(tester, 'Fade In'), 0.5); controller.value = 0.75; await tester.pump(); expect(getOpacity(tester, 'Fade In'), 0.75); controller.value = 1.0; await tester.pump(); expect(getOpacity(tester, 'Fade In'), 1.0); }); }); group('SliverFadeTransition', () { double getOpacity(WidgetTester tester, String textValue) { final SliverFadeTransition opacityWidget = tester.widget<SliverFadeTransition>( find.ancestor( of: find.text(textValue), matching: find.byType(SliverFadeTransition), ).first, ); return opacityWidget.opacity.value; } testWidgets('animates', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); final Widget widget = Localizations( locale: const Locale('en', 'us'), delegates: const <LocalizationsDelegate<dynamic>>[ DefaultWidgetsLocalizations.delegate, DefaultMaterialLocalizations.delegate, ], child: Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(), child: CustomScrollView( slivers: <Widget>[ SliverFadeTransition( opacity: animation, sliver: const SliverToBoxAdapter( child: Text('Fade In'), ), ), ], ), ), ), ); await tester.pumpWidget(widget); expect(getOpacity(tester, 'Fade In'), 0.0); controller.value = 0.25; await tester.pump(); expect(getOpacity(tester, 'Fade In'), 0.25); controller.value = 0.5; await tester.pump(); expect(getOpacity(tester, 'Fade In'), 0.5); controller.value = 0.75; await tester.pump(); expect(getOpacity(tester, 'Fade In'), 0.75); controller.value = 1.0; await tester.pump(); expect(getOpacity(tester, 'Fade In'), 1.0); }); }); group('MatrixTransition', () { testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); final Widget widget = Directionality( textDirection: TextDirection.ltr, child: MatrixTransition( animation: animation, onTransform: (double value) => Matrix4.identity(), filterQuality: FilterQuality.none, child: const Text('Matrix Transition'), ), ); await tester.pumpWidget(widget); // Validate that expensive layer is not left in tree before animation has started. expect(tester.layers, isNot(contains(isA<ImageFilterLayer>()))); controller.value = 0.25; await tester.pump(); expect( tester.layers, contains(isA<ImageFilterLayer>().having( (ImageFilterLayer layer) => layer.imageFilter.toString(), 'image filter', startsWith('ImageFilter.matrix('), )), ); controller.value = 0.5; await tester.pump(); expect( tester.layers, contains(isA<ImageFilterLayer>().having( (ImageFilterLayer layer) => layer.imageFilter.toString(), 'image filter', startsWith('ImageFilter.matrix('), )), ); controller.value = 0.75; await tester.pump(); expect( tester.layers, contains(isA<ImageFilterLayer>().having( (ImageFilterLayer layer) => layer.imageFilter.toString(), 'image filter', startsWith('ImageFilter.matrix('), )), ); controller.value = 1; await tester.pump(); // Validate that expensive layer is not left in tree after animation has finished. expect(tester.layers, isNot(contains(isA<ImageFilterLayer>()))); }); }); group('ScaleTransition', () { testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); final Widget widget = Directionality( textDirection: TextDirection.ltr, child: ScaleTransition( scale: animation, filterQuality: FilterQuality.none, child: const Text('Scale Transition'), ), ); await tester.pumpWidget(widget); // Validate that expensive layer is not left in tree before animation has started. expect(tester.layers, isNot(contains(isA<ImageFilterLayer>()))); controller.value = 0.25; await tester.pump(); expect(tester.layers, contains(isA<ImageFilterLayer>().having( (ImageFilterLayer layer) => layer.imageFilter.toString(), 'image filter', startsWith('ImageFilter.matrix('), ))); controller.value = 0.5; await tester.pump(); expect(tester.layers, contains(isA<ImageFilterLayer>().having( (ImageFilterLayer layer) => layer.imageFilter.toString(), 'image filter', startsWith('ImageFilter.matrix('), ))); controller.value = 0.75; await tester.pump(); expect(tester.layers, contains(isA<ImageFilterLayer>().having( (ImageFilterLayer layer) => layer.imageFilter.toString(), 'image filter', startsWith('ImageFilter.matrix('), ))); controller.value = 1; await tester.pump(); // Validate that expensive layer is not left in tree after animation has finished. expect(tester.layers, isNot(contains(isA<ImageFilterLayer>()))); }); }); group('RotationTransition', () { testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async { final AnimationController controller = AnimationController(vsync: const TestVSync()); addTearDown(controller.dispose); final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); final Widget widget = Directionality( textDirection: TextDirection.ltr, child: RotationTransition( turns: animation, filterQuality: FilterQuality.none, child: const Text('Scale Transition'), ), ); await tester.pumpWidget(widget); // Validate that expensive layer is not left in tree before animation has started. expect(tester.layers, isNot(contains(isA<ImageFilterLayer>()))); controller.value = 0.25; await tester.pump(); expect(tester.layers, contains(isA<ImageFilterLayer>().having( (ImageFilterLayer layer) => layer.imageFilter.toString(), 'image filter', startsWith('ImageFilter.matrix('), ))); controller.value = 0.5; await tester.pump(); expect(tester.layers, contains(isA<ImageFilterLayer>().having( (ImageFilterLayer layer) => layer.imageFilter.toString(), 'image filter', startsWith('ImageFilter.matrix('), ))); controller.value = 0.75; await tester.pump(); expect(tester.layers, contains(isA<ImageFilterLayer>().having( (ImageFilterLayer layer) => layer.imageFilter.toString(), 'image filter', startsWith('ImageFilter.matrix('), ))); controller.value = 1; await tester.pump(); // Validate that expensive layer is not left in tree after animation has finished. expect(tester.layers, isNot(contains(isA<ImageFilterLayer>()))); }); }); group('Builders', () { testWidgets('AnimatedBuilder rebuilds when changed', (WidgetTester tester) async { final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>(); final ChangeNotifier notifier = ChangeNotifier(); addTearDown(notifier.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: AnimatedBuilder( animation: notifier, builder: (BuildContext context, Widget? child) { return RedrawCounter(key: redrawKey, child: child); }, ), ), ); expect(redrawKey.currentState!.redraws, equals(1)); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(1)); notifier.notifyListeners(); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(2)); // Pump a few more times to make sure that we don't rebuild unnecessarily. await tester.pump(); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(2)); }); testWidgets("AnimatedBuilder doesn't rebuild the child", (WidgetTester tester) async { final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>(); final GlobalKey<RedrawCounterState> redrawKeyChild = GlobalKey<RedrawCounterState>(); final ChangeNotifier notifier = ChangeNotifier(); addTearDown(notifier.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: AnimatedBuilder( animation: notifier, builder: (BuildContext context, Widget? child) { return RedrawCounter(key: redrawKey, child: child); }, child: RedrawCounter(key: redrawKeyChild), ), ), ); expect(redrawKey.currentState!.redraws, equals(1)); expect(redrawKeyChild.currentState!.redraws, equals(1)); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(1)); expect(redrawKeyChild.currentState!.redraws, equals(1)); notifier.notifyListeners(); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(2)); expect(redrawKeyChild.currentState!.redraws, equals(1)); // Pump a few more times to make sure that we don't rebuild unnecessarily. await tester.pump(); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(2)); expect(redrawKeyChild.currentState!.redraws, equals(1)); }); testWidgets('ListenableBuilder rebuilds when changed', (WidgetTester tester) async { final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>(); final ChangeNotifier notifier = ChangeNotifier(); addTearDown(notifier.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: ListenableBuilder( listenable: notifier, builder: (BuildContext context, Widget? child) { return RedrawCounter(key: redrawKey, child: child); }, ), ), ); expect(redrawKey.currentState!.redraws, equals(1)); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(1)); notifier.notifyListeners(); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(2)); // Pump a few more times to make sure that we don't rebuild unnecessarily. await tester.pump(); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(2)); }); testWidgets("ListenableBuilder doesn't rebuild the child", (WidgetTester tester) async { final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>(); final GlobalKey<RedrawCounterState> redrawKeyChild = GlobalKey<RedrawCounterState>(); final ChangeNotifier notifier = ChangeNotifier(); addTearDown(notifier.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: ListenableBuilder( listenable: notifier, builder: (BuildContext context, Widget? child) { return RedrawCounter(key: redrawKey, child: child); }, child: RedrawCounter(key: redrawKeyChild), ), ), ); expect(redrawKey.currentState!.redraws, equals(1)); expect(redrawKeyChild.currentState!.redraws, equals(1)); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(1)); expect(redrawKeyChild.currentState!.redraws, equals(1)); notifier.notifyListeners(); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(2)); expect(redrawKeyChild.currentState!.redraws, equals(1)); // Pump a few more times to make sure that we don't rebuild unnecessarily. await tester.pump(); await tester.pump(); expect(redrawKey.currentState!.redraws, equals(2)); expect(redrawKeyChild.currentState!.redraws, equals(1)); }); }); } class RedrawCounter extends StatefulWidget { const RedrawCounter({ super.key, this.child }); final Widget? child; @override State<RedrawCounter> createState() => RedrawCounterState(); } class RedrawCounterState extends State<RedrawCounter> { int redraws = 0; @override Widget build(BuildContext context) { redraws += 1; return SizedBox(child: widget.child); } }