gesture_recorder 0.1.1 copy "gesture_recorder: ^0.1.1" to clipboard
gesture_recorder: ^0.1.1 copied to clipboard

gesture_recorder enables you to record and replay gesture events

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:gesture_recorder/gesture_recorder.dart';
import 'package:scribble/scribble.dart';

void main() {
  runApp(GestureRecorder(child: const MainApp()));
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Drawing Canvas',
      theme: ThemeData.from(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
      ),
      home: const DrawingCanvasPage(),
    );
  }
}

class DrawingCanvasPage extends StatefulWidget {
  const DrawingCanvasPage({super.key});

  @override
  State<DrawingCanvasPage> createState() => _DrawingCanvasPageState();
}

class _DrawingCanvasPageState extends State<DrawingCanvasPage> {
  late ScribbleNotifier notifier;
  List<CapturedPointerData>? _pointerHistory;

  @override
  void initState() {
    super.initState();
    notifier = ScribbleNotifier();
  }

  @override
  void dispose() {
    notifier.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).colorScheme.surface,
      appBar: AppBar(
        title: const Text('Drawing Canvas'),
        actions: [
          ValueListenableBuilder(
            valueListenable: notifier,
            builder: (context, value, child) => IconButton(
              icon: child as Icon,
              tooltip: "Undo",
              onPressed: notifier.canUndo ? notifier.undo : null,
            ),
            child: const Icon(Icons.undo),
          ),
          ValueListenableBuilder(
            valueListenable: notifier,
            builder: (context, value, child) => IconButton(
              icon: child as Icon,
              tooltip: "Redo",
              onPressed: notifier.canRedo ? notifier.redo : null,
            ),
            child: const Icon(Icons.redo),
          ),
          IconButton(
            icon: const Icon(Icons.clear),
            tooltip: "Clear",
            onPressed: notifier.clear,
          ),
        ],
      ),
      body: LayoutBuilder(
        builder: (context, constraints) {
          // Optimize for iPad landscape (wider screen)
          final isLandscape = constraints.maxWidth > constraints.maxHeight;

          if (isLandscape) {
            // Landscape layout: toolbar on left, canvas on right
            return Row(
              children: [
                // Toolbar panel
                Container(
                  width: 200,
                  decoration: BoxDecoration(
                    color: Theme.of(
                      context,
                    ).colorScheme.surfaceContainerHighest,
                    border: Border(
                      right: BorderSide(
                        color: Theme.of(context).dividerColor,
                        width: 1,
                      ),
                    ),
                  ),
                  child: SingleChildScrollView(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Text(
                          'Colors',
                          style: TextStyle(
                            fontSize: 16,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(height: 12),
                        _buildColorToolbar(),
                        const SizedBox(height: 24),
                        const Text(
                          'Stroke Width',
                          style: TextStyle(
                            fontSize: 16,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(height: 12),
                        _buildStrokeToolbar(),
                      ],
                    ),
                  ),
                ),
                // Canvas area
                Expanded(
                  child: Container(
                    margin: const EdgeInsets.all(24),
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(8),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withValues(alpha: 0.1),
                          blurRadius: 10,
                          offset: const Offset(0, 4),
                        ),
                      ],
                    ),
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(8),
                      child: Scribble(notifier: notifier, drawPen: true),
                    ),
                  ),
                ),
              ],
            );
          } else {
            // Portrait layout: toolbar on top, canvas below
            return Column(
              children: [
                Container(
                  constraints: const BoxConstraints(maxHeight: 200),
                  decoration: BoxDecoration(
                    color: Theme.of(
                      context,
                    ).colorScheme.surfaceContainerHighest,
                    border: Border(
                      bottom: BorderSide(
                        color: Theme.of(context).dividerColor,
                        width: 1,
                      ),
                    ),
                  ),
                  child: SingleChildScrollView(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        _buildColorToolbar(),
                        const SizedBox(height: 12),
                        _buildStrokeToolbar(),
                      ],
                    ),
                  ),
                ),
                Expanded(
                  child: Container(
                    margin: const EdgeInsets.all(16),
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(8),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withValues(alpha: 0.1),
                          blurRadius: 10,
                          offset: const Offset(0, 4),
                        ),
                      ],
                    ),
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(8),
                      child: Scribble(notifier: notifier, drawPen: true),
                    ),
                  ),
                ),
              ],
            );
          }
        },
      ),
      floatingActionButton: _buildFloatingActionButton(),
    );
  }

  IconData _getRecordIcon(RecordState recordState) {
    switch (recordState) {
      case RecordState.none:
        return _pointerHistory != null
            ? Icons.play_arrow
            : Icons.fiber_manual_record;
      case RecordState.recording:
        return Icons.stop;
      case RecordState.playing:
        return Icons.play_arrow;
    }
  }

  Color _getRecordColor(RecordState recordState) {
    switch (recordState) {
      case RecordState.none:
        return _pointerHistory != null ? Colors.green : Colors.red;
      case RecordState.recording:
        return Colors.grey;
      case RecordState.playing:
        return Colors.blue;
    }
  }

  Future<void> _toggleRecordState(RecordState recordState) async {
    switch (recordState) {
      case RecordState.none:
        if (_pointerHistory != null) {
          await GestureRecorder.replay(context, _pointerHistory!);
        } else {
          GestureRecorder.start(context);
        }
      case RecordState.recording:
        final history = await GestureRecorder.stop(context);
        setState(() {
          _pointerHistory = history;
        });
      case RecordState.playing:
        // do nothing
        break;
    }
  }

  Widget _buildFloatingActionButton() {
    final recordState = GestureRecorder.stateOf(context);
    return FloatingActionButton(
      onPressed: recordState == RecordState.playing
          ? null
          : () => _toggleRecordState(recordState),
      backgroundColor: _getRecordColor(recordState),
      child: Icon(_getRecordIcon(recordState), color: Colors.white),
    );
  }

  // Rich color palette organized by categories
  static final List<Color> _primaryColors = [
    Colors.black,
    Colors.white,
    Colors.grey.shade700,
    Colors.grey.shade400,
  ];

  static final List<Color> _warmColors = [
    Colors.red,
    Colors.redAccent,
    Colors.deepOrange,
    Colors.orange,
    Colors.orangeAccent,
    Colors.amber,
    Colors.yellow,
    Colors.pink,
    Colors.pinkAccent,
  ];

  static final List<Color> _coolColors = [
    Colors.blue,
    Colors.blueAccent,
    Colors.lightBlue,
    Colors.cyan,
    Colors.teal,
    Colors.green,
    Colors.greenAccent,
    Colors.lightGreen,
    Colors.lime,
  ];

  static final List<Color> _purpleColors = [
    Colors.purple,
    Colors.purpleAccent,
    Colors.deepPurple,
    Colors.indigo,
    Colors.indigoAccent,
  ];

  static final List<Color> _brownColors = [
    Colors.brown,
    Colors.brown.shade300,
    const Color(0xFF8B4513), // SaddleBrown
    const Color(0xFFD2691E), // Chocolate
    const Color(0xFFCD853F), // Peru
  ];

  Widget _buildColorToolbar() {
    return ValueListenableBuilder<ScribbleState>(
      valueListenable: notifier,
      builder: (context, state, _) {
        final selectedColor = state.map(
          drawing: (s) => Color(s.selectedColor),
          erasing: (_) => null,
        );

        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Primary colors (black, white, grays)
            _buildColorGroup('Primary', _primaryColors, selectedColor, state),
            const SizedBox(height: 12),
            // Warm colors
            _buildColorGroup('Warm', _warmColors, selectedColor, state),
            const SizedBox(height: 12),
            // Cool colors
            _buildColorGroup('Cool', _coolColors, selectedColor, state),
            const SizedBox(height: 12),
            // Purple/Indigo colors
            _buildColorGroup('Purple', _purpleColors, selectedColor, state),
            const SizedBox(height: 12),
            // Brown/Earth tones
            _buildColorGroup('Earth', _brownColors, selectedColor, state),
            const SizedBox(height: 12),
            // Eraser
            _buildEraserButton(),
          ],
        );
      },
    );
  }

  Widget _buildColorGroup(
    String label,
    List<Color> colors,
    Color? selectedColor,
    ScribbleState state,
  ) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.only(bottom: 6),
          child: Text(
            label,
            style: TextStyle(
              fontSize: 12,
              fontWeight: FontWeight.w600,
              color: Theme.of(context)
                  .colorScheme
                  .onSurface
                  .withValues(alpha: 0.7),
            ),
          ),
        ),
        Wrap(
          spacing: 6,
          runSpacing: 6,
          children: colors.map((color) {
            final isActive = state.map(
              drawing: (s) => s.selectedColor == color.toARGB32(),
              erasing: (_) => false,
            );
            return _buildColorButton(
              color: color,
              isActive: isActive,
              onPressed: () => notifier.setColor(color),
            );
          }).toList(),
        ),
      ],
    );
  }

  Widget _buildStrokeToolbar() {
    return ValueListenableBuilder<ScribbleState>(
      valueListenable: notifier,
      builder: (context, state, _) => Wrap(
        spacing: 8,
        runSpacing: 8,
        children: [
          for (final width in notifier.widths)
            _buildStrokeButton(strokeWidth: width, state: state),
        ],
      ),
    );
  }

  Widget _buildStrokeButton({
    required double strokeWidth,
    required ScribbleState state,
  }) {
    final selected = state.selectedWidth == strokeWidth;
    return Material(
      elevation: selected ? 4 : 0,
      shape: const CircleBorder(),
      child: InkWell(
        onTap: () => notifier.setStrokeWidth(strokeWidth),
        customBorder: const CircleBorder(),
        child: AnimatedContainer(
          duration: kThemeAnimationDuration,
          width: strokeWidth * 2 + 8,
          height: strokeWidth * 2 + 8,
          decoration: BoxDecoration(
            color: state.map(
              drawing: (s) => Color(s.selectedColor),
              erasing: (_) => Colors.transparent,
            ),
            border: state.map(
              drawing: (_) => null,
              erasing: (_) => Border.all(color: Colors.black, width: 1),
            ),
            shape: BoxShape.circle,
          ),
          child: Center(
            child: Container(
              width: strokeWidth * 2,
              height: strokeWidth * 2,
              decoration: BoxDecoration(
                color: state.map(
                  drawing: (_) => Colors.white,
                  erasing: (_) => Colors.black,
                ),
                shape: BoxShape.circle,
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildEraserButton() {
    return ValueListenableBuilder<ScribbleState>(
      valueListenable: notifier,
      builder: (context, state, _) {
        final isEraser = state is Erasing;
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.only(bottom: 6),
              child: Text(
                'Tools',
                style: TextStyle(
                  fontSize: 12,
                  fontWeight: FontWeight.w600,
                  color: Theme.of(
                    context,
                  ).colorScheme.onSurface.withValues(alpha: 0.7),
                ),
              ),
            ),
            Material(
              elevation: isEraser ? 6 : 2,
              shadowColor: isEraser
                  ? Colors.blue.withValues(alpha: 0.5)
                  : Colors.black.withValues(alpha: 0.2),
              shape: const CircleBorder(),
              child: InkWell(
                onTap: () => notifier.setEraser(),
                customBorder: const CircleBorder(),
                borderRadius: BorderRadius.circular(24),
                child: AnimatedContainer(
                  duration: kThemeAnimationDuration,
                  curve: Curves.easeInOut,
                  width: isEraser ? 52 : 44,
                  height: isEraser ? 52 : 44,
                  decoration: BoxDecoration(
                    color: Colors.white,
                    shape: BoxShape.circle,
                    border: Border.all(
                      color: isEraser
                          ? Theme.of(context).colorScheme.primary
                          : Colors.grey.shade300,
                      width: isEraser ? 3.5 : 1.5,
                    ),
                    boxShadow: isEraser
                        ? [
                            BoxShadow(
                              color: Colors.blue.withValues(alpha: 0.4),
                              blurRadius: 8,
                              spreadRadius: 2,
                              offset: const Offset(0, 2),
                            ),
                          ]
                        : [
                            BoxShadow(
                              color: Colors.black.withValues(alpha: 0.1),
                              blurRadius: 2,
                              offset: const Offset(0, 1),
                            ),
                          ],
                  ),
                  child: Icon(
                    Icons.cleaning_services,
                    color: isEraser
                        ? Theme.of(context).colorScheme.primary
                        : Colors.grey.shade600,
                    size: 24,
                  ),
                ),
              ),
            ),
          ],
        );
      },
    );
  }

  Widget _buildColorButton({
    required Color color,
    required bool isActive,
    required VoidCallback onPressed,
    Widget? child,
  }) {
    // Special handling for white color - needs a border to be visible
    final needsBorder = color == Colors.white || color == Colors.grey.shade400;

    return Material(
      elevation: isActive ? 6 : 2,
      shadowColor: isActive
          ? color.withValues(alpha: 0.5)
          : Colors.black.withValues(alpha: 0.2),
      shape: const CircleBorder(),
      child: InkWell(
        onTap: onPressed,
        customBorder: const CircleBorder(),
        borderRadius: BorderRadius.circular(24),
        child: AnimatedContainer(
          duration: kThemeAnimationDuration,
          curve: Curves.easeInOut,
          width: isActive ? 52 : 44,
          height: isActive ? 52 : 44,
          decoration: BoxDecoration(
            color: color,
            shape: BoxShape.circle,
            border: Border.all(
              color: isActive
                  ? Theme.of(context).colorScheme.primary
                  : (needsBorder ? Colors.grey.shade400 : Colors.grey.shade300),
              width: isActive ? 3.5 : (needsBorder ? 1.5 : 1),
            ),
            boxShadow: isActive
                ? [
                    BoxShadow(
                      color: color.withValues(alpha: 0.4),
                      blurRadius: 8,
                      spreadRadius: 2,
                      offset: const Offset(0, 2),
                    ),
                  ]
                : [
                    BoxShadow(
                      color: Colors.black.withValues(alpha: 0.1),
                      blurRadius: 2,
                      offset: const Offset(0, 1),
                    ),
                  ],
          ),
          child: child ??
              (isActive
                  ? Center(
                      child: Icon(
                        Icons.check,
                        size: 20,
                        color: _getContrastColor(color),
                      ),
                    )
                  : const SizedBox()),
        ),
      ),
    );
  }

  Color _getContrastColor(Color color) {
    // Calculate luminance to determine if we need light or dark text
    final luminance = color.computeLuminance();
    return luminance > 0.5 ? Colors.black : Colors.white;
  }
}
24
likes
0
points
306
downloads

Publisher

verified publishertsuyoshichujo.com

Weekly Downloads

gesture_recorder enables you to record and replay gesture events

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on gesture_recorder