本文涉及的知識點:截圖、圖片保存、根據用戶手勢實時繪製canvas。git
GitHub地址:github.com/yumi0629/Fl…github
先上效果圖:canvas
此次的想法是移植自項目中的一個小功能:截屏當前頁面,添加塗鴉功能後,分享給第三方APP。分享功能咱們暫不討論,使用插件能夠輕鬆完成,重點是截屏+塗鴉+圖片保存。
具體實現思路是:截取當前屏幕內容,保存至APP緩存目錄,塗鴉頁面再去讀取文件,依然是使用CustomerPaint實現根據用戶手勢實時繪製,最後將用戶塗鴉部分與原圖片組合保存至本地。給圖片加水印的實現其實就是截屏,由於截取當前屏幕內容實際上也是將Widget轉化爲byteData再轉化爲File的過程。緩存
Flutter提供了一個RepaintBoundary
Widget來實現截圖的功能,用RepaintBoundary
包裹須要截取的部分,RenderRepaintBoundary
能夠將RepaintBoundary
包裹的部分截取出來;而後經過boundary.toImage()
方法轉化爲ui.Image
對象,再使用image.toByteData()
將image轉化爲byteData
;最後經過File().writeAsBytes()
存儲爲文件對象:bash
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);
複製代碼
須要注意的地方:app
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
。 咱們能夠經過官方插件path_provider
來獲取APP的內部和外部存儲路徑:async
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
插件。 還有就是存儲文件的時候要養成一個好習慣,先判斷下父目錄是否存在:ide
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()
方法來繪製路徑:ui
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
,你會發現,繪製出來的點集合並非無縫鏈接在一塊兒的,看起來就像是虛線同樣:spa
PointMode.polygon
,polygon確實能夠繪製連續的曲線,可是有兩個缺點:一是繪製不如drawLine流暢,延遲會大一點,由於暫時的實現方式是用戶手指每動一次都會重繪一次,消耗比較大;二是繪製出來的線條不如直接繪製line來得圓潤。我以前沒有深度考慮drawPoints的一個緣由就是延遲太大。之後若是能找到更好的刷新機制,我會嘗試使用drawPoints來繪製的。
drawLine()
強行將全部點鏈接起來。
RepaintBoundary
來保存圖片就能夠啦~~~