295 lines
7.8 KiB
Markdown
295 lines
7.8 KiB
Markdown
|
||
|
||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||
|
||
|
||
20 通过手势在白板上绘制
|
||
1. 白板绘制思路分析
|
||
|
||
对功能需求的分析主要从 界面数据信息、交互与数据维护、界面构建逻辑 三个方面来思考。
|
||
|
||
|
||
数据信息
|
||
|
||
|
||
界面上需要呈现多条线,每条线由若干个点构成,另外可以指定线的颜色和粗细。所以这里可以封装一个 Line 类维护这些数据:
|
||
|
||
class Line {
|
||
List<Offset> points;
|
||
Color color;
|
||
double strokeWidth;
|
||
|
||
Line({
|
||
required this.points,
|
||
this.color = Colors.black,
|
||
this.strokeWidth = 1,
|
||
});
|
||
}
|
||
|
||
|
||
|
||
|
||
然后需要 List<Line> 线列表来表示若干条线;由于支持颜色和粗细选择,需要给出支持的颜色、粗细选项列表,以及两者的激活索引:
|
||
|
||
List<Line> _lines = []; // 线列表
|
||
|
||
int _activeColorIndex = 0; // 颜色激活索引
|
||
int _activeStorkWidthIndex = 0; // 线宽激活索引
|
||
|
||
// 支持的颜色
|
||
final List<Color> supportColors = [
|
||
Colors.black, Colors.red, Colors.orange,
|
||
Colors.yellow, Colors.green, Colors.blue,
|
||
Colors.indigo, Colors.purple,
|
||
];
|
||
|
||
// 支持的线粗
|
||
final List<double> supportStorkWidths = [1,2, 4, 6, 8, 10];
|
||
|
||
|
||
|
||
|
||
|
||
交互与数据维护
|
||
|
||
|
||
线列表数据的维护和用户的拖拽事件息息相关:
|
||
|
||
|
||
用户开始拖拽开始时,需要创建 Line 对象,加入线列表。
|
||
用户拖拽过程中,将触点添加到线列表最后一条线中。
|
||
用户点击清除时,清空线列表。
|
||
用户可以通过交互选择颜色,更新颜色激活索引。
|
||
用户可以通过交互选择线宽,更新线宽激活索引。
|
||
|
||
|
||
|
||
|
||
|
||
界面构建逻辑
|
||
|
||
|
||
在该需求中的数据和交互分析完后,就可以考虑界面的构建逻辑了。下面是一个示意简图:
|
||
|
||
|
||
|
||
|
||
使用 Scaffold + Appbar 组件构建整体结构。
|
||
使用 CustomPaint 组件构建主体内容,作为可绘制区域。
|
||
通过 GestureDetector 组件监听拖拽手势,维护数据变化。
|
||
通过 Stack 组件将画笔颜色和线粗选择器叠放在绘制区上。
|
||
画笔颜色和线粗选择器的构建将在下一篇详细介绍。
|
||
|
||
|
||
到这里,主要的数据和交互,以及实现的思路就分析的差不多了,接下来就进入项目代码的编写。
|
||
|
||
|
||
|
||
2. 手势交互与数据维护
|
||
|
||
如下,新建一个 paper 文件夹,用于盛放白板绘制的相关代码。其中:
|
||
|
||
|
||
model.dart 中盛放相关的数据模型,比如 Line 类。
|
||
paper_app_bar.dart 是抽离的头部标题组件。
|
||
paper.dart 是白板绘制的主界面。
|
||
|
||
|
||
首先在 _PaperState 中放入之前分析的数据:
|
||
|
||
|
||
|
||
|
||
|
||
想要监听用户的拖拽手势,可以使用 GestureDetector 组件的 pan 系列回调。如下所示,在 CustomPaint 之上嵌套 GestureDetector 组件;通过 onPanStart 可以监听到用户开始拖拽那刻的事件、通过 onPanUpdate 可以监听到用户拖拽中的事件:
|
||
|
||
body: GestureDetector(
|
||
onPanStart: _onPanStart,
|
||
onPanUpdate: _onPanUpdate,
|
||
child: CustomPaint(
|
||
//略同...
|
||
),
|
||
)
|
||
|
||
void _onPanStart(DragStartDetails details) {}
|
||
|
||
void _onPanUpdate(DragUpdateDetails details) {}
|
||
|
||
|
||
|
||
|
||
回调中的逻辑处理也比较简单,在开始拖拽时为线列表添加新线;此时新线就是 _lines 的最后一个元素,在拖拽中,为新线添加点即可:
|
||
|
||
// 拖拽开始,添加新线
|
||
void _onPanStart(DragStartDetails details) {
|
||
_lines.add(Line(points: [details.localPosition],));
|
||
}
|
||
|
||
// 拖拽中,为新线添加点
|
||
void _onPanUpdate(DragUpdateDetails details) {
|
||
_lines.last.points.add(details.localPosition);
|
||
setState(() {
|
||
|
||
});
|
||
}
|
||
|
||
|
||
|
||
|
||
3. 画板的绘制逻辑
|
||
|
||
在 PaperPainter 中处理绘制逻辑,绘制过程中需要线列表的数据,而数据在 _PaperState 中维护。可以通过构造函数,将数据传入 PaperPainter 中,以供绘制时使用:
|
||
|
||
class PaperPainter extends CustomPainter {
|
||
PaperPainter({
|
||
required this.lines,
|
||
}) {
|
||
_paint = Paint()
|
||
..style = PaintingStyle.stroke
|
||
..strokeCap = StrokeCap.round;
|
||
}
|
||
|
||
late Paint _paint;
|
||
final List<Line> lines;
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
for (int i = 0; i < lines.length; i++) {
|
||
drawLine(canvas, lines[i]);
|
||
}
|
||
}
|
||
|
||
///根据点集绘制线
|
||
void drawLine(Canvas canvas, Line line) {
|
||
_paint.color = line.color;
|
||
_paint.strokeWidth = line.strokeWidth;
|
||
canvas.drawPoints(PointMode.polygon, line.points, _paint);
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||
}
|
||
|
||
|
||
绘制逻辑在上面的 paint 方法中,便历 Line 列表,通过 drawPoints 绘制 PointMode.polygon 类型的点集; Line 对象中的颜色和边线数据,可以在绘制前为画笔设置对应属性。最后,在 _PaperState 状态类中,当 PaperPainter 对象创建时,将 _lines 列表作为入参提供给画板即可:
|
||
|
||
|
||
|
||
|
||
————————————————————
|
||
————————————————————
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
--->[_PaperState]----
|
||
child: CustomPaint(
|
||
painter: PaperPainter(
|
||
lines: _lines
|
||
),
|
||
|
||
|
||
|
||
|
||
4.弹出对话框
|
||
|
||
在点击清除按钮时,清空线列表。一般对于清除的操作,需要给用户一个确认的对话框,从而避免误操作。如下所示:
|
||
|
||
|
||
|
||
|
||
点击弹框
|
||
确认清除
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
弹出对话框可以使用框架中提供的 showDialog 方法,在 builder 回调函数中创建需要展示的组件。这里封装了 ConformDialog 组件用于展示对话框,将一些文字描述作为参数。这样其他地方想弹出类似的对话框,可以用 ConformDialog 组件,这就是封装的可复用性。
|
||
|
||
void _showClearDialog() {
|
||
String msg = "您的当前操作会清空绘制内容,是否确定删除!";
|
||
showDialog(
|
||
context: context,
|
||
builder: (ctx) => ConformDialog(
|
||
title: '清空提示',
|
||
conformText: '确定',
|
||
msg: msg,
|
||
onConform: _clear,
|
||
));
|
||
}
|
||
|
||
// 点击清除按钮,清空线列表
|
||
void _clear() {
|
||
_lines.clear();
|
||
setState(() {
|
||
});
|
||
}
|
||
|
||
|
||
|
||
|
||
对话框背景是不透明灰色,这种展示效果可以使用 Dialog 组件;弹框整体结构也比较简单,是上中下竖直排列,可以使用 Column 组件;标题、消息内容、按钮这里通过三个函数来构建:
|
||
|
||
|
||
小练习: 自己完成 ConformDialog 中三个构建函数的逻辑。
|
||
|
||
|
||
|
||
|
||
class ConformDialog extends StatelessWidget {
|
||
final String title;
|
||
final String msg;
|
||
final String conformText;
|
||
final VoidCallback onConform;
|
||
|
||
ConformDialog({
|
||
Key? key,
|
||
required this.title,
|
||
required this.msg,
|
||
required this.onConform,
|
||
this.conformText = '删除',
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Dialog(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(20.0),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: <Widget>[
|
||
_buildTitle(),
|
||
_buildMessage(),
|
||
_buildButtons(context),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
//...
|
||
}
|
||
|
||
|
||
|
||
|
||
5. 本章小结
|
||
|
||
本章主要完成白板绘制的基础功能,包括整体布局结构、监听拖拽事件,维护点集数据、以及绘制线集数据。其中蕴含着数据和界面展现的关系,比如,数据是如何产生的、数据在用户的交互期间有哪些变化、数据是如何呈现到界面上的,大家可以自己多多思考和理解。
|
||
|
||
到这里,界面上的线条会随着手指的拖动而呈现,完成了最基础的功能,当前代码位置 paper。下一篇将介绍一下,如何通过交互来修改画笔颜色和粗细,让界面的呈现更加丰富。
|
||
|
||
|
||
|
||
|