Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Last active March 22, 2025 00:27
Show Gist options
  • Save slightfoot/3259279e23688039f69489394eea75f9 to your computer and use it in GitHub Desktop.
Save slightfoot/3259279e23688039f69489394eea75f9 to your computer and use it in GitHub Desktop.
Drawing Area Example - by Simon Lightfoot :: 21/03/2025
// MIT License
//
// Copyright (c) 2025 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:async';
import 'dart:collection';
import 'dart:ui' as ui show Image, PointMode, PictureRecorder;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show BoxHitTestEntry;
void main() {
runApp(ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(useMaterial3: false),
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
Paint strokePaint = Paint()
..style = PaintingStyle.stroke
..color = Colors.white
..strokeWidth = 10.0
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
ui.Image? image;
void _onDrawingChanged(Drawing drawing) {
setState(() {
image = drawing.toImage(50, 50);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Painter'),
actions: [
IconButton(
onPressed: () {},
icon: Icon(Icons.delete),
),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: image != null //
? FittedBox(
child: RawImage(
image: image!,
filterQuality: FilterQuality.none,
),
)
: SizedBox(),
),
const Divider(),
Expanded(
child: DrawingArea(
strokePaint: strokePaint,
onDrawingChanged: _onDrawingChanged,
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (final primary in Colors.primaries) //
IconButton(
onPressed: () {
setState(() {
strokePaint = Paint.from(strokePaint) //
..color = primary;
});
},
padding: EdgeInsets.symmetric(vertical: 24.0),
color: primary,
icon: Icon(Icons.circle),
),
],
),
),
],
),
);
}
}
class Drawing {
const Drawing({
required this.size,
required this.polylines,
});
final Size size;
final List<Polyline> polylines;
static const empty = Drawing(size: Size.zero, polylines: []);
void render(Canvas canvas) {
for (final polyline in polylines) {
canvas.drawPoints(
ui.PointMode.polygon,
polyline.points,
polyline.strokePaint,
);
}
}
ui.Image toImage(int width, int height) {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder, Offset.zero & size);
canvas.scale(width / size.width, height / size.height);
render(canvas);
final picture = recorder.endRecording();
return picture.toImageSync(width, height);
}
}
class Polyline {
const Polyline({required this.strokePaint, required this.points});
final Paint strokePaint;
final List<Offset> points;
}
class DrawingArea extends SingleChildRenderObjectWidget {
const DrawingArea({
super.key,
required this.strokePaint,
required this.onDrawingChanged,
});
final Paint strokePaint;
final ValueChanged<Drawing> onDrawingChanged;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderDrawingArea(
strokePaint: strokePaint,
onDrawingChanged: onDrawingChanged,
);
}
@override
void updateRenderObject(
BuildContext context,
covariant RenderDrawingArea renderObject,
) {
renderObject
..strokePaint = strokePaint
..onDrawingChanged = onDrawingChanged;
}
}
class RenderDrawingArea extends RenderBox {
RenderDrawingArea({
required Paint strokePaint,
required this.onDrawingChanged,
}) : _strokePaint = strokePaint;
final polylines = <Polyline>[];
Paint _strokePaint;
ValueChanged<Drawing> onDrawingChanged;
Paint get strokePaint => _strokePaint;
set strokePaint(Paint value) {
if (_strokePaint != value) {
_strokePaint = value;
}
}
Drawing get drawing {
return Drawing(
size: size,
polylines: UnmodifiableListView(polylines),
);
}
@override
void performLayout() {
size = constraints.biggest;
}
@override
bool hitTestSelf(Offset position) => true;
void dispatchDrawingUpdate() {
scheduleMicrotask(() => onDrawingChanged(drawing));
}
@override
void handleEvent(PointerEvent event, covariant BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry));
switch (event) {
case PointerDownEvent():
startNewPolyline();
addPoint(event.localPosition);
dispatchDrawingUpdate();
case PointerMoveEvent():
addPoint(event.localPosition);
dispatchDrawingUpdate();
case PointerUpEvent():
addPoint(event.localPosition);
dispatchDrawingUpdate();
}
}
void startNewPolyline() {
polylines.add(Polyline(strokePaint: strokePaint, points: []));
}
void addPoint(Offset offset) {
polylines.last.points.add(offset);
}
@override
void paint(PaintingContext context, Offset offset) {
context.clipRectAndPaint(
offset & size,
Clip.hardEdge,
offset & size,
() {
final canvas = context.canvas;
canvas.save();
canvas.translate(offset.dx, offset.dy);
drawing.render(context.canvas);
canvas.restore();
},
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment