Skip to content

Commit ea19a77

Browse files
[framework] elide ImageFilter layers when animation is stopped (#101731)
1 parent 8824603 commit ea19a77

File tree

2 files changed

+144
-2
lines changed

2 files changed

+144
-2
lines changed

packages/flutter/lib/src/widgets/transitions.dart

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ class ScaleTransition extends AnimatedWidget {
274274

275275
/// The filter quality with which to apply the transform as a bitmap operation.
276276
///
277+
/// When the animation is stopped (either in [AnimationStatus.dismissed] or
278+
/// [AnimationStatus.completed]), the filter quality argument will be ignored.
279+
///
277280
/// {@macro flutter.widgets.Transform.optional.FilterQuality}
278281
final FilterQuality? filterQuality;
279282

@@ -284,10 +287,25 @@ class ScaleTransition extends AnimatedWidget {
284287

285288
@override
286289
Widget build(BuildContext context) {
290+
// The ImageFilter layer created by setting filterQuality will introduce
291+
// a saveLayer call. This is usually worthwhile when animating the layer,
292+
// but leaving it in the layer tree before the animation has started or after
293+
// it has finished significantly hurts performance.
294+
final bool useFilterQuality;
295+
switch (scale.status) {
296+
case AnimationStatus.dismissed:
297+
case AnimationStatus.completed:
298+
useFilterQuality = false;
299+
break;
300+
case AnimationStatus.forward:
301+
case AnimationStatus.reverse:
302+
useFilterQuality = true;
303+
break;
304+
}
287305
return Transform.scale(
288306
scale: scale.value,
289307
alignment: alignment,
290-
filterQuality: filterQuality,
308+
filterQuality: useFilterQuality ? filterQuality : null,
291309
child: child,
292310
);
293311
}
@@ -340,6 +358,9 @@ class RotationTransition extends AnimatedWidget {
340358

341359
/// The filter quality with which to apply the transform as a bitmap operation.
342360
///
361+
/// When the animation is stopped (either in [AnimationStatus.dismissed] or
362+
/// [AnimationStatus.completed]), the filter quality argument will be ignored.
363+
///
343364
/// {@macro flutter.widgets.Transform.optional.FilterQuality}
344365
final FilterQuality? filterQuality;
345366

@@ -350,10 +371,25 @@ class RotationTransition extends AnimatedWidget {
350371

351372
@override
352373
Widget build(BuildContext context) {
374+
// The ImageFilter layer created by setting filterQuality will introduce
375+
// a saveLayer call. This is usually worthwhile when animating the layer,
376+
// but leaving it in the layer tree before the animation has started or after
377+
// it has finished significantly hurts performance.
378+
final bool useFilterQuality;
379+
switch (turns.status) {
380+
case AnimationStatus.dismissed:
381+
case AnimationStatus.completed:
382+
useFilterQuality = false;
383+
break;
384+
case AnimationStatus.forward:
385+
case AnimationStatus.reverse:
386+
useFilterQuality = true;
387+
break;
388+
}
353389
return Transform.rotate(
354390
angle: turns.value * math.pi * 2.0,
355391
alignment: alignment,
356-
filterQuality: filterQuality,
392+
filterQuality: useFilterQuality ? filterQuality : null,
357393
child: child,
358394
);
359395
}

packages/flutter/test/widgets/transitions_test.dart

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,4 +446,110 @@ void main() {
446446
expect(_getOpacity(tester, 'Fade In'), 1.0);
447447
});
448448
});
449+
450+
group('ScaleTransition', () {
451+
testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async {
452+
final AnimationController controller = AnimationController(vsync: const TestVSync());
453+
final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
454+
final Widget widget = Directionality(
455+
textDirection: TextDirection.ltr,
456+
child: ScaleTransition(
457+
scale: animation,
458+
filterQuality: FilterQuality.none,
459+
child: const Text('Scale Transition'),
460+
),
461+
);
462+
463+
await tester.pumpWidget(widget);
464+
465+
// Validate that expensive layer is not left in tree before animation has started.
466+
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
467+
468+
controller.value = 0.25;
469+
await tester.pump();
470+
471+
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
472+
(ImageFilterLayer layer) => layer.imageFilter.toString(),
473+
'image filter',
474+
startsWith('ImageFilter.matrix('),
475+
)));
476+
477+
controller.value = 0.5;
478+
await tester.pump();
479+
480+
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
481+
(ImageFilterLayer layer) => layer.imageFilter.toString(),
482+
'image filter',
483+
startsWith('ImageFilter.matrix('),
484+
)));
485+
486+
controller.value = 0.75;
487+
await tester.pump();
488+
489+
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
490+
(ImageFilterLayer layer) => layer.imageFilter.toString(),
491+
'image filter',
492+
startsWith('ImageFilter.matrix('),
493+
)));
494+
495+
controller.value = 1;
496+
await tester.pump();
497+
498+
// Validate that expensive layer is not left in tree after animation has finished.
499+
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
500+
});
501+
});
502+
503+
group('RotationTransition', () {
504+
testWidgets('uses ImageFilter when provided with FilterQuality argument', (WidgetTester tester) async {
505+
final AnimationController controller = AnimationController(vsync: const TestVSync());
506+
final Animation<double> animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller);
507+
final Widget widget = Directionality(
508+
textDirection: TextDirection.ltr,
509+
child: RotationTransition(
510+
turns: animation,
511+
filterQuality: FilterQuality.none,
512+
child: const Text('Scale Transition'),
513+
),
514+
);
515+
516+
await tester.pumpWidget(widget);
517+
518+
// Validate that expensive layer is not left in tree before animation has started.
519+
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
520+
521+
controller.value = 0.25;
522+
await tester.pump();
523+
524+
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
525+
(ImageFilterLayer layer) => layer.imageFilter.toString(),
526+
'image filter',
527+
startsWith('ImageFilter.matrix('),
528+
)));
529+
530+
controller.value = 0.5;
531+
await tester.pump();
532+
533+
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
534+
(ImageFilterLayer layer) => layer.imageFilter.toString(),
535+
'image filter',
536+
startsWith('ImageFilter.matrix('),
537+
)));
538+
539+
controller.value = 0.75;
540+
await tester.pump();
541+
542+
expect(tester.layers, contains(isA<ImageFilterLayer>().having(
543+
(ImageFilterLayer layer) => layer.imageFilter.toString(),
544+
'image filter',
545+
startsWith('ImageFilter.matrix('),
546+
)));
547+
548+
controller.value = 1;
549+
await tester.pump();
550+
551+
// Validate that expensive layer is not left in tree after animation has finished.
552+
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
553+
});
554+
});
449555
}

0 commit comments

Comments
 (0)