Unverified Commit ea19a77b authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[framework] elide ImageFilter layers when animation is stopped (#101731)

parent 88246030
...@@ -274,6 +274,9 @@ class ScaleTransition extends AnimatedWidget { ...@@ -274,6 +274,9 @@ class ScaleTransition extends AnimatedWidget {
/// The filter quality with which to apply the transform as a bitmap operation. /// The filter quality with which to apply the transform as a bitmap operation.
/// ///
/// When the animation is stopped (either in [AnimationStatus.dismissed] or
/// [AnimationStatus.completed]), the filter quality argument will be ignored.
///
/// {@macro flutter.widgets.Transform.optional.FilterQuality} /// {@macro flutter.widgets.Transform.optional.FilterQuality}
final FilterQuality? filterQuality; final FilterQuality? filterQuality;
...@@ -284,10 +287,25 @@ class ScaleTransition extends AnimatedWidget { ...@@ -284,10 +287,25 @@ class ScaleTransition extends AnimatedWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// The ImageFilter layer created by setting filterQuality will introduce
// a saveLayer call. This is usually worthwhile when animating the layer,
// but leaving it in the layer tree before the animation has started or after
// it has finished significantly hurts performance.
final bool useFilterQuality;
switch (scale.status) {
case AnimationStatus.dismissed:
case AnimationStatus.completed:
useFilterQuality = false;
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
useFilterQuality = true;
break;
}
return Transform.scale( return Transform.scale(
scale: scale.value, scale: scale.value,
alignment: alignment, alignment: alignment,
filterQuality: filterQuality, filterQuality: useFilterQuality ? filterQuality : null,
child: child, child: child,
); );
} }
...@@ -340,6 +358,9 @@ class RotationTransition extends AnimatedWidget { ...@@ -340,6 +358,9 @@ class RotationTransition extends AnimatedWidget {
/// The filter quality with which to apply the transform as a bitmap operation. /// The filter quality with which to apply the transform as a bitmap operation.
/// ///
/// When the animation is stopped (either in [AnimationStatus.dismissed] or
/// [AnimationStatus.completed]), the filter quality argument will be ignored.
///
/// {@macro flutter.widgets.Transform.optional.FilterQuality} /// {@macro flutter.widgets.Transform.optional.FilterQuality}
final FilterQuality? filterQuality; final FilterQuality? filterQuality;
...@@ -350,10 +371,25 @@ class RotationTransition extends AnimatedWidget { ...@@ -350,10 +371,25 @@ class RotationTransition extends AnimatedWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// The ImageFilter layer created by setting filterQuality will introduce
// a saveLayer call. This is usually worthwhile when animating the layer,
// but leaving it in the layer tree before the animation has started or after
// it has finished significantly hurts performance.
final bool useFilterQuality;
switch (turns.status) {
case AnimationStatus.dismissed:
case AnimationStatus.completed:
useFilterQuality = false;
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
useFilterQuality = true;
break;
}
return Transform.rotate( return Transform.rotate(
angle: turns.value * math.pi * 2.0, angle: turns.value * math.pi * 2.0,
alignment: alignment, alignment: alignment,
filterQuality: filterQuality, filterQuality: useFilterQuality ? filterQuality : null,
child: child, child: child,
); );
} }
......
...@@ -446,4 +446,110 @@ void main() { ...@@ -446,4 +446,110 @@ void main() {
expect(_getOpacity(tester, 'Fade In'), 1.0); expect(_getOpacity(tester, 'Fade In'), 1.0);
}); });
}); });
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>())));
});
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment