Flutter手写涂鸦插件scribble的使用
Flutter手写涂鸦插件scribble的使用
插件介绍
Scribble 是一个轻量级的Flutter库,支持自由绘制,提供压力感应、可变线宽等功能。以下是它的主要特性:
- 可变线宽
- 图像导出
- 支持笔和触摸压力
- 选择哪些指针可以绘制(触摸、笔、鼠标等)
- 笔画速度越快线条越细
- 线条橡皮擦支持
- 使用 value_notifier_tools 完整的撤销/重做支持
- 草图完全序列化为JSON
- 导出草图为PNG格式
安装
为了开始使用Scribble,您必须先安装Dart SDK。
通过 dart pub add
命令安装:
dart pub add scribble
使用示例
下面是一个完整的示例应用,展示了如何在Flutter中使用Scribble插件进行手写涂鸦。
示例代码
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:scribble/scribble.dart';
import 'package:value_notifier_tools/value_notifier_tools.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scribble',
theme: ThemeData.from(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple)),
home: const HomePage(title: 'Scribble'),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key, required this.title});
final String title;
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late ScribbleNotifier notifier;
@override
void initState() {
notifier = ScribbleNotifier();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: AppBar(
title: Text(widget.title),
actions: _buildActions(context),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 64),
child: Column(
children: [
Expanded(
child: Card(
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
color: Colors.white,
surfaceTintColor: Colors.white,
child: Scribble(
notifier: notifier,
drawPen: true,
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
_buildColorToolbar(context),
const VerticalDivider(width: 32),
_buildStrokeToolbar(context),
const Expanded(child: SizedBox()),
_buildPointerModeSwitcher(context),
],
),
)
],
),
),
);
}
List<Widget> _buildActions(context) {
return [
ValueListenableBuilder(
valueListenable: notifier,
builder: (context, value, child) => IconButton(
icon: child as Icon,
tooltip: "Undo",
onPressed: notifier.canUndo ? notifier.undo : null,
),
child: const Icon(Icons.undo),
),
ValueListenableBuilder(
valueListenable: notifier,
builder: (context, value, child) => IconButton(
icon: child as Icon,
tooltip: "Redo",
onPressed: notifier.canRedo ? notifier.redo : null,
),
child: const Icon(Icons.redo),
),
IconButton(
icon: const Icon(Icons.clear),
tooltip: "Clear",
onPressed: notifier.clear,
),
IconButton(
icon: const Icon(Icons.image),
tooltip: "Show PNG Image",
onPressed: () => _showImage(context),
),
IconButton(
icon: const Icon(Icons.data_object),
tooltip: "Show JSON",
onPressed: () => _showJson(context),
),
];
}
void _showImage(BuildContext context) async {
final image = notifier.renderImage();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Generated Image"),
content: SizedBox.expand(
child: FutureBuilder(
future: image,
builder: (context, snapshot) => snapshot.hasData
? Image.memory(snapshot.data!.buffer.asUint8List())
: const Center(child: CircularProgressIndicator()),
),
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text("Close"),
)
],
),
);
}
void _showJson(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Sketch as JSON"),
content: SizedBox.expand(
child: SelectableText(
jsonEncode(notifier.currentSketch.toJson()),
autofocus: true,
),
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text("Close"),
)
],
),
);
}
Widget _buildStrokeToolbar(BuildContext context) {
return ValueListenableBuilder<ScribbleState>(
valueListenable: notifier,
builder: (context, state, _) => Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (final w in notifier.widths)
_buildStrokeButton(
context,
strokeWidth: w,
state: state,
),
],
),
);
}
Widget _buildStrokeButton(
BuildContext context, {
required double strokeWidth,
required ScribbleState state,
}) {
final selected = state.selectedWidth == strokeWidth;
return Padding(
padding: const EdgeInsets.all(4),
child: Material(
elevation: selected ? 4 : 0,
shape: const CircleBorder(),
child: InkWell(
onTap: () => notifier.setStrokeWidth(strokeWidth),
customBorder: const CircleBorder(),
child: AnimatedContainer(
duration: kThemeAnimationDuration,
width: strokeWidth * 2,
height: strokeWidth * 2,
decoration: BoxDecoration(
color: state.map(
drawing: (s) => Color(s.selectedColor),
erasing: (_) => Colors.transparent,
),
border: state.map(
drawing: (_) => null,
erasing: (_) => Border.all(width: 1),
),
borderRadius: BorderRadius.circular(50.0)),
),
),
),
);
}
Widget _buildColorToolbar(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
_buildColorButton(context, color: Colors.black),
_buildColorButton(context, color: Colors.red),
_buildColorButton(context, color: Colors.green),
_buildColorButton(context, color: Colors.blue),
_buildColorButton(context, color: Colors.yellow),
_buildEraserButton(context),
],
);
}
Widget _buildPointerModeSwitcher(BuildContext context) {
return ValueListenableBuilder(
valueListenable: notifier.select(
(value) => value.allowedPointersMode,
),
builder: (context, value, child) {
return SegmentedButton<ScribblePointerMode>(
multiSelectionEnabled: false,
emptySelectionAllowed: false,
onSelectionChanged: (v) => notifier.setAllowedPointersMode(v.first),
segments: const [
ButtonSegment(
value: ScribblePointerMode.all,
icon: Icon(Icons.touch_app),
label: Text("All pointers"),
),
ButtonSegment(
value: ScribblePointerMode.penOnly,
icon: Icon(Icons.draw),
label: Text("Pen only"),
),
],
selected: {value},
);
});
}
Widget _buildEraserButton(BuildContext context) {
return ValueListenableBuilder(
valueListenable: notifier.select((value) => value is Erasing),
builder: (context, value, child) => ColorButton(
color: Colors.transparent,
outlineColor: Colors.black,
isActive: value,
onPressed: () => notifier.setEraser(),
child: const Icon(Icons.cleaning_services),
),
);
}
Widget _buildColorButton(
BuildContext context, {
required Color color,
}) {
return ValueListenableBuilder(
valueListenable: notifier.select(
(value) => value is Drawing && value.selectedColor == color.value),
builder: (context, value, child) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ColorButton(
color: color,
isActive: value,
onPressed: () => notifier.setColor(color),
),
),
);
}
}
class ColorButton extends StatelessWidget {
const ColorButton({
required this.color,
required this.isActive,
required this.onPressed,
this.outlineColor,
this.child,
super.key,
});
final Color color;
final Color? outlineColor;
final bool isActive;
final VoidCallback onPressed;
final Icon? child;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: kThemeAnimationDuration,
decoration: ShapeDecoration(
shape: CircleBorder(
side: BorderSide(
color: switch (isActive) {
true => outlineColor ?? color,
false => Colors.transparent,
},
width: 2,
),
),
),
child: IconButton(
style: FilledButton.styleFrom(
backgroundColor: color,
shape: const CircleBorder(),
side: isActive
? const BorderSide(color: Colors.white, width: 2)
: const BorderSide(color: Colors.transparent),
),
onPressed: onPressed,
icon: child ?? const SizedBox(),
),
);
}
}
关键点说明
- Scribble Notifier:通过
ScribbleNotifier
来管理绘制状态。 - 绘制区域:使用
Scribble
小部件创建绘制区域,并传递ScribbleNotifier
。 - 控制按钮:提供了撤销、重做、清除、导出图像和JSON等功能按钮。
- 颜色和宽度选择器:动态调整绘笔的颜色和宽度。
- 指针模式切换:可以选择所有指针或仅限笔绘制。
通过以上步骤,您可以轻松地将手写涂鸦功能集成到您的Flutter应用中。如果有任何问题或需要进一步的帮助,请参考Scribble GitHub仓库。
更多关于Flutter手写涂鸦插件scribble的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
1 回复
更多关于Flutter手写涂鸦插件scribble的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,下面是一个关于如何在Flutter应用中使用scribble
插件来实现手写涂鸦功能的代码示例。scribble
插件允许用户在画布上进行自由绘制,非常适合实现涂鸦、签名等功能。
首先,确保你已经在pubspec.yaml
文件中添加了scribble
依赖:
dependencies:
flutter:
sdk: flutter
scribble: ^最新版本号 # 请替换为当前可用的最新版本号
然后运行flutter pub get
来安装依赖。
接下来是一个简单的Flutter应用示例,展示了如何使用scribble
插件:
import 'package:flutter/material.dart';
import 'package:scribble/scribble.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scribble Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Scribble Demo'),
),
body: DrawingBoard(),
),
);
}
}
class DrawingBoard extends StatefulWidget {
@override
_DrawingBoardState createState() => _DrawingBoardState();
}
class _DrawingBoardState extends State<DrawingBoard> {
final _controller = ScribbleController();
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Scribble(
controller: _controller,
backgroundColor: Colors.white,
strokeColor: Colors.black,
strokeWidth: 5.0,
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// 清除画布
_controller.clear();
},
child: Text('Clear'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
// 保存图片到相册(需要添加权限处理)
final imageFile = await _controller.exportImage();
// 这里可以添加将imageFile保存到相册的代码,具体实现取决于平台
print('Image saved to $imageFile');
},
child: Text('Save Image'),
),
],
),
);
}
}
代码解释
-
依赖添加:
- 在
pubspec.yaml
文件中添加scribble
依赖。
- 在
-
主应用:
MyApp
类定义了应用的主结构和主题。
-
绘图板:
DrawingBoard
是一个有状态的组件,它持有一个ScribbleController
实例来控制绘图操作。Scribble
组件用于显示绘图区域,接受controller
、backgroundColor
、strokeColor
和strokeWidth
等参数。
-
按钮操作:
- 第一个按钮调用
_controller.clear()
方法清除画布。 - 第二个按钮调用
_controller.exportImage()
方法导出绘制的图像,并打印图像文件的路径(实际应用中可以将图像保存到相册,这需要额外的权限处理和平台特定代码)。
- 第一个按钮调用
注意事项
- 权限处理:保存图像到相册通常需要请求存储权限,这部分代码没有包含在上述示例中,需要根据平台(iOS和Android)分别处理。
- 图像保存:
_controller.exportImage()
方法返回的是一个File
对象,你可以根据需要使用它。
这个示例提供了一个基础框架,你可以根据需要进一步扩展功能,比如添加颜色选择器、笔触大小调整器等。