废话不多说,先上本次要实现的效果图。
Gif格式是渣像素,实际效果要自然的多。这个项目其实是看到小池记账小程序后实现的一个类似效果,小池比较闪光的一点就是这个主界面的动态高斯模糊效果,不过小池的动态模糊效果就不如Flutter可以做的这么自然流畅了,模糊时还是有肉眼可见的卡顿的。大家可以搜索小池记账对比一下,下面进入正文。
知识点
- Flutter中如何截取当前屏幕的Widget图片。
- Flutter如何对一张图片进行高斯模糊。
- 如何淡入淡出切换两个Widget。
Flutter截取当前屏幕的Widget图片
- 目前官网文档还没有相关的例子,正式发布的Beta3版本也没有公开的方法,但事实上,在未公开发布的Flutter v0.4.4中,已经有截取当前屏幕Widget图片的文档了,地址在这里。我们需要切换到当前的开发分支才能看到这个新的方法
toImage()
。你可以直接在Flutter的本地git仓库checkout到master
分支,当然,也可以用下面更简单的方法,运行这两个命令,Flutter会自动切换到最新分支并下载依赖,到这里,准备工作就算完成了。
flutter channel master
flutter doctor -v
复制代码
- 给需要截图的Widget包裹一个
RepaintBoundary
,如下示例代码:
class _PngHomeState extends State<PngHome> {
GlobalKey globalKey = new GlobalKey();
// 截图boundary,并且返回图片的二进制数据。
Future _capturePng() async {
RenderRepaintBoundary boundary = globalKey.currentContext.findRenderObject();
ui.Image image = await boundary.toImage();
// 注意:png是压缩后格式,如果需要图片的原始像素数据,请使用rawRgba
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
return pngBytes;
}
@override
Widget build(BuildContext context) {
return RepaintBoundary(
//globalKey用于识别
key: globalKey,
child: Center(
child: FlatButton(
child: Text('Hello World', textDirection: TextDirection.ltr),
onPressed: _capturePng,
),
),
);
}
复制代码
目前这个方法还是挺费时间的,大概在100ms左右才能得到截图,更别说我们还要对图片做处理,所以,尽管我对生成的图片做了缓存,但是第一次得到图片的时候,还是会有一小会的停顿(200-300ms)
对得到的图片进行高斯模糊
拿到了图片的二进制数据,怎么对其进行高斯模糊?搜索官网发现了一个很好的库:image
,把它添加到项目中来,在pubspec.yaml
中添加如下代码:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.0
image: "^1.1.32"
复制代码
然后在项目中import,注意取一个别名,不要跟Flutter已有的Image
库冲突:
import 'package:image/image.dart' as gimage;
复制代码
接下来,我们就可以直接调用库中的gaussianBlur()
方法了:
//接着上面的示例代码
RenderRepaintBoundary boundary =
globalKey.currentContext.findRenderObject();
ui.Image image = await boundary.toImage();
//必须转成rawRgba,不然转码很浪费时间
var pixelsData = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
var pixels = pixelsData.buffer.asUint8List();
var width = image.width;
var height = image.height;
//得到im对象
gimage.Image im = gimage.Image.fromBytes(width, height, pixels);
//高斯模糊一下(很慢)
var gsImage = gimage.gaussianBlur(im, 10);
// 这里有个问题,你要把gsImage显示在屏幕上,必须给它转码,Image小部件是不支持rgba格式的。
// 转JPG格式
var jpgImage = gimage.encodeJpg(gsImage, quality: 80);
复制代码
这么一通操作下来,诺大一个界面截图,转来转去,还高斯模糊一下,你可想而知效率有多慢,基本上等个两三秒是正常的事,如果你对时间不敏感,这倒不是个大问题,不过这还远远不能达到我要的动态模糊当前界面的效果。 于是乎找遍Github
,然而并没有发现一个快速模糊算法是Dart
语言实现的,这倒也暴露出Flutter
的一个很大的问题,就是生态基本还没建立起来。实际上,Dart
在Github上的开源项目总共也没几个,好在这个语言对标的是JS,语法上两者相差不大,两者可以相互转换。我在Google上找到这个JS项目StackBlur,直接把它转成Dart
语言格式的,并没有碰到什么特别大的困难。稍后我会附上本项目的Github
仓库地址,需要这个算法的同学可以到lib/widget/blur.dart
下自取。
使用新的算法还不够,因为模糊所需时间和图片大小成正比,所以我们最好还要对图片进行缩放,然后再模糊化,代码如下:
//接着上面的示例代码
RenderRepaintBoundary boundary =
globalKey.currentContext.findRenderObject();
// 先缩放,取原图的0.5倍大小图片,减少模糊时间 (60ms左右)
ui.Image image = await boundary.toImage(pixelRatio: 0.5);
//转成rawRgba格式
var pixelsData = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
var pixels = pixelsData.buffer.asUint8List();
//使用新算法进行模糊 (50-60ms)
processImageDataRGBA(pixels, 0, 0, image.width, image.height, 4);
//把pixels转回Image并且编码成JPG (200ms左右)
gimage.Image newImage =
new gimage.Image.fromBytes(image.width, image.height, pixels);
List<int> jpgList = gimage.encodeJpg(newImage);
return jpgList;
复制代码
经过这几步操作,延迟时间能控制在400ms内,虽然没办法做到完全不卡顿,至少等待时间勉强可以接受。有的同学可能发现了编码成JPG格式花费了大量的时间,这我暂时找不到什么好办法(还在研究),因为Flutter的Image 小部件只支持显示已编码的图片,比如:JPG、PNG、WebP、Gif等等。但是不能显示原始像素格式图片,所以必须把处理过的图片编码成JPG格式。(为什么只有toByteData()
没有fromByteData()
?鬼知道Flutter开发者是怎么想的,可能以后会有相关的API放出。)
如何淡入淡出切换两个Widget。
目前我们是有两个Widget
,一个是原本的界面,另一个是含有原本界面的截图+模糊处理的图片的ImageWidget,我们要实现开头Gif图那种渐变切换的效果,就需要用到一个新的控件:AnimatedCrossFade
,它的效果很简单:就是包含两个Widget
,切换时,淡出第一个Widget,淡入第二个Widget,达到一种平滑的效果,其效果也是由两个FadeTransition
Widget实现的。使用方法很简单:
new AnimatedCrossFade(
duration: const Duration(seconds: 3),
firstChild: const FlutterLogo(style: FlutterLogoStyle.horizontal, size: 100.0),
secondChild: const FlutterLogo(style: FlutterLogoStyle.stacked, size: 100.0),
crossFadeState: _first ? CrossFadeState.showFirst : CrossFadeState.showSecond,
)
复制代码
以上代码即是一个变化FlutterLogo的例子,只需要在setState()
中改变_first
的值,即可选择显示FirstChild还是SecondChild。完整界面代码可以参见源码,这里就不再放出了。
附:
Chic_Github
感受
Flutter作为Google最近力推的一个技术,致力于改变当前移动APP两套班子、两次实现的问题,得到了很多人的关注。在此我不敢妄论这项技术的未来前景,不过一段时间的使用下来后,有了一点小体会,记录下来供读者参考:
- 轻
没有千奇百怪的Gradle版本,没有各式各样的AndroidStudio,甚至连Android的版本你都不需要刻意管理。选一个你喜欢的编辑器(VSCode、AS、IDEA),建好Flutter项目,就可以直接在main.dart
里面开干。开发过Android的人都知道有时候这些七七八八的版本问题是有多头疼,而且是你拿到一个项目跑不起来的时候首先要了解的事儿。从这点来看,Flutter在开发体验上就在我心里大大的加分。更别说大大缩短等待时间的hot reload
这样的特色功能。总之,轻松加愉快是我学习Flutter的第一感受。 - 全
如果说让开发者尽快专注于业务逻辑开发是一个框架的应尽义务的话,那健全的文档和注释就是Flutter带来的意外惊喜了。不说官网上那些详细的文档,还有视频教程,单说代码里的注释,就足够详细。举个小例子:你如果想自定义一个ViewGroup
,在Android中,你可以继承ViewGroup
然后进行measure
,layout
,draw
三大步,那在Flutter中,其实文档里没有具体介绍怎么定制一个你想要的ViewGroup
的。于是你想啃源码看看官方怎么实现的,就拿最简单的Stack
这个Widget开刀,没想到它的注释里说了这么一句:
/// If you want to lay a number of children out in a particular pattern, or if
/// you want to make a custom layout manager, you probably want to use
/// [CustomMultiChildLayout] instead.
复制代码
- 于是你赶紧点进
CustomMultiChildLayout
瞅瞅,结果发现这这个CustomMultiChildLayout
类的注释哪里是代码注释,根本就是隐藏在注释里的教程啊。
/// ## Sample code
...
/// // Define your own slot numbers, depending upon the id assigned by LayoutId.
/// // Typical usage is to define an enum like the one below, and use those
/// // values as the ids.
/// enum _Slot {
/// leader,
/// follower,
/// }
///
/// class FollowTheLeader extends MultiChildLayoutDelegate {
/// @override
/// void performLayout(Size size) {
/// Size leaderSize = Size.zero;
///
/// if (hasChild(_Slot.leader)) {
/// leaderSize = layoutChild(_Slot.leader, new BoxConstraints.loose(size));
/// positionChild(_Slot.leader, Offset.zero);
/// }
///
...
复制代码
- 直接给你甩出一串样例代码,还有一大坨详细说明我就不截取过来了。我想说的是,这样的例子在Flutter中并不特殊,大量的
Widget
的注释中都会带有示例代码,无须切换到文档,也无须费劲搜索答案,代码自带的注释就能解决很大一部分问题。从这些详细的注释里我们也可以看出Google对Flutter的重视和用心程度。
- 稳
说实话从安装flutter
到跑起来一个HelloWorld
并没有花我太多时间。我还以为Flutter又是Dart语言,又是Android、iOS支持,又是AndroidStudio插件支持,这环境配置该有多繁琐!在Flutter下载的时候,我承认我花了十多秒思考我电脑的AndroidStudio3
到底安装在哪个路径,愣是没想起来。没想到flutter doctor -v
一运行,出来检测报告:Flutter安装OK;Dart安装OK;你电脑安装了AndroidSDK,版本27.0.3;VSCode位置在那儿,没安装Flutter插件;AndroidStudio安装在那儿,装好了Flutter插件... 顿时心中有一种解放生产力的感动,终于不用配置这个Path那个Path了(说的就是你JAVA),终于不用把一串路径名复制来复制去了。这才是来自未来的编程工具,把一切搞定,然后汇报,而不是对你的记忆力进行各种拷问。在这一点上,Flutter当得起这个稳字。
当然,目前Flutter开发还存在很多问题,最大问题是围绕Flutter的生态圈还没有起来,Flutter连一个正经的ORM框架都没有,虽然有官方推荐的sqflite
,不过就这几天的使用来看,这个库还是对数据库操作进行一些很简单的封装,很多操作都必须自己写SQL语句完成。等过段时间我再分享一篇有关sqflite
的相关操作的博客。直到利用业余时间完成仿小池记账APP的全部功能,应该还有很长的路要走,同样,本文的主人公----Flutter也还有很长的路要走。
5.31更新
以上方法现只适用于对一张图片进行高斯模糊,如果是对界面的Widget进行高斯模糊,应该使用系统提供的BackdropFilter
。其利用Skia
提供的模糊算法,会带来更快更节省资源的模糊效果。
BackdropFilter
使用方法
new BackdropFilter(
filter: new ui.ImageFilter.blur(sigmaX: _sigma, sigmaY: _sigma),
child: new Container(
color: Colors.blue.withOpacity(_opacity),
padding: const EdgeInsets.all(30.0),
width: 90.0,
height: 90.0,
child: new Center(
child: _showText ? new Text('Test') : null,
),
),
);
复制代码
需要注意的是BackdropFilter
是怎么绘制filter区域的,首先它根据内部child
的大小确定需要进行filter的区域,然后把filter
提供的效果(比如模糊)绘制到背景上,接着再绘制child
。直接看上面代码的运行效果就一目了然了(蓝色区域即为模糊区域):
可以看到Test没有被模糊,但是蓝色区域下面的背景已经被模糊化了,可以通过Sigma调整模糊的水平。
但是BackdropFilter
使用时有个坑,就是它只能处理矩形区域,当你把蓝色区域变成圆形(利用ClipOval),模糊效果就失效了,这是一个Skia
引擎的BUG,目前应该还没有被解决,关于此Bug的详细讨论可见:Flutter BackdropFilter can't handle non-rectangular clipping and text causes blur to bleed outside bounds
如果你想运行上图的模糊例子,可以访问()原版,或者在下面连接下载它的复制版。
BackdropFilter
模糊样例