Flutter學習 —- 螢幕截圖和高斯模糊

Fengy發表於2019-03-04

廢話不多說,先上本次要實現的效果圖。

效果

Gif格式是渣畫素,實際效果要自然的多。這個專案其實是看到小池記賬小程式後實現的一個類似效果,小池比較閃光的一點就是這個主介面的動態高斯模糊效果,不過小池的動態模糊效果就不如Flutter可以做的這麼自然流暢了,模糊時還是有肉眼可見的卡頓的。大家可以搜尋小池記賬對比一下,下面進入正文。

知識點

  1. Flutter中如何擷取當前螢幕的Widget圖片。
  2. Flutter如何對一張圖片進行高斯模糊。
  3. 如何淡入淡出切換兩個Widget。

Flutter擷取當前螢幕的Widget圖片

  1. 目前官網文件還沒有相關的例子,正式釋出的Beta3版本也沒有公開的方法,但事實上,在未公開發布的Flutter v0.4.4中,已經有擷取當前螢幕Widget圖片的文件了,地址在這裡。我們需要切換到當前的開發分支才能看到這個新的方法toImage()。你可以直接在Flutter的本地git倉庫checkout到master分支,當然,也可以用下面更簡單的方法,執行這兩個命令,Flutter會自動切換到最新分支並下載依賴,到這裡,準備工作就算完成了。
flutter channel master
flutter doctor -v
複製程式碼
  1. 給需要截圖的Widget包裹一個RepaintBoundary,如下示例程式碼:
class _PngHomeState extends State<PngHome> {
  GlobalKey globalKey = new GlobalKey();

  // 截圖boundary,並且返回圖片的二進位制資料。
  Future<Uint8List> _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,達到一種平滑的效果,其效果也是由兩個FadeTransitionWidget實現的。使用方法很簡單:

 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兩套班子、兩次實現的問題,得到了很多人的關注。在此我不敢妄論這項技術的未來前景,不過一段時間的使用下來後,有了一點小體會,記錄下來供讀者參考:


  1. 沒有千奇百怪的Gradle版本,沒有各式各樣的AndroidStudio,甚至連Android的版本你都不需要刻意管理。選一個你喜歡的編輯器(VSCode、AS、IDEA),建好Flutter專案,就可以直接在main.dart裡面開幹。開發過Android的人都知道有時候這些七七八八的版本問題是有多頭疼,而且是你拿到一個專案跑不起來的時候首先要了解的事兒。從這點來看,Flutter在開發體驗上就在我心裡大大的加分。更別說大大縮短等待時間的hot reload這樣的特色功能。總之,輕鬆加愉快是我學習Flutter的第一感受。

  2. 如果說讓開發者儘快專注於業務邏輯開發是一個框架的應盡義務的話,那健全的文件和註釋就是Flutter帶來的意外驚喜了。不說官網上那些詳細的文件,還有視訊教程,單說程式碼裡的註釋,就足夠詳細。舉個小例子:你如果想自定義一個ViewGroup,在Android中,你可以繼承ViewGroup然後進行measurelayoutdraw三大步,那在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的重視和用心程度。

  1. 說實話從安裝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。直接看上面程式碼的執行效果就一目瞭然了(藍色區域即為模糊區域):

image

可以看到Test沒有被模糊,但是藍色區域下面的背景已經被模糊化了,可以通過Sigma調整模糊的水平。

但是BackdropFilter使用時有個坑,就是它只能處理矩形區域,當你把藍色區域變成圓形(利用ClipOval),模糊效果就失效了,這是一個Skia引擎的BUG,目前應該還沒有被解決,關於此Bug的詳細討論可見:Flutter BackdropFilter can`t handle non-rectangular clipping and text causes blur to bleed outside bounds

image

如果你想執行上圖的模糊例子,可以訪問(翻牆)原版,或者在下面連線下載它的複製版。

BackdropFilter模糊樣例

相關文章