本文涉及的知識點:截圖、圖片儲存、根據使用者手勢實時繪製canvas。
GitHub地址:github.com/yumi0629/Fl…
先上效果圖:
需求分析
這次的想法是移植自專案中的一個小功能:截圖當前頁面,新增塗鴉功能後,分享給第三方APP。分享功能我們暫不討論,使用外掛可以輕鬆完成,重點是截圖+塗鴉+圖片儲存。
具體實現思路是:擷取當前螢幕內容,儲存至APP快取目錄,塗鴉頁面再去讀取檔案,依然是使用CustomerPaint實現根據使用者手勢實時繪製,最後將使用者塗鴉部分與原圖片組合儲存至本地。給圖片加水印的實現其實就是截圖,因為擷取當前螢幕內容實際上也是將Widget轉化為byteData再轉化為File的過程。
截圖並儲存
Flutter提供了一個RepaintBoundary
Widget來實現截圖的功能,用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
的時候會尤其明顯。toImage({double pixelRatio = 1.0
中的
})pixelRatio
引數是可以表面上提高圖片清晰度的。為什麼說是表面上呢?因為這個引數是純px為單位的,和你的螢幕密度並沒有關係。以安卓為例,螢幕基礎寬高為360px*480px, 預設輸出也是360px*480px的圖片檔案;如果你將pixelRatio
設定為10,那麼你輸出的圖片檔案大小將是3600px*4800px,但是展示這張圖的手機螢幕大小並不會改變,圖變大了,自然看起來“清晰”了。因此,這個值我建議取window.devicePixelRatio
。
Flutter中獲取儲存路徑
我們可以通過官方外掛path_provider
來獲取APP的內部和外部儲存路徑:
Directory tempDir = await getTemporaryDirectory()
,等同於iOS中的NSCachesDirectory
API和Android中的getCacheDir
API;Directory externalDir = await getExternalStorageDirectory()
,不支援iOS(會丟擲UnsupportedError),等同於Android中的getExternalStorageDirectory
API;Directory applicationDir = await getApplicationDocumentsDirectory()
,等同於iOS中的NSDocumentsDirectory
API和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
,你會發現,繪製出來的點集合並不是無縫連線在一起的,看起來就像是虛線一樣:
更新: 評論有人說可以使用PointMode.polygon
,polygon確實可以繪製連續的曲線,但是有兩個缺點:一是繪製不如drawLine流暢,延遲會大一點,因為暫時的實現方式是使用者手指每動一次都會重繪一次,消耗比較大;二是繪製出來的線條不如直接繪製line來得圓潤。我之前沒有深度考慮drawPoints的一個原因就是延遲太大。以後如果能找到更好的重新整理機制,我會嘗試使用drawPoints來繪製的。
因此我們還是使用drawLine()
強行將所有點連線起來。
使用者繪製完成後,依然是使用RepaintBoundary
來儲存圖片就可以啦~~~