flutter/packages/flutter/test/widgets/transitions_test.dart
gmilou 58019b3428
Add a new MatrixTransition and refactor ScaleTransition and RotationT… (#131084)
…ransition to derive from it.

The MatrixTransition class uses a callback to handle any value => Matrix animation.

The alignment and filterQuality logic that was in ScaleTransition and RotationTransition is now factored in MatrixTransition.

The ScaleTransition.scale and RotationTransition.turns getters had to be kept because they're still referenced in https://github.com/flutter/packages/tree/main/packages/animations, and https://github.com/flutter/packages/flutter/test/. I plan to remove the references there, once this PR is generally available, and then remove the getters here.

A RotationTransition test was updated to use matrixMoreOrLessEquals because using Matrix4.rotationZ doesn't have the special cases Transform.Rotation had, and zeroes in matrix weren't exactly zeroes.

fixes #130946
2023-08-18 16:50:06 +00:00

835 lines
29 KiB
Dart

// 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());
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());
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());
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());
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,
child: const Text('Ready'),
),
);
await tester.pumpWidget(widget);
final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align));
expect(actualPositionedBox.heightFactor, 0.0);
controller.value = 0.0;
await tester.pump();
expect(actualPositionedBox.heightFactor, 0.0);
controller.value = 0.75;
await tester.pump();
expect(actualPositionedBox.heightFactor, 0.5);
controller.value = 1.0;
await tester.pump();
expect(actualPositionedBox.heightFactor, 1.0);
});
testWidgets('SizeTransition clamps negative size factors - horizontal axis', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
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,
child: const Text('Ready'),
),
);
await tester.pumpWidget(widget);
final RenderPositionedBox actualPositionedBox = tester.renderObject(find.byType(Align));
expect(actualPositionedBox.widthFactor, 0.0);
controller.value = 0.0;
await tester.pump();
expect(actualPositionedBox.widthFactor, 0.0);
controller.value = 0.75;
await tester.pump();
expect(actualPositionedBox.widthFactor, 0.5);
controller.value = 1.0;
await tester.pump();
expect(actualPositionedBox.widthFactor, 1.0);
});
testWidgets('MatrixTransition animates', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: const TestVSync());
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());
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());
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());
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());
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());
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());
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());
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());
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();
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();
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();
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();
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);
}
}