draw_your_image 0.6.1 copy "draw_your_image: ^0.6.1" to clipboard
draw_your_image: ^0.6.1 copied to clipboard

A Flutter package which enables users to draw with fingers in your designed UI.

example/lib/main.dart

import 'dart:ui';
import 'package:draw_your_image/draw_your_image.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'draw_your_image Demo', home: MyHomePage());
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

/// Type definition for local state for undo/redo functionality
typedef StrokeState = List<Stroke>;

/// Stroke update mode for onStrokeUpdated callback
enum StrokeUpdateMode { none, trailing, rectangle, colorByLength }

class _MyHomePageState extends State<MyHomePage> {
  var _strokes = <Stroke>[];

  /// Undo and redo stacks.
  /// Undo/redo is implemented by saving the list of states.
  var _undoStack = <StrokeState>[];
  var _redoStack = <StrokeState>[];

  PointerDeviceKind? _currentDevice;
  PointerDeviceKind? _visibleDevice;
  ErasingBehavior _erasingBehavior = ErasingBehavior.none;
  StrokeUpdateMode _strokeUpdateMode = StrokeUpdateMode.none;

  bool get _canUndo => _undoStack.isNotEmpty;
  bool get _canRedo => _redoStack.isNotEmpty;
  bool get _isDrawing => _currentDevice != null;

  void _clear() {
    setState(() {
      _strokes = [];
      _undoStack = [];
      _redoStack = [];
    });
  }

  void _undo() {
    setState(() {
      /// save current state to redo stack
      _redoStack.add(List.from(_strokes));

      /// restore last state from undo stack
      _strokes = _undoStack.removeLast();
    });
  }

  void _redo() {
    setState(() {
      /// save current state to undo stack
      _undoStack.add(List.from(_strokes));

      /// restore last state from redo stack
      _strokes = _redoStack.removeLast();
    });
  }

  MaterialColor get _deviceColor =>
      _currentDevice == PointerDeviceKind.stylus ? Colors.blue : Colors.green;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('draw_your_image Demo')),
      body: Column(
        children: [
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(16),
            color: Colors.grey[100],
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Show Strokes From:',
                  style: TextStyle(
                    fontSize: 14,
                    fontWeight: FontWeight.bold,
                    color: Colors.grey[700],
                  ),
                ),
                SizedBox(height: 8),
                Row(
                  spacing: 16,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    FilterChip(
                      label: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Icon(Icons.edit, size: 32),
                          SizedBox(width: 8),
                          Text('Stylus', style: TextStyle(fontSize: 24)),
                        ],
                      ),
                      selected: _visibleDevice == PointerDeviceKind.stylus,
                      onSelected: (selected) {
                        setState(
                          () => _visibleDevice = selected
                              ? PointerDeviceKind.stylus
                              : null,
                        );
                      },
                    ),
                    FilterChip(
                      label: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Icon(Icons.touch_app, size: 32),
                          SizedBox(width: 8),
                          Text('Finger', style: TextStyle(fontSize: 24)),
                        ],
                      ),
                      selected: _visibleDevice == PointerDeviceKind.touch,
                      onSelected: (selected) {
                        setState(
                          () => _visibleDevice = selected
                              ? PointerDeviceKind.touch
                              : null,
                        );
                      },
                    ),
                    FilterChip(
                      label: Text('All', style: TextStyle(fontSize: 24)),
                      selected: _visibleDevice == null,
                      onSelected: (selected) {
                        setState(() => _visibleDevice = null);
                      },
                    ),
                  ],
                ),
                SizedBox(height: 16),
                Text(
                  'Stroke Update Mode:',
                  style: TextStyle(
                    fontSize: 14,
                    fontWeight: FontWeight.bold,
                    color: Colors.grey[700],
                  ),
                ),
                SizedBox(height: 8),
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  alignment: WrapAlignment.center,
                  children: [
                    FilterChip(
                      label: Text('None'),
                      selected: _strokeUpdateMode == StrokeUpdateMode.none,
                      onSelected: (selected) {
                        setState(
                          () => _strokeUpdateMode = StrokeUpdateMode.none,
                        );
                      },
                    ),
                    FilterChip(
                      label: Text('Trailing'),
                      selected: _strokeUpdateMode == StrokeUpdateMode.trailing,
                      onSelected: (selected) {
                        setState(
                          () => _strokeUpdateMode = StrokeUpdateMode.trailing,
                        );
                      },
                      selectedColor: Colors.purple[100],
                    ),
                    FilterChip(
                      label: Text('Rectangle'),
                      selected: _strokeUpdateMode == StrokeUpdateMode.rectangle,
                      onSelected: (selected) {
                        setState(
                          () => _strokeUpdateMode = StrokeUpdateMode.rectangle,
                        );
                      },
                      selectedColor: Colors.blue[100],
                    ),
                    FilterChip(
                      label: Text('Color by Length'),
                      selected:
                          _strokeUpdateMode == StrokeUpdateMode.colorByLength,
                      onSelected: (selected) {
                        setState(
                          () => _strokeUpdateMode =
                              StrokeUpdateMode.colorByLength,
                        );
                      },
                      selectedColor: Colors.green[100],
                    ),
                  ],
                ),
              ],
            ),
          ),
          // Drawing area
          Expanded(
            child: Container(
              margin: EdgeInsets.all(16),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.grey[300]!),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Stack(
                children: [
                  ClipRRect(
                    borderRadius: BorderRadius.circular(8),
                    child: InteractiveViewer(
                      scaleEnabled: !_isDrawing,
                      panEnabled: !_isDrawing,
                      child: Draw(
                        strokes: _strokes
                            .map(
                              (stroke) =>
                                  (stroke.deviceKind == _visibleDevice ||
                                      _visibleDevice == null)
                                  ? stroke
                                  : stroke.copyWith(color: Colors.grey[200]!),
                            )
                            .toList(),
                        strokeColor: Colors.black,
                        strokeWidth: 4.0,
                        backgroundColor: const Color.fromRGBO(255, 255, 255, 1),
                        erasingBehavior: _erasingBehavior,
                        smoothingFunc:
                            _strokeUpdateMode == StrokeUpdateMode.rectangle
                            ? SmoothingMode.none.converter
                            : null,
                        onStrokeDrawn: (stroke) {
                          setState(() => _currentDevice = null);
                          if (stroke.shouldPaint) {
                            final reducedStroke = stroke.copyWith(
                              points: stroke.points.reduced(epsilon: 0.5),
                            );
                            final before = stroke.points.length;
                            final after = reducedStroke.points.length;
                            debugPrint(
                              'Before: $before points, After: $after points',
                            );
                            setState(() {
                              _undoStack.add(List.from(_strokes));
                              _strokes = [..._strokes, reducedStroke];
                              _redoStack = [];
                            });
                          }
                        },
                        onStrokeStarted: (newStroke, currentStroke) {
                          if (currentStroke != null) {
                            return currentStroke;
                          }
                          setState(() => _currentDevice = newStroke.deviceKind);
                          return newStroke.copyWith(
                            color:
                                newStroke.deviceKind == PointerDeviceKind.stylus
                                ? Colors.blue
                                : Colors.blue,
                            width:
                                newStroke.deviceKind == PointerDeviceKind.stylus
                                ? 4.0
                                : 8.0,
                          );
                        },
                        onStrokesRemoved: (value) {
                          setState(() {
                            /// save current state to undo stack
                            _undoStack.add(List.from(_strokes));

                            /// clear redo stack
                            _redoStack = [];
                            _strokes = _strokes
                                .where((stroke) => !value.contains(stroke))
                                .toList();
                          });
                        },
                        onStrokeUpdated: (currentStroke) {
                          switch (_strokeUpdateMode) {
                            case StrokeUpdateMode.none:
                              return _noEffect(currentStroke);
                            case StrokeUpdateMode.trailing:
                              return _trailingEffect(currentStroke);
                            case StrokeUpdateMode.rectangle:
                              return _rectangleShape(currentStroke);
                            case StrokeUpdateMode.colorByLength:
                              return _colorByLength(currentStroke);
                          }
                        },
                      ),
                    ),
                  ),
                  if (_currentDevice != null)
                    Positioned(
                      top: 16,
                      left: 0,
                      right: 0,
                      child: Align(
                        alignment: Alignment.topCenter,
                        child: Container(
                          padding: EdgeInsets.symmetric(
                            horizontal: 12,
                            vertical: 6,
                          ),
                          decoration: BoxDecoration(
                            color: _deviceColor[50],
                            borderRadius: BorderRadius.circular(4),
                            border: Border.all(color: _deviceColor[200]!),
                          ),
                          child: Row(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              Icon(
                                _currentDevice == PointerDeviceKind.stylus
                                    ? Icons.edit
                                    : Icons.touch_app,
                                size: 32,
                                color: _deviceColor[700],
                              ),
                              SizedBox(width: 4),
                              Text(
                                'Drawing with ${_currentDevice == PointerDeviceKind.stylus ? "Stylus" : "Finger"}...',
                                style: TextStyle(
                                  fontSize: 24,
                                  color: _deviceColor[700],
                                  fontWeight: FontWeight.w500,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
                ],
              ),
            ),
          ),
          // Control buttons
          Container(
            width: double.infinity,
            padding: EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.white,
              border: Border(top: BorderSide(color: Colors.grey[300]!)),
            ),
            child: Column(
              children: [
                // Eraser mode toggle
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    Expanded(
                      child: FilterChip(
                        label: Text('Draw'),
                        selected: _erasingBehavior == ErasingBehavior.none,
                        onSelected: (_) {
                          setState(
                            () => _erasingBehavior = ErasingBehavior.none,
                          );
                        },
                      ),
                    ),
                    SizedBox(width: 8),
                    Expanded(
                      child: FilterChip(
                        label: Text('Erase Pixel'),
                        selected: _erasingBehavior == ErasingBehavior.pixel,
                        onSelected: (_) {
                          setState(
                            () => _erasingBehavior = ErasingBehavior.pixel,
                          );
                        },
                        selectedColor: Colors.orange[100],
                      ),
                    ),
                    SizedBox(width: 8),
                    Expanded(
                      child: FilterChip(
                        label: Text('Erase Stroke'),
                        selected: _erasingBehavior == ErasingBehavior.stroke,
                        onSelected: (_) {
                          setState(
                            () => _erasingBehavior = ErasingBehavior.stroke,
                          );
                        },
                        selectedColor: Colors.red[100],
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 8),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    Expanded(
                      child: OutlinedButton.icon(
                        icon: Icon(Icons.undo),
                        label: Text('Undo'),
                        onPressed: _canUndo ? _undo : null,
                      ),
                    ),
                    SizedBox(width: 8),
                    Expanded(
                      child: OutlinedButton.icon(
                        icon: Icon(Icons.redo),
                        label: Text('Redo'),
                        onPressed: _canRedo ? _redo : null,
                      ),
                    ),
                    SizedBox(width: 8),
                    Expanded(
                      child: ElevatedButton.icon(
                        icon: Icon(Icons.clear),
                        label: Text('Clear'),
                        onPressed: _strokes.isEmpty ? null : _clear,
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.red[50],
                          foregroundColor: Colors.red[700],
                        ),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  /// No effect - returns the stroke as-is
  Stroke? _noEffect(Stroke currentStroke) {
    return currentStroke;
  }

  /// Trailing effect - keeps only the last 30 points
  Stroke? _trailingEffect(Stroke currentStroke) {
    return currentStroke.copyWith(
      points: currentStroke.points.length > 30
          ? currentStroke.points.sublist(currentStroke.points.length - 30)
          : currentStroke.points,
    );
  }

  /// Rectangle shape - draws a rectangle using first and last points
  Stroke? _rectangleShape(Stroke currentStroke) {
    return currentStroke.points.length > 2
        ? currentStroke.copyWith(
            points: [
              currentStroke.points.first,
              Offset(
                currentStroke.points.first.dx,
                currentStroke.points.last.dy,
              ),
              currentStroke.points.last,
              Offset(
                currentStroke.points.last.dx,
                currentStroke.points.first.dy,
              ),
              currentStroke.points.first,
            ],
          )
        : currentStroke;
  }

  /// Color by length - changes color based on stroke length
  Stroke? _colorByLength(Stroke currentStroke) {
    final pointCount = currentStroke.points.length;
    final ratio = (pointCount / 5000).clamp(0.0, 1.0);
    final originalColor = currentStroke.color;

    final newColor = Color.lerp(originalColor, Colors.greenAccent, ratio);

    return currentStroke.copyWith(color: newColor);
  }
}
45
likes
160
points
410
downloads

Publisher

verified publishertsuyoshichujo.com

Weekly Downloads

A Flutter package which enables users to draw with fingers in your designed UI.

Repository (GitHub)
View/report issues

Documentation

API reference

License

Apache-2.0 (license)

Dependencies

flutter

More

Packages that depend on draw_your_image