Flutter實(shí)現(xiàn)給圖片添加涂鴉功能
簡(jiǎn)介
先來張圖,看一下最終效果
關(guān)閉和確定
- 關(guān)閉和確定功能對(duì)應(yīng)的是界面左下角叉號(hào),和右下角對(duì)鉤,關(guān)閉按鈕僅做取消當(dāng)次涂鴉,讀者可自行設(shè)置點(diǎn)擊后功能,也可自行更改相應(yīng)UI。選擇功能點(diǎn)擊后會(huì)執(zhí)行一段把當(dāng)前涂鴉后的圖片合成并保存到本地的操作。具體請(qǐng)看示例代碼。
顏色選擇
- 顏色選擇功能可選擇和標(biāo)識(shí)當(dāng)前涂鴉顏色,和指示當(dāng)前涂鴉顏色的選中狀態(tài)(以白色外圈標(biāo)識(shí))。切換顏色后下一次涂鴉即會(huì)使用新的顏色。
撤銷功能
- 撤銷功能可撤銷最近的一次涂鴉。如沒有涂鴉時(shí)顯示置灰的撤銷按鈕。
清除功能
- 清除功能可清除所有涂鴉,如當(dāng)前沒有任何涂鴉時(shí)顯示置灰的清除按鈕。
涂鴉圖片的放大和縮小
- 可雙指滑動(dòng)切換涂鴉放大縮小的效果。
放大縮小后按照新的線條粗細(xì)繼續(xù)涂鴉
- 涂鴉放大或縮小后,涂鴉線條會(huì)隨之放大和縮小,此時(shí)如果繼續(xù)涂鴉,則新涂鴉顯示的粗細(xì)程度與放大或縮小后的線條粗細(xì)程度保持一致。
保存涂鴉圖片到本地。
- flutter涂鴉后的圖片可合成新圖片并保存到本地路徑。
代碼介紹
涂鴉顏色選擇組件。
主要是顯示為可配置的圓點(diǎn)和外圈
circle_ring_widget.dart
import 'package:flutter/material.dart'; class CircleRingWidget extends StatelessWidget { late bool isShowRing; late Color dotColor; CircleRingWidget(this.isShowRing,this.dotColor, {super.key}); @override Widget build(BuildContext context) { return CustomPaint( painter: CircleAndRingPainter(isShowRing,dotColor), size: const Size(56.0, 81.0), // 調(diào)整尺寸大小 ); } } class CircleAndRingPainter extends CustomPainter { late bool isShowRing; late Color dotColor; CircleAndRingPainter(this.isShowRing,this.dotColor); @override void paint(Canvas canvas, Size size) { Paint circlePaint = Paint() ..color = dotColor // 設(shè)置圓點(diǎn)的顏色 ..strokeCap = StrokeCap.round ..strokeWidth = 1.0; Paint ringPaint = Paint() ..color = Colors.white // 設(shè)置圓環(huán)的顏色 ..strokeCap = StrokeCap.round ..strokeWidth = 1.0 ..style = PaintingStyle.stroke; Offset center = size.center(Offset.zero); // 畫一個(gè)半徑為10的圓點(diǎn) canvas.drawCircle(center, 13.0, circlePaint); if(isShowRing){ // 畫一個(gè)半徑為20的圓環(huán) canvas.drawCircle(center, 18.0, ringPaint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } }
存儲(chǔ)顏色和食指劃過點(diǎn)的數(shù)據(jù)類
color_offset.dart
import 'dart:ui'; class ColorOffset{ late Color color; late List<Offset> offsets=[]; ColorOffset(Color color,List<Offset> offsets){ this.color=color; this.offsets.addAll(offsets); } }
具體涂鴉點(diǎn)的繪制
此處是具體繪制涂鴉點(diǎn)的自定義view。大家是不是覺得哇,好簡(jiǎn)單呢。兩個(gè)循環(huán)一嵌套,瞬間所有涂鴉就都出來了。其實(shí)做這個(gè)功能時(shí),我參考了其他各種涂鴉控件,但是總覺得流程非常復(fù)雜。難以理解。原因是他們的顏色和點(diǎn)在數(shù)據(jù)層面都是混合到一起的,而且還得判斷哪里是新畫的涂鴉線條,來回控制。用這個(gè)demo的結(jié)構(gòu),相信各位讀者一看就能知道里面的思路
doodle_painter.dart
import 'package:flutter/cupertino.dart'; import 'color_offset.dart'; class DoodleImagePainter extends CustomPainter { late Map<int,ColorOffset> newPoints; DoodleImagePainter(this.newPoints); @override void paint(Canvas canvas, Size size) { newPoints.forEach((key, value) { Paint paint = _getPaint(value.color); for(int i=0;i<value.offsets.length - 1;i++){ //最后一個(gè)畫點(diǎn),其他畫線 if(i==value.offsets.length-1){ canvas.drawCircle(value.offsets[i], 2.0, paint); }else{ canvas.drawLine(value.offsets[i], value.offsets[i + 1], paint); } } }); } Paint _getPaint(Color color){ return Paint() ..color = color ..strokeCap = StrokeCap.round ..strokeWidth = 5.0; } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return true; } }
涂鴉主要界面代碼
- 包含整體涂鴉數(shù)據(jù)的構(gòu)建
- 包含涂鴉圖片的合成和本地存儲(chǔ)
- 包含涂鴉顏色列表的自定義
- 包含涂鴉原圖片的放大縮小
- 包含撤銷一步和清屏功能
下面這些就是整體涂鴉相關(guān)功能代碼,其中一些資源圖片未提供,請(qǐng)根據(jù)需要自己去設(shè)計(jì)處獲取。
import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import '../player_base_control.dart'; import 'circle_ring_widget.dart'; import 'color_offset.dart'; import 'doodle_painter.dart'; class DoodleWidget extends PlayerBaseControllableWidget { final String snapShotPath; final ValueChanged<bool>? completed; const DoodleWidget(super.controller, {super.key, required this.snapShotPath, this.completed}); @override State<StatefulWidget> createState() => _DoodleWidgetState(); } class _DoodleWidgetState extends State<DoodleWidget> { Map<int, ColorOffset> newPoints = {}; List<Offset> points = []; int lineIndex = 0; GlobalKey globalKey = GlobalKey(); int currentSelect = 0; final double maxScale = 3.0; final double minScale = 1.0; List<Color> colors = const [ Color(0xffff0000), Color(0xfffae03d), Color(0xff6f52ff), Color(0xffffffff), Color(0xff000000) ]; TransformationController controller = TransformationController(); double realScale = 1.0; Offset realTransLocation = Offset.zero; late Image currentImg; bool isSaved = false; @override void initState() { currentImg = Image.memory(File(widget.snapShotPath).readAsBytesSync()); controller.addListener(() { ///獲取矩陣?yán)锩娴目s放具體值 realScale = controller.value.entry(0, 0); ///獲取矩陣?yán)锩娴奈恢闷屏? realTransLocation = Offset(controller.value.getTranslation().x, controller.value.getTranslation().y); }); super.initState(); } @override Widget build(BuildContext context) { return Stack( children: [ Positioned.fill(child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraint) { return InteractiveViewer( panEnabled: false, scaleEnabled: true, maxScale: maxScale, minScale: minScale, transformationController: controller, onInteractionStart: (ScaleStartDetails details) { // print("--------------onInteractionStart執(zhí)行了 dx=${details.focalPoint.dx} dy=${details.focalPoint.dy}"); }, onInteractionUpdate: (ScaleUpdateDetails details) { if (details.pointerCount == 1) { /// 獲取 x,y 拿到值后進(jìn)行縮放偏移等換算 var x = details.focalPoint.dx; var y = details.focalPoint.dy; var point = Offset( _getScaleTranslateValue(x, realScale, realTransLocation.dx), _getScaleTranslateValue( y, realScale, realTransLocation.dy)); setState(() { points.add(point); newPoints[lineIndex] = ColorOffset(colors[currentSelect], points); }); } }, onInteractionEnd: (ScaleEndDetails details) { // print("onInteractionEnd執(zhí)行了"); if (points.length > 5) { newPoints[lineIndex] = ColorOffset(colors[currentSelect], points); lineIndex++; } // newPoints.addAll({lineIndex:ColorOffset(colors[currentSelect],points)}); //清空原數(shù)組 points.clear(); }, child: RepaintBoundary( key: globalKey, child: Stack( alignment: AlignmentDirectional.center, children: [ Positioned.fill(child: currentImg), Positioned.fill( child: CustomPaint(painter: DoodleImagePainter(newPoints))), ], ), ), ); })), Positioned( bottom: 0, left: 0, right: 0, child: _bottomActions(), ) ], ); } double _getScaleTranslateValue( double current, double scale, double translate) { return current / scale - translate / scale; } Widget _bottomActions() { return Container( height: 81, color: const Color(0xaa17161f), child: Row( children: [ /// 關(guān)閉按鈕 SizedBox( width: 95, height: 81, child: Center( child: GestureDetector( onTap: () { widget.completed?.call(false); }, child: Image.asset( "images/icon_close_white.webp", width: 30, height: 30, scale: 3, package: "koo_daxue_record_player", ), ), ), ), const VerticalDivider( thickness: 1, indent: 15, endIndent: 15, color: Colors.white, ), Row( children: _colorListWidget(), ), Expanded(child: Container()), /// 退一步按鈕 SizedBox( width: 66, height: 81, child: GestureDetector( onTap: () { setState(() { if (lineIndex > 0) { lineIndex--; newPoints.remove(lineIndex); } }); }, child: Center( child: Image.asset( lineIndex == 0 ? "images/icon_undo.webp" : "images/icon_undo_white.webp", width: 30, height: 30, scale: 3, package: "koo_daxue_record_player", )), ), ), /// 清除按鈕 SizedBox( width: 66, height: 81, child: Center( child: GestureDetector( onTap: () { setState(() { lineIndex = 0; newPoints.clear(); }); }, child: Image.asset( lineIndex == 0 ? "images/icon_clear_doodle.webp" : "images/icon_clear_doodle_white.webp", width: 30, height: 30, scale: 3, package: "koo_daxue_record_player", ), )), ), const VerticalDivider( thickness: 1, indent: 15, endIndent: 15, color: Colors.white, ), /// 確定按鈕 SizedBox( width: 85, height: 81, child: Center( child: GestureDetector( onTap: () { if (isSaved) return; isSaved = true; if (newPoints.isEmpty) { widget.completed?.call(false); return; } saveDoodle(widget.snapShotPath).then((value) { if (value) { widget.completed?.call(true); } else { widget.completed?.call(false); } }); }, child: Image.asset( "images/icon_finish_white.webp", width: 30, height: 30, scale: 3, package: "koo_daxue_record_player", ), )), ) ], ), ); } List<Widget> _colorListWidget() { List<Widget> widgetList = []; for (int i = 0; i < colors.length; i++) { Color color = colors[i]; widgetList.add(GestureDetector( onTap: () { setState(() { currentSelect = i; }); }, child: CircleRingWidget(i == currentSelect, color), )); } return widgetList; } Future<bool> saveDoodle(String imgPath) async { try { RenderRepaintBoundary boundary = globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary; ui.Image image = await boundary.toImage(pixelRatio: 3.0); ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png); Uint8List pngBytes = byteData!.buffer.asUint8List(); // 保存圖片到文件 File imgFile = File(imgPath); await imgFile.writeAsBytes(pngBytes); return true; } catch (e) { return false; } } }
以上就是Flutter實(shí)現(xiàn)給圖片添加涂鴉功能的詳細(xì)內(nèi)容,更多關(guān)于Flutter給圖片添加涂鴉的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)文件按時(shí)間先后順序排列顯示的示例代碼
在很多Android應(yīng)用中,需要管理和展示本地文件,對(duì)文件按最后修改時(shí)間排序展示,能讓用戶直觀地了解文件的創(chuàng)建或修改順序,從而更方便地查找最新或最舊的文件,本文將介紹如何在Android平臺(tái)上獲取指定目錄下的文件列表,并按照時(shí)間先后排序,需要的朋友可以參考下2025-04-04Flutter開發(fā)技巧RadialGradient中radius計(jì)算詳解
這篇文章主要為大家介紹了Flutter小技巧RadialGradient?中?radius?的計(jì)算詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01深入分析Android ViewStub的應(yīng)用詳解
本篇文章是對(duì)Android ViewStub的應(yīng)用進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05android實(shí)現(xiàn)session保持簡(jiǎn)要概述及實(shí)現(xiàn)
其實(shí)sesion在瀏覽器和web服務(wù)器直接是通過一個(gè)叫做name為sessionid的cookie來傳遞的,所以只要在每次數(shù)據(jù)請(qǐng)求時(shí)保持sessionid是同一個(gè)不變就可以用到web的session了,感興趣的你可以參考下本文或許對(duì)你有所幫助2013-03-03Android中DrawerLayout+ViewPager滑動(dòng)沖突的解決方法
這篇文章主要為大家詳細(xì)介紹了Android中DrawerLayout+ViewPager滑動(dòng)沖突的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06