Skip to content

Commit c4c5452

Browse files
author
Jonah Williams
authored
Support backdrop key in flutter framework. (#157278)
The backdrop key functionality allows multiple backdrop filters to share the same input filter, dramatically improving raster performance. This is only supported on the Impeller backend. The backdrop key class allocates a new int from a static and passes this to the engine layer. with 64 bit integers, we can allocate many backdrop filter ids per frame and never run out. See also: flutter/flutter#156455 ```dart import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; final _random = Random(); void main() => runApp(const BackdropFilterDemo()); class BackdropFilterDemo extends StatelessWidget { const BackdropFilterDemo({super.key}); static final listKey = BackdropKey(); static final overlayKey = BackdropKey(); @OverRide Widget build(BuildContext context) { return MaterialApp( home: Scaffold( backgroundColor: Colors.white, body: Stack( children: [ ListView.builder( itemCount: 120, // 60 pairs of red and blue containers itemBuilder: (context, index) { return Container( height: 100, color: index % 2 == 0 ? Colors.red : Colors.blue, ); }, ), Center( child: Container( width: 400, height: 400, decoration: BoxDecoration( border: Border.all(color: Colors.black), ), child: Image.network('https://picsum.photos/400'), ), ), ListView.separated( separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) => BlurEffect( backdropKey: listKey, child: SizedBox( height: 50, child: Center( child: Text(index.toString(), style: const TextStyle(color: Colors.white)), ), ), ), itemCount: 200, ), Positioned.fill( bottom: null, child: BlurEffect( backdropKey: overlayKey, child: Padding( padding: EdgeInsets.only( top: MediaQuery.of(context).viewPadding.top, ), child: const SizedBox(height: 45), ), ), ), Positioned.fill( top: null, child: BlurEffect( backdropKey: overlayKey, child: Padding( padding: EdgeInsets.only( top: MediaQuery.of(context).viewPadding.bottom, ), child: const SizedBox(height: 50), ), ), ), ], ), ), ); } } class BlurEffect extends StatelessWidget { final Widget child; const BlurEffect({ required this.child, required this.backdropKey, super.key, }); final BackdropKey backdropKey; @OverRide Widget build(BuildContext context) { return ClipRect( child: BackdropFilter( backdropKey: backdropKey, filter: ImageFilter.blur( sigmaX: 40, sigmaY: 40, // tileMode: TileMode.mirror, ), child: DecoratedBox( decoration: BoxDecoration(color: Colors.black.withOpacity(.65)), child: child, ), ), ); } } ``` ### Skia <img src="https://github.com/user-attachments/assets/4c08e92d-f0ba-42b2-a4c4-fc44efbcfae0" width="200"/> ### Impeller <img src="https://github.com/user-attachments/assets/21e95efd-5e0c-4f41-8f84-af3f0e47d1aa" width="200"/>
1 parent 2327428 commit c4c5452

File tree

4 files changed

+261
-3
lines changed

4 files changed

+261
-3
lines changed

packages/flutter/lib/src/rendering/layer.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,6 +2193,28 @@ class ShaderMaskLayer extends ContainerLayer {
21932193
}
21942194
}
21952195

2196+
/// A backdrop key uniquely identifies the backdrop that a [BackdropFilterLayer]
2197+
/// samples from.
2198+
///
2199+
/// When multiple backdrop filters share the same key, the Flutter engine can
2200+
/// more efficiently perform the backdrop operations.
2201+
///
2202+
/// Instead of using a backdrop key directly, consider using a [BackdropGroup]
2203+
/// and the [BackdropFilter.grouped] constructor. The framework will
2204+
/// automatically group child backdrop filters that use the `.grouped`
2205+
/// constructor when they are placed as children of a [BackdropGroup].
2206+
///
2207+
/// For more information, see [BackdropFilter].
2208+
@immutable
2209+
final class BackdropKey {
2210+
/// Create a new [BackdropKey].
2211+
BackdropKey() : _key = _nextKey++;
2212+
2213+
static int _nextKey = 0;
2214+
2215+
final int _key;
2216+
}
2217+
21962218
/// A composited layer that applies a filter to the existing contents of the scene.
21972219
class BackdropFilterLayer extends ContainerLayer {
21982220
/// Creates a backdrop filter layer.
@@ -2237,13 +2259,30 @@ class BackdropFilterLayer extends ContainerLayer {
22372259
}
22382260
}
22392261

2262+
/// The backdrop key that identifies the [BackdropGroup] this filter will apply to.
2263+
///
2264+
/// The default value for the backdrop key is `null`, meaning that it's not
2265+
/// part of a [BackdropGroup].
2266+
///
2267+
/// The scene must be explicitly recomposited after this property is changed
2268+
/// (as described at [Layer]).
2269+
BackdropKey? get backdropKey => _backdropKey;
2270+
BackdropKey? _backdropKey;
2271+
set backdropKey(BackdropKey? value) {
2272+
if (value != _backdropKey) {
2273+
_backdropKey = value;
2274+
markNeedsAddToScene();
2275+
}
2276+
}
2277+
22402278
@override
22412279
void addToScene(ui.SceneBuilder builder) {
22422280
assert(filter != null);
22432281
engineLayer = builder.pushBackdropFilter(
22442282
filter!,
22452283
blendMode: blendMode,
22462284
oldLayer: _engineLayer as ui.BackdropFilterEngineLayer?,
2285+
backdropId: _backdropKey?._key
22472286
);
22482287
addChildrenToScene(builder);
22492288
builder.pop();
@@ -2254,6 +2293,7 @@ class BackdropFilterLayer extends ContainerLayer {
22542293
super.debugFillProperties(properties);
22552294
properties.add(DiagnosticsProperty<ui.ImageFilter>('filter', filter));
22562295
properties.add(EnumProperty<BlendMode>('blendMode', blendMode));
2296+
properties.add(IntProperty('backdropKey', _backdropKey?._key));
22572297
}
22582298
}
22592299

packages/flutter/lib/src/rendering/proxy_box.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,10 +1213,12 @@ class RenderBackdropFilter extends RenderProxyBox {
12131213
required ui.ImageFilter filter,
12141214
BlendMode blendMode = BlendMode.srcOver,
12151215
bool enabled = true,
1216+
BackdropKey? backdropKey,
12161217
})
12171218
: _filter = filter,
12181219
_enabled = enabled,
12191220
_blendMode = blendMode,
1221+
_backdropKey = backdropKey,
12201222
super(child);
12211223

12221224
@override
@@ -1261,6 +1263,19 @@ class RenderBackdropFilter extends RenderProxyBox {
12611263
_blendMode = value;
12621264
markNeedsPaint();
12631265
}
1266+
/// The backdrop key that identifies the [BackdropGroup] this filter will
1267+
/// read from.
1268+
///
1269+
/// The default value for the backdrop key is `null`.
1270+
BackdropKey? get backdropKey => _backdropKey;
1271+
BackdropKey? _backdropKey;
1272+
set backdropKey(BackdropKey? value) {
1273+
if (value == _backdropKey) {
1274+
return;
1275+
}
1276+
_backdropKey = value;
1277+
markNeedsPaint();
1278+
}
12641279

12651280
@override
12661281
bool get alwaysNeedsCompositing => child != null;
@@ -1277,6 +1292,7 @@ class RenderBackdropFilter extends RenderProxyBox {
12771292
layer ??= BackdropFilterLayer();
12781293
layer!.filter = _filter;
12791294
layer!.blendMode = _blendMode;
1295+
layer!.backdropKey = _backdropKey;
12801296
context.pushLayer(layer!, super.paint, offset);
12811297
assert(() {
12821298
layer!.debugCreator = debugCreator;

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

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export 'package:flutter/rendering.dart' show
3636
AlignmentGeometryTween,
3737
AlignmentTween,
3838
Axis,
39+
BackdropKey,
3940
BoxConstraints,
4041
BoxConstraintsTransform,
4142
CrossAxisAlignment,
@@ -466,6 +467,34 @@ class ShaderMask extends SingleChildRenderObjectWidget {
466467
}
467468
}
468469

470+
/// A widget that establishes a shared backdrop layer for all child [BackdropFilter]
471+
/// widgets that opt into using it.
472+
///
473+
/// Sharing a backdrop filter layer will improve the performance of multiple
474+
/// backdrop filters. To opt into using a shared [BackdropGroup], the special
475+
/// [BackdropFilter.grouped] constructor must be used.
476+
class BackdropGroup extends InheritedWidget {
477+
/// Create a new [BackdropGroup] widget.
478+
BackdropGroup({
479+
super.key,
480+
required super.child,
481+
BackdropKey? backdropKey,
482+
}) : backdropKey = backdropKey ?? BackdropKey();
483+
484+
/// The backdrop key this backdrop group will use with shared child layers.
485+
final BackdropKey backdropKey;
486+
487+
@override
488+
bool updateShouldNotify(covariant BackdropGroup oldWidget) {
489+
return oldWidget.backdropKey != backdropKey;
490+
}
491+
492+
/// Look up the nearest [BackdropGroup], or `null` if there is not one.
493+
static BackdropGroup? of(BuildContext context) {
494+
return context.dependOnInheritedWidgetOfExactType<BackdropGroup>();
495+
}
496+
}
497+
469498
/// A widget that applies a filter to the existing painted content and then
470499
/// paints [child].
471500
///
@@ -485,6 +514,48 @@ class ShaderMask extends SingleChildRenderObjectWidget {
485514
/// html renderer for web applications.
486515
/// {@endtemplate}
487516
///
517+
/// Multiple backdrop filters can be combined into a single rendering operation
518+
/// by the Flutter engine if these backdrop filters widgets all share a common
519+
/// [BackdropKey]. The backdrop key uniquely identifies the input for a backdrop
520+
/// filter, and when shared, indicates the filtering can be performed once. This
521+
/// can significantly reduce the overhead of using multiple backdrop filters in
522+
/// a scene. The key can either be provided manually via the `backdropKey`
523+
/// constructor parameter or looked up from a [BackdropGroup] inherited widget
524+
/// via the `.grouped` constructor.
525+
///
526+
/// Backdrop filters that overlap with each other should not use the same
527+
/// backdrop key, otherwise the results may look as if only one filter is
528+
/// applied in the overlapping regions.
529+
///
530+
/// The following snippet demonstrates how to use the backdrop key to allow each
531+
/// list item to have an efficient blur. The engine will perform only one
532+
/// backdrop blur but the results will be visually identical to multiple blurs.
533+
///
534+
/// ```dart
535+
/// Widget build(BuildContext context) {
536+
/// return BackdropGroup(
537+
/// child: ListView.builder(
538+
/// itemCount: 60,
539+
/// itemBuilder: (BuildContext context, int index) {
540+
/// return ClipRect(
541+
/// child: BackdropFilter.grouped(
542+
/// filter: ui.ImageFilter.blur(
543+
/// sigmaX: 40,
544+
/// sigmaY: 40,
545+
/// ),
546+
/// child: Container(
547+
/// color: Colors.black.withOpacity(0.2),
548+
/// height: 200,
549+
/// child: const Text('Blur item'),
550+
/// ),
551+
/// ),
552+
/// );
553+
/// }
554+
/// ),
555+
/// );
556+
/// }
557+
/// ```
558+
///
488559
/// {@youtube 560 315 https://www.youtube.com/watch?v=dYRs7Q1vfYI}
489560
///
490561
/// {@tool snippet}
@@ -582,7 +653,26 @@ class BackdropFilter extends SingleChildRenderObjectWidget {
582653
super.child,
583654
this.blendMode = BlendMode.srcOver,
584655
this.enabled = true,
585-
});
656+
this.backdropGroupKey,
657+
}) : _useSharedKey = false;
658+
659+
/// Creates a backdrop filter that groups itself with the nearest parent
660+
/// [BackdropGroup].
661+
///
662+
/// The [blendMode] argument will default to [BlendMode.srcOver] and must not be
663+
/// null if provided.
664+
///
665+
/// This constructor will automatically look up the nearest [BackdropGroup]
666+
/// and will share the backdrop input with sibling and child [BackdropFilter]
667+
/// widgets.
668+
const BackdropFilter.grouped({
669+
super.key,
670+
required this.filter,
671+
super.child,
672+
this.blendMode = BlendMode.srcOver,
673+
this.enabled = true,
674+
}) : backdropGroupKey = null,
675+
_useSharedKey = true;
586676

587677
/// The image filter to apply to the existing painted content before painting the child.
588678
///
@@ -603,17 +693,33 @@ class BackdropFilter extends SingleChildRenderObjectWidget {
603693
/// type for performance reasons.
604694
final bool enabled;
605695

696+
/// The [BackdropKey] that identifies the backdrop this filter will apply to.
697+
///
698+
/// The default value for the backdrop key is `null`.
699+
final BackdropKey? backdropGroupKey;
700+
701+
// Whether to look up the [backdropKey] from a parent [BackdropGroup].
702+
final bool _useSharedKey;
703+
704+
BackdropKey? _getBackdropGroupKey(BuildContext context) {
705+
if (_useSharedKey) {
706+
return BackdropGroup.of(context)?.backdropKey;
707+
}
708+
return backdropGroupKey;
709+
}
710+
606711
@override
607712
RenderBackdropFilter createRenderObject(BuildContext context) {
608-
return RenderBackdropFilter(filter: filter, blendMode: blendMode, enabled: enabled);
713+
return RenderBackdropFilter(filter: filter, blendMode: blendMode, enabled: enabled, backdropKey: _getBackdropGroupKey(context));
609714
}
610715

611716
@override
612717
void updateRenderObject(BuildContext context, RenderBackdropFilter renderObject) {
613718
renderObject
614719
..filter = filter
615720
..enabled = enabled
616-
..blendMode = blendMode;
721+
..blendMode = blendMode
722+
..backdropKey = _getBackdropGroupKey(context);
617723
}
618724
}
619725

packages/flutter/test/widgets/backdrop_filter_test.dart

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,105 @@ library;
1010
import 'dart:ui';
1111

1212
import 'package:flutter/material.dart';
13+
import 'package:flutter/rendering.dart';
1314
import 'package:flutter_test/flutter_test.dart';
1415

1516
void main() {
17+
testWidgets('Backdrop key is passed to backdrop Layer', (WidgetTester tester) async {
18+
final BackdropKey backdropKey = BackdropKey();
19+
20+
Widget build({required bool enableKeys}) {
21+
return MaterialApp(
22+
home: Scaffold(
23+
body: ListView(
24+
children: <Widget>[
25+
ClipRect(
26+
child: BackdropFilter(
27+
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
28+
backdropGroupKey: enableKeys ? backdropKey : null,
29+
child: Container(
30+
color: Colors.black.withAlpha(40),
31+
height: 200,
32+
child: const Text('Item 1'),
33+
),
34+
),
35+
),
36+
ClipRect(
37+
child: BackdropFilter(
38+
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
39+
backdropGroupKey: enableKeys ? backdropKey : null,
40+
child: Container(
41+
color: Colors.black.withAlpha(40),
42+
height: 200,
43+
child: const Text('Item 1'),
44+
),
45+
),
46+
),
47+
],
48+
)
49+
),
50+
);
51+
}
52+
53+
await tester.pumpWidget(build(enableKeys: true));
54+
55+
List<BackdropFilterLayer> layers = tester.layers.whereType<BackdropFilterLayer>().toList();
56+
57+
expect(layers.length, 2);
58+
expect(layers[0].backdropKey, backdropKey);
59+
expect(layers[1].backdropKey, backdropKey);
60+
61+
await tester.pumpWidget(build(enableKeys: false));
62+
63+
layers = tester.layers.whereType<BackdropFilterLayer>().toList();
64+
65+
expect(layers.length, 2);
66+
expect(layers[0].backdropKey, null);
67+
expect(layers[1].backdropKey, null);
68+
});
69+
70+
testWidgets('Backdrop key is passed to backdrop Layer via backdrop group', (WidgetTester tester) async {
71+
Widget build() {
72+
return MaterialApp(
73+
home: Scaffold(
74+
body: BackdropGroup(
75+
child: ListView(
76+
children: <Widget>[
77+
ClipRect(
78+
child: BackdropFilter.grouped(
79+
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
80+
child: Container(
81+
color: Colors.black.withAlpha(40),
82+
height: 200,
83+
child: const Text('Item 1'),
84+
),
85+
),
86+
),
87+
ClipRect(
88+
child: BackdropFilter.grouped(
89+
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
90+
child: Container(
91+
color: Colors.black.withAlpha(40),
92+
height: 200,
93+
child: const Text('Item 1'),
94+
),
95+
),
96+
),
97+
],
98+
)
99+
),
100+
),
101+
);
102+
}
103+
104+
await tester.pumpWidget(build());
105+
106+
final List<BackdropFilterLayer> layers = tester.layers.whereType<BackdropFilterLayer>().toList();
107+
108+
expect(layers.length, 2);
109+
expect(layers[0].backdropKey, layers[1].backdropKey);
110+
});
111+
16112
testWidgets("Material2 - BackdropFilter's cull rect does not shrink", (WidgetTester tester) async {
17113
await tester.pumpWidget(
18114
MaterialApp(

0 commit comments

Comments
 (0)