draw_your_image 0.8.0
draw_your_image: ^0.8.0 copied to clipboard
A Flutter package which enables users to draw with fingers in your designed UI.
draw_your_image #
A Flutter package for creating customizable drawing canvases with a declarative API.

Core Concept #
Fully declarative, fully customizable
- No controllers required - Manage stroke data in your widget state
- Customize everything - Control stroke behavior through simple callbacks
- Bring your own features - Implement undo/redo, zoom/pan, image export as your app needs
This package focuses on providing a flexible drawing widget, leaving app-specific features to you.
Features #
โจ Device-aware drawing - Distinguish between stylus, finger, and mouse input
๐จ Flexible stroke handling - Customize behavior per input device or any other criteria
๐๏ธ Built-in smoothing - Catmull-Rom spline interpolation included
โ๏ธ Fully customizable - Colors, widths, smoothing algorithms
๐งน Multiple erasing modes - Pixel-level and stroke-level erasing
๐ Intersection detection - Customizable stroke overlap detection
Quick Start #
class MyDrawingPage extends StatefulWidget {
@override
_MyDrawingPageState createState() => _MyDrawingPageState();
}
class _MyDrawingPageState extends State<MyDrawingPage> {
/// Store all the strokes on app side as state
List<Stroke> _strokes = [];
@override
Widget build(BuildContext context) {
return Draw(
strokes: _strokes, // pass strokes via
onStrokeDrawn: (stroke) {
// store new drawn stroke and rebuild
setState(() {
_strokes = [..._strokes, stroke];
});
},
);
}
}
That's it! The canvas accepts any input and draws with default settings.
Device-Aware Drawing with onStrokeStarted #
The onStrokeStarted callback lets you control stroke behavior based on input devices or any other criteria.
Stroke? Function(Stroke newStroke, Stroke? currentStroke)
Parameters:
newStroke- The stroke about to be startedcurrentStroke- The stroke currently being drawn (null if none)
Return:
- The stroke to draw (can be
newStroke,currentStroke, or a modified version) nullto reject the stroke
Example: Stylus draws, finger erases #
If you want to draw lines with stylus while erase them with a finger, the function can be implemented like below:
extension on PointerDeviceKind {
bool get isStylus =>
this ||
this == PointerDeviceKind.invertedStylus;
}
Stroke? customHandler(Stroke newStroke, Stroke? currentStroke) {
// if we have an ongoing stroke, just continue.
if (currentStroke != null) {
return currentStroke;
}
if (newStroke.deviceKind == PointerDeviceKind.stylus) {
// if stylus, draw black line
return newStroke.copyWith(color: Colors.black);
} else {
// if finger, erasor mode
return newStroke.copyWith(isErasing: true, width: 20.0);
}
}
Draw(
strokes: _strokes,
onStrokeDrawn: (stroke) => setState(() => _strokes.add(stroke)),
onStrokeStarted: customHandler,
)
Example: Stylus-only drawing #
If pre-defined utility functions fit to your needs, you can pick one of them.
Draw(
strokes: _strokes,
onStrokeDrawn: (stroke) => setState(() => _strokes.add(stroke)),
onStrokeStarted: stylusOnlyHandler, // Pre-defined utility
)
Pre-defined Utilities #
stylusOnlyHandler- Accept only stylus inputstylusPriorHandler- Prioritize stylus when drawing (palm rejection)
API Reference #
Draw Widget Properties #
| Property | Type | Required | Description |
|---|---|---|---|
strokes |
List<Stroke> |
โ | List of strokes to display |
onStrokeDrawn |
void Function(Stroke) |
โ | Called when a stroke is complete |
onStrokeStarted |
Stroke? Function(Stroke, Stroke?) |
Control stroke behavior based on input | |
onStrokeUpdated |
Stroke? Function(Stroke) |
Modify stroke in real-time as points are added | |
onStrokesRemoved |
void Function(List<Stroke>) |
Called when strokes are removed by erasing | |
strokeColor |
Color |
Default stroke color | |
strokeWidth |
double |
Default stroke width | |
backgroundColor |
Color |
Canvas background color | |
erasingBehavior |
ErasingBehavior |
Erasing mode (none, pixel, stroke) |
|
smoothingFunc |
Path Function(Stroke) |
Custom smoothing function | |
strokePainter |
List<Paint> Function(Stroke) |
Custom stroke painting function | |
intersectionDetector |
IntersectionDetector |
Custom intersection detection function | |
shouldAbsorb |
bool Function(PointerDownEvent) |
Control whether to absorb pointer events |
Stroke Properties #
class Stroke {
PointerDeviceKind deviceKind; // Input device type
List<Offset> points; // Stroke points
Color color; // Stroke color
double width; // Stroke width
ErasingBehavior erasingBehavior; // Erasing mode
}
ErasingBehavior #
enum ErasingBehavior {
none, // Normal drawing (default)
pixel, // Pixel-level erasing (BlendMode.clear)
stroke, // Stroke-level erasing (removes entire strokes)
}
Smoothing Modes #
Smoothing algorithm is also customizable. You can choose pre-defined functions below or make your own function.
SmoothingMode.catmullRom.converter // Smooth curves (default)
SmoothingMode.none.converter // No smoothing (straight lines)
Custom Stroke Painting with strokePainter #
The strokePainter callback allows you to fully customize how strokes are rendered. You can create advanced visual effects like gradients, glows, shadows, and shader effects by returning multiple Paint objects.
Draw(
strokePainter: (Stroke stroke) {
final List<Paint> paint = _buildYourPaint(stroke);
return paint;
},
)
Parameters:
stroke- The stroke to be painted
Return:
- A list of
Paintobjects to be applied to the stroke (in order)
Example: Gradient Effect #
LayoutBuilder(
builder: (context, constraints) {
final canvasSize = Size(constraints.maxWidth, constraints.maxHeight);
return Draw(
strokes: _strokes,
onStrokeDrawn: (stroke) => setState(() => _strokes.add(stroke)),
strokePainter: (stroke) {
final gradient = ui.Gradient.linear(
Offset.zero,
Offset(canvasSize.width, canvasSize.height),
[Colors.blue, Colors.purple, Colors.pink],
);
return [
Paint()
..shader = gradient
..strokeWidth = stroke.width
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
];
},
);
},
)
Example: Multi-Layer Effect (Glow) #
strokePainter: (stroke) {
return [
// Outer glow
paintWithOverride(stroke, strokeWidth: stroke.width + 8, strokeColor: Colors.cyan)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8),
// Inner glow
paintWithOverride(stroke, strokeWidth: stroke.width + 4, strokeColor: Colors.blue)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4),
// Core stroke
paintWithDefault(stroke),
];
}
Helper Functions #
The package provides utility functions for creating Paint objects:
paintWithDefault- Creates a paint with default propertiespaintWithOverride- Creates a paint with overridden properties
Using with InteractiveViewer #
The shouldAbsorb callback allows you to control which pointer events are handled by the Draw widget versus parent widgets like InteractiveViewer. This enables powerful combinations like:
- Touch for pan/zoom, stylus for drawing
- Device-specific gesture handling
- Conditional pointer event absorption
InteractiveViewer(
child: Draw(
strokes: _strokes,
shouldAbsorb: (event) {
// Absorb stylus events for drawing, let touch events pass through for pan/zoom
return event.kind == PointerDeviceKind.stylus ||
event.kind == PointerDeviceKind.invertedStylus;
},
onStrokeDrawn: (stroke) => setState(() => _strokes.add(stroke)),
onStrokeStarted: (newStroke, currentStroke) {
if (currentStroke != null) return currentStroke;
// Only draw with stylus (touch is for pan/zoom)
return newStroke.deviceKind == PointerDeviceKind.stylus ? newStroke : null;
},
),
)
See the example app for a complete implementation of touch-for-pan/stylus-for-draw functionality.
Working with AI Assistants #
This package is designed to work well with AI coding assistants. If you want accurate response from AI agents, refer AI_GUIDE.md before asking.