-
Notifications
You must be signed in to change notification settings - Fork 66
/
painter.dart
325 lines (274 loc) · 9.09 KB
/
painter.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
/// Provides a widget and an associated controller for simple painting using touch.
library painter;
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart' hide Image;
import 'package:flutter/widgets.dart' hide Image;
/// A very simple widget that supports drawing using touch.
class Painter extends StatefulWidget {
final PainterController painterController;
/// Creates an instance of this widget that operates on top of the supplied [PainterController].
Painter(PainterController painterController)
: this.painterController = painterController,
super(key: new ValueKey<PainterController>(painterController));
@override
_PainterState createState() => new _PainterState();
}
class _PainterState extends State<Painter> {
bool _finished = false;
@override
void initState() {
super.initState();
widget.painterController._widgetFinish = _finish;
}
Size _finish() {
setState(() {
_finished = true;
});
return context.size ?? const Size(0, 0);
}
@override
Widget build(BuildContext context) {
Widget child = new CustomPaint(
willChange: true,
painter: new _PainterPainter(widget.painterController._pathHistory,
repaint: widget.painterController),
);
child = new ClipRect(child: child);
if (!_finished) {
child = new GestureDetector(
child: child,
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
);
}
return new Container(
child: child,
width: double.infinity,
height: double.infinity,
);
}
void _onPanStart(DragStartDetails start) {
Offset pos = (context.findRenderObject() as RenderBox)
.globalToLocal(start.globalPosition);
widget.painterController._pathHistory.add(pos);
widget.painterController._notifyListeners();
}
void _onPanUpdate(DragUpdateDetails update) {
Offset pos = (context.findRenderObject() as RenderBox)
.globalToLocal(update.globalPosition);
widget.painterController._pathHistory.updateCurrent(pos);
widget.painterController._notifyListeners();
}
void _onPanEnd(DragEndDetails end) {
widget.painterController._pathHistory.endCurrent();
widget.painterController._notifyListeners();
}
}
class _PainterPainter extends CustomPainter {
final _PathHistory _path;
_PainterPainter(this._path, {Listenable? repaint}) : super(repaint: repaint);
@override
void paint(Canvas canvas, Size size) {
_path.draw(canvas, size);
}
@override
bool shouldRepaint(_PainterPainter oldDelegate) {
return true;
}
}
class _PathHistory {
List<MapEntry<Path, Paint>> _paths;
Paint currentPaint;
Paint _backgroundPaint;
bool _inDrag;
bool get isEmpty => _paths.isEmpty || (_paths.length == 1 && _inDrag);
_PathHistory()
: _paths = <MapEntry<Path, Paint>>[],
_inDrag = false,
_backgroundPaint = new Paint()..blendMode = BlendMode.dstOver,
currentPaint = new Paint()
..color = Colors.black
..strokeWidth = 1.0
..style = PaintingStyle.fill;
void setBackgroundColor(Color backgroundColor) {
_backgroundPaint.color = backgroundColor;
}
void undo() {
if (!_inDrag) {
_paths.removeLast();
}
}
void clear() {
if (!_inDrag) {
_paths.clear();
}
}
void add(Offset startPoint) {
if (!_inDrag) {
_inDrag = true;
Path path = new Path();
path.moveTo(startPoint.dx, startPoint.dy);
_paths.add(new MapEntry<Path, Paint>(path, currentPaint));
}
}
void updateCurrent(Offset nextPoint) {
if (_inDrag) {
Path path = _paths.last.key;
path.lineTo(nextPoint.dx, nextPoint.dy);
}
}
void endCurrent() {
_inDrag = false;
}
void draw(Canvas canvas, Size size) {
canvas.saveLayer(Offset.zero & size, Paint());
for (MapEntry<Path, Paint> path in _paths) {
Paint p = path.value;
canvas.drawPath(path.key, p);
}
canvas.drawRect(
new Rect.fromLTWH(0.0, 0.0, size.width, size.height), _backgroundPaint);
canvas.restore();
}
}
/// Container that holds the size of a finished drawing and the drawed data as [Picture].
class PictureDetails {
/// The drawings data as [Picture].
final Picture picture;
/// The width of the drawing.
final int width;
/// The height of the drawing.
final int height;
/// Creates an immutable instance with the given drawing information.
const PictureDetails(this.picture, this.width, this.height);
/// Converts the [picture] to an [Image].
Future<Image> toImage() => picture.toImage(width, height);
/// Converts the [picture] to a PNG and returns the bytes of the PNG.
///
/// This might throw a [FlutterError], if flutter is not able to convert
/// the intermediate [Image] to a PNG.
Future<Uint8List> toPNG() async {
Image image = await toImage();
ByteData? data = await image.toByteData(format: ImageByteFormat.png);
if (data != null) {
return data.buffer.asUint8List();
} else {
throw new FlutterError('Flutter failed to convert an Image to bytes!');
}
}
}
/// Used with a [Painter] widget to control drawing.
class PainterController extends ChangeNotifier {
Color _drawColor = new Color.fromARGB(255, 0, 0, 0);
Color _backgroundColor = new Color.fromARGB(255, 255, 255, 255);
bool _eraseMode = false;
double _thickness = 1.0;
PictureDetails? _cached;
_PathHistory _pathHistory;
ValueGetter<Size>? _widgetFinish;
/// Creates a new instance for the use in a [Painter] widget.
PainterController() : _pathHistory = new _PathHistory();
/// Returns true if nothing has been drawn yet.
bool get isEmpty => _pathHistory.isEmpty;
/// Returns true if the the [PainterController] is currently in erase mode,
/// false otherwise.
bool get eraseMode => _eraseMode;
/// If set to true, erase mode is enabled, until this is called again with
/// false to disable erase mode.
set eraseMode(bool enabled) {
_eraseMode = enabled;
_updatePaint();
}
/// Retrieves the current draw color.
Color get drawColor => _drawColor;
/// Sets the draw color.
set drawColor(Color color) {
_drawColor = color;
_updatePaint();
}
/// Retrieves the current background color.
Color get backgroundColor => _backgroundColor;
/// Updates the background color.
set backgroundColor(Color color) {
_backgroundColor = color;
_updatePaint();
}
/// Returns the current thickness that is used for drawing.
double get thickness => _thickness;
/// Sets the draw thickness..
set thickness(double t) {
_thickness = t;
_updatePaint();
}
void _updatePaint() {
Paint paint = new Paint();
if (_eraseMode) {
paint.blendMode = BlendMode.clear;
paint.color = Color.fromARGB(0, 255, 0, 0);
} else {
paint.color = drawColor;
paint.blendMode = BlendMode.srcOver;
}
paint.style = PaintingStyle.stroke;
paint.strokeWidth = thickness;
_pathHistory.currentPaint = paint;
_pathHistory.setBackgroundColor(backgroundColor);
notifyListeners();
}
/// Undoes the last drawing action (but not a background color change).
/// If the picture is already finished, this is a no-op and does nothing.
void undo() {
if (!isFinished()) {
_pathHistory.undo();
notifyListeners();
}
}
void _notifyListeners() {
notifyListeners();
}
/// Deletes all drawing actions, but does not affect the background.
/// If the picture is already finished, this is a no-op and does nothing.
void clear() {
if (!isFinished()) {
_pathHistory.clear();
notifyListeners();
}
}
/// Finishes drawing and returns the rendered [PictureDetails] of the drawing.
/// The drawing is cached and on subsequent calls to this method, the cached
/// drawing is returned.
///
/// This might throw a [StateError] if this PainterController is not attached
/// to a widget, or the associated widget's [Size.isEmpty].
PictureDetails finish() {
if (!isFinished()) {
if (_widgetFinish != null) {
_cached = _render(_widgetFinish!());
} else {
throw new StateError(
'Called finish on a PainterController that was not connected to a widget yet!');
}
}
return _cached!;
}
PictureDetails _render(Size size) {
if (size.isEmpty) {
throw new StateError('Tried to render a picture with an invalid size!');
} else {
PictureRecorder recorder = new PictureRecorder();
Canvas canvas = new Canvas(recorder);
_pathHistory.draw(canvas, size);
return new PictureDetails(
recorder.endRecording(), size.width.floor(), size.height.floor());
}
}
/// Returns true if this drawing is finished.
///
/// Trying to modify a finished drawing is a no-op.
bool isFinished() {
return _cached != null;
}
}