对功能需求的分析主要从 界面数据信息、交互与数据维护、界面构建逻辑 三个方面来思考。
界面上需要呈现多条线,每条线由若干个点构成,另外可以指定线的颜色和粗细。所以这里可以封装一个 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];
线列表数据的维护和用户的拖拽事件息息相关:
在该需求中的数据和交互分析完后,就可以考虑界面的构建逻辑了。下面是一个示意简图:
到这里,主要的数据和交互,以及实现的思路就分析的差不多了,接下来就进入项目代码的编写。
如下,新建一个 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(() {
});
}
在 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
),
在点击清除按钮时,清空线列表。一般对于清除的操作,需要给用户一个确认的对话框,从而避免误操作。如下所示:
点击弹框 | 确认清除 |
---|---|
弹出对话框可以使用框架中提供的 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),
],
),
),
);
}
//...
}
本章主要完成白板绘制的基础功能,包括整体布局结构、监听拖拽事件,维护点集数据、以及绘制线集数据。其中蕴含着数据和界面展现的关系,比如,数据是如何产生的、数据在用户的交互期间有哪些变化、数据是如何呈现到界面上的,大家可以自己多多思考和理解。
到这里,界面上的线条会随着手指的拖动而呈现,完成了最基础的功能,当前代码位置 paper。下一篇将介绍一下,如何通过交互来修改画笔颜色和粗细,让界面的呈现更加丰富。