1. 程式人生 > >用Flutter實現一個塗鴉和加水印功能

用Flutter實現一個塗鴉和加水印功能

本文涉及的知識點:截圖、圖片儲存、根據使用者手勢實時繪製canvas。

GitHub地址:github.com/yumi0629/Fl…

先上效果圖:

需求分析

  這次的想法是移植自專案中的一個小功能:截屏當前頁面,添加塗鴉功能後,分享給第三方APP。分享功能我們暫不討論,使用外掛可以輕鬆完成,重點是截圖+塗鴉+圖片儲存。
  具體實現思路是:擷取當前螢幕內容,儲存至APP快取目錄,塗鴉頁面再去讀取檔案,依然是使用CustomerPaint實現根據使用者手勢實時繪製,最後將使用者塗鴉部分與原圖片組合儲存至本地。給圖片加水印的實現其實就是截圖,因為擷取當前螢幕內容實際上也是將Widget轉化為byteData再轉化為File的過程。

截圖並儲存

  Flutter提供了一個RepaintBoundaryWidget來實現截圖的功能,用RepaintBoundary包裹需要擷取的部分,RenderRepaintBoundary可以將RepaintBoundary包裹的部分截取出來;然後通過boundary.toImage()方法轉化為ui.Image物件,再使用image.toByteData()將image轉化為byteData;最後通過File().writeAsBytes()儲存為檔案物件:

RepaintBoundary(
                  key: _repaintKey,
                  child: Stack(
                    alignment: Alignment.bottomRight,
                    children: <Widget>[
                      Image.asset(
                        'images/food01.jpeg'
, fit: BoxFit.cover, ), Icon(Icons.translate,), ], ), ) 複製程式碼
RenderRepaintBoundary boundary =
                    _repaintKey.currentContext.findRenderObject();
ui.Image image = await boundary.toImage();
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
File(tempPath).writeAsBytes(pngBytes);
複製程式碼

  需要注意的地方:

  • 記住給RepaintBoundary新增一個key,因為我們需要通過這個key來找到當前的RenderObject,生成一個RenderRepaintBoundary物件;
  • image.toByteData(format: format)可以自定義儲存格式,一般圖片為png格式,但是要注意,如果你的RepaintBoundary包裹的部分沒有設定背景色,那麼儲存出來的圖片可能會有背景色缺失的問題,boundary.toImage()並不會自動新增一個你想要的白色背景,這種情況在擷取Text的時候會尤其明顯。

Flutter中獲取儲存路徑

  我們可以通過官方外掛path_provider來獲取APP的內部和外部儲存路徑:

  • Directory tempDir = await getTemporaryDirectory(),等同於iOS中的NSCachesDirectoryAPI和Android中的getCacheDirAPI;
  • Directory externalDir = await getExternalStorageDirectory(),不支援iOS(會丟擲UnsupportedError),等同於Android中的getExternalStorageDirectoryAPI;
  • Directory applicationDir = await getApplicationDocumentsDirectory(),等同於iOS中的NSDocumentsDirectoryAPI和Android中的AppData目錄;

  要注意的是getExternalStorageDirectory()方法,大多數情況下是訪問SDCard路徑,因此即使在Android中呼叫,也要注意許可權問題,推薦使用permission_handler外掛。   還有就是儲存檔案的時候要養成一個好習慣,先判斷下父目錄是否存在:

void _saveImage(Uint8List uint8List, Directory dir, String fileName) async {
  bool isDirExist = await Directory(dir.path).exists();
  if (!isDirExist) Directory(dir.path).create();
  ······
  File(tempPath).writeAsBytes(uint8List);
}
複製程式碼

塗鴉

  通過GestureDetector包裹需要繪製的區域,收集使用者的手勢路徑資訊,通過canvas.drawLine()方法來繪製路徑:

List<Offset> points = [];

GestureDetector(
          onPanStart: (details) {
          },
          onPanUpdate: (details) {
            RenderBox referenceBox = context.findRenderObject();
            Offset localPosition =
                referenceBox.globalToLocal(details.globalPosition);
            state(() {
              points.add(localPosition);
            });
          },
          onPanEnd: (details) {
          },
        )
複製程式碼
for (int i = 0; i < points.length - 1; i++) {
        if (points[i] != null && points[i + 1] != null)
          canvas.drawLine(points[i], points[i + 1], _linePaint);
      }
複製程式碼

  為什麼不直接用canvas.drawPoints()方法呢?
  即使將PointMode設定為PointMode.lines,你會發現,繪製出來的點集合並不是無縫連線在一起的,看起來就像是虛線一樣:

  因此我們還是使用 drawLine()強行將所有點連線起來。
  使用者繪製完成後,依然是使用 RepaintBoundary來儲存圖片就可以啦~~~