Created
January 4, 2020 04:23
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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