Skip to content

Instantly share code, notes, and snippets.

@flar
Created January 4, 2020 04:23
Show Gist options
  • Save flar/efe8f4aadeb41f587a8d86a07c483fe4 to your computer and use it in GitHub Desktop.
Save flar/efe8f4aadeb41f587a8d86a07c483fe4 to your computer and use it in GitHub Desktop.
Blurring a background with either a cached BackdropFilter or an ImageFiltered widget for comparison.
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
showPerformanceOverlay: true,
);
}
}
class MyHomePage extends StatefulWidget {
@override
State createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
bool _useSnapshot = true;
Widget frost({Widget opaqueBackground, double sigmaX, double sigmaY, Widget child}) {
if (_useSnapshot) {
print('Using snapshot');
return CachedFrostedBox(
opaqueBackground: opaqueBackground,
sigmaX: sigmaX,
sigmaY: sigmaY,
child: child,
);
} else {
print('Using ImageFiltered widget');
return ImageFilteredFrostedBox(
opaqueBackground: opaqueBackground,
sigmaX: sigmaX,
sigmaY: sigmaY,
child: child,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: <Widget>[
frost(
opaqueBackground: Container(
color: Colors.white,
child: Text('0' * 10000),
),
sigmaX: 2.0,
sigmaY: 2.0,
child: Container(
alignment: Alignment.center,
child: Text('Hello'),
),
),
Center(
child: Padding(
padding: EdgeInsets.fromLTRB(0, 20, 0, 0),
child: LinearProgressIndicator(),
),
),
],
),
bottomNavigationBar: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Use snapshot:'),
Checkbox(value: _useSnapshot, onChanged: (b) => setState(() { _useSnapshot = b; }),),
],
),
);
}
}
class ImageFilteredFrostedBox extends StatelessWidget {
ImageFilteredFrostedBox({@required this.child, this.sigmaX = 8, this.sigmaY = 8, this.opaqueBackground,});
final Widget opaqueBackground;
final double sigmaX;
final double sigmaY;
final Widget child;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
RepaintBoundary(
child: ImageFiltered(
child: opaqueBackground,
imageFilter: ui.ImageFilter.blur(
sigmaX: sigmaX,
sigmaY: sigmaY,
),
),
),
child,
],
);
}
}
class CachedFrostedBox extends StatefulWidget {
CachedFrostedBox({@required this.child, this.sigmaX = 8, this.sigmaY = 8, this.opaqueBackground})
: this.frostBackground = Stack(
children: <Widget>[
opaqueBackground,
ClipRect(
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY),
child: new Container(
decoration: new BoxDecoration(
color: Colors.white.withOpacity(0.1),
)
),
)
),
],
);
final Widget child;
final double sigmaY;
final double sigmaX;
/// This must be opaque so the backdrop filter won't access any colors beneath this background.
final Widget opaqueBackground;
/// Blur applied to the opaqueBackground. See the constructor.
final Widget frostBackground;
@override
State<StatefulWidget> createState() {
return CachedFrostedBoxState();
}
}
class CachedFrostedBoxState extends State<CachedFrostedBox> {
final GlobalKey _snapshotKey = GlobalKey();
Image _backgroundSnapshot;
bool _snapshotLoaded = false;
bool _skipSnapshot = false;
void _snapshot(Duration _) async {
final RenderRepaintBoundary renderBackground = _snapshotKey.currentContext.findRenderObject();
final ui.Image image = await renderBackground.toImage(
pixelRatio: WidgetsBinding.instance.window.devicePixelRatio,
);
// !!! The default encoding rawRgba will throw exceptions. This bug is introducing a lot
// of encoding/decoding work.
final ByteData imageByteData = await image.toByteData(format: ui.ImageByteFormat.png);
setState(() {
_backgroundSnapshot = Image.memory(imageByteData.buffer.asUint8List());
});
}
@override
Widget build(BuildContext context) {
Widget frostedBackground;
if (_backgroundSnapshot == null || _skipSnapshot) {
frostedBackground = RepaintBoundary(
key: _snapshotKey,
child: widget.frostBackground,
);
if (!_skipSnapshot) {
SchedulerBinding.instance.addPostFrameCallback(_snapshot);
}
} else {
// !!! We don't seem to have a way to know when IO thread
// decoded the image.
if (!_snapshotLoaded) {
frostedBackground = widget.frostBackground;
Future.delayed(Duration(seconds: 1), () {
setState(() {
_snapshotLoaded = true;
});
});
} else {
frostedBackground = Offstage();
}
}
return Stack(
children: <Widget>[
frostedBackground,
if (_backgroundSnapshot != null) _backgroundSnapshot,
widget.child,
GestureDetector(
onTap: () {
setState(() { _skipSnapshot = !_skipSnapshot; });
}
),
],
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment