Flutter實現(xiàn)給圖片添加涂鴉功能
簡介
先來張圖,看一下最終效果

關閉和確定
- 關閉和確定功能對應的是界面左下角叉號,和右下角對鉤,關閉按鈕僅做取消當次涂鴉,讀者可自行設置點擊后功能,也可自行更改相應UI。選擇功能點擊后會執(zhí)行一段把當前涂鴉后的圖片合成并保存到本地的操作。具體請看示例代碼。
顏色選擇
- 顏色選擇功能可選擇和標識當前涂鴉顏色,和指示當前涂鴉顏色的選中狀態(tài)(以白色外圈標識)。切換顏色后下一次涂鴉即會使用新的顏色。
撤銷功能
- 撤銷功能可撤銷最近的一次涂鴉。如沒有涂鴉時顯示置灰的撤銷按鈕。
清除功能
- 清除功能可清除所有涂鴉,如當前沒有任何涂鴉時顯示置灰的清除按鈕。
涂鴉圖片的放大和縮小
- 可雙指滑動切換涂鴉放大縮小的效果。
放大縮小后按照新的線條粗細繼續(xù)涂鴉
- 涂鴉放大或縮小后,涂鴉線條會隨之放大和縮小,此時如果繼續(xù)涂鴉,則新涂鴉顯示的粗細程度與放大或縮小后的線條粗細程度保持一致。
保存涂鴉圖片到本地。
- flutter涂鴉后的圖片可合成新圖片并保存到本地路徑。
代碼介紹
涂鴉顏色選擇組件。
主要是顯示為可配置的圓點和外圈
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), // 調整尺寸大小
);
}
}
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 // 設置圓點的顏色
..strokeCap = StrokeCap.round
..strokeWidth = 1.0;
Paint ringPaint = Paint()
..color = Colors.white // 設置圓環(huán)的顏色
..strokeCap = StrokeCap.round
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
Offset center = size.center(Offset.zero);
// 畫一個半徑為10的圓點
canvas.drawCircle(center, 13.0, circlePaint);
if(isShowRing){
// 畫一個半徑為20的圓環(huán)
canvas.drawCircle(center, 18.0, ringPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
存儲顏色和食指劃過點的數(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);
}
}
具體涂鴉點的繪制
此處是具體繪制涂鴉點的自定義view。大家是不是覺得哇,好簡單呢。兩個循環(huán)一嵌套,瞬間所有涂鴉就都出來了。其實做這個功能時,我參考了其他各種涂鴉控件,但是總覺得流程非常復雜。難以理解。原因是他們的顏色和點在數(shù)據(jù)層面都是混合到一起的,而且還得判斷哪里是新畫的涂鴉線條,來回控制。用這個demo的結構,相信各位讀者一看就能知道里面的思路
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++){
//最后一個畫點,其他畫線
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ù)的構建
- 包含涂鴉圖片的合成和本地存儲
- 包含涂鴉顏色列表的自定義
- 包含涂鴉原圖片的放大縮小
- 包含撤銷一步和清屏功能
下面這些就是整體涂鴉相關功能代碼,其中一些資源圖片未提供,請根據(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(() {
///獲取矩陣里面的縮放具體值
realScale = controller.value.entry(0, 0);
///獲取矩陣里面的位置偏移量
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 拿到值后進行縮放偏移等換算
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: [
/// 關閉按鈕
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實現(xiàn)給圖片添加涂鴉功能的詳細內容,更多關于Flutter給圖片添加涂鴉的資料請關注腳本之家其它相關文章!
相關文章
Android實現(xiàn)文件按時間先后順序排列顯示的示例代碼
在很多Android應用中,需要管理和展示本地文件,對文件按最后修改時間排序展示,能讓用戶直觀地了解文件的創(chuàng)建或修改順序,從而更方便地查找最新或最舊的文件,本文將介紹如何在Android平臺上獲取指定目錄下的文件列表,并按照時間先后排序,需要的朋友可以參考下2025-04-04
Flutter開發(fā)技巧RadialGradient中radius計算詳解
這篇文章主要為大家介紹了Flutter小技巧RadialGradient?中?radius?的計算詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01
android實現(xiàn)session保持簡要概述及實現(xiàn)
其實sesion在瀏覽器和web服務器直接是通過一個叫做name為sessionid的cookie來傳遞的,所以只要在每次數(shù)據(jù)請求時保持sessionid是同一個不變就可以用到web的session了,感興趣的你可以參考下本文或許對你有所幫助2013-03-03
Android中DrawerLayout+ViewPager滑動沖突的解決方法
這篇文章主要為大家詳細介紹了Android中DrawerLayout+ViewPager滑動沖突的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06

