guideline_cam 0.0.1 copy "guideline_cam: ^0.0.1" to clipboard
guideline_cam: ^0.0.1 copied to clipboard

A lightweight Flutter package that helps capture IDs, documents, or faces with a guided camera overlay.

example/lib/main.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:guideline_cam/guideline_cam.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const GuidelineCamDemoApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const DemoHomePage(),
    );
  }
}

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

  @override
  State<DemoHomePage> createState() => _DemoHomePageState();
}

class _DemoHomePageState extends State<DemoHomePage>
    with SingleTickerProviderStateMixin {
  late final TabController _tabController;
  late GuidelineCamController _controller;
  Color _maskColor = Colors.black54;
  Color _frameColor = Colors.white;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 5, vsync: this);
    _controller = GuidelineCamController();
    _controller.initialize();
    _tabController.addListener(() {
      if (mounted) setState(() {});
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    _tabController.dispose();
    super.dispose();
  }

  Future<void> _capture() async {
    try {
      final result = await _controller.capture();
      if (result != null) {
        if (mounted) {
          await _showCaptureDialog(
            file: result,
            capturedAt: DateTime.now(),
            lens: _controller.lensDirection,
          );
        }
      }
    } catch (e, st) {
      // You can also provide onError to GuidelineCamBuilder
      debugPrint('Capture error: $e\n$st');
    }
  }

  Future<void> _showCaptureDialog({
    required XFile file,
    required DateTime capturedAt,
    required CameraLensDirection lens,
  }) async {
    if (!mounted) return;
    return showDialog<void>(
      context: context,
      barrierDismissible: true,
      builder: (context) {
        return AlertDialog(
          shape:
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
          contentPadding: const EdgeInsets.all(12),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              ClipRRect(
                borderRadius: BorderRadius.circular(12),
                child: SizedBox(
                  width: 300,
                  height: 200,
                  child: Image.file(
                    File(file.path),
                    fit: BoxFit.cover,
                    errorBuilder: (_, __, ___) => const SizedBox(
                      width: 300,
                      height: 200,
                      child: Center(child: Icon(Icons.image_not_supported)),
                    ),
                  ),
                ),
              ),
              const SizedBox(height: 12),
              Text(
                'Captured: ${capturedAt.toLocal()}',
                style: const TextStyle(fontWeight: FontWeight.w600),
              ),
              const SizedBox(height: 4),
              Text('Direction: ${lens.name}'),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).maybePop(),
              child: const Text('Close'),
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    final bool showFab =
        _tabController.index != 2; // Hide on Overlay Builder tab
    return Scaffold(
      appBar: AppBar(
        title: const Text('GuidelineCam Example'),
        bottom: TabBar(
          controller: _tabController,
          isScrollable: true,
          tabs: const [
            Tab(text: 'Basic'),
            Tab(text: 'Custom Buttons'),
            Tab(text: 'Overlay Builder'),
            Tab(text: 'Multi/Nested'),
            Tab(text: 'Instruction'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          _BasicDemo(
              controller: _controller,
              onCaptured: (x) async {
                if (x != null) {
                  await _showCaptureDialog(
                    file: x,
                    capturedAt: DateTime.now(),
                    lens: _controller.lensDirection,
                  );
                }
              },
              maskColor: _maskColor,
              frameColor: _frameColor),
          _CustomButtonsDemo(
              controller: _controller,
              onCaptured: (x) async {
                if (x != null) {
                  await _showCaptureDialog(
                    file: x,
                    capturedAt: DateTime.now(),
                    lens: _controller.lensDirection,
                  );
                }
              },
              maskColor: _maskColor,
              frameColor: _frameColor),
          _OverlayBuilderDemo(
              controller: _controller,
              onCaptured: (x) async {
                if (x != null) {
                  await _showCaptureDialog(
                    file: x,
                    capturedAt: DateTime.now(),
                    lens: _controller.lensDirection,
                  );
                }
              },
              maskColor: _maskColor,
              frameColor: _frameColor),
          _MultiNestedDemo(
              controller: _controller,
              onCaptured: (x) async {
                if (x != null) {
                  await _showCaptureDialog(
                    file: x,
                    capturedAt: DateTime.now(),
                    lens: _controller.lensDirection,
                  );
                }
              },
              maskColor: _maskColor),
          _InstructionDemo(
              controller: _controller,
              onCaptured: (x) async {
                if (x != null) {
                  await _showCaptureDialog(
                    file: x,
                    capturedAt: DateTime.now(),
                    lens: _controller.lensDirection,
                  );
                }
              },
              maskColor: _maskColor,
              frameColor: _frameColor),
        ],
      ),
      bottomNavigationBar: _CapturePreviewBar(
        maskColor: _maskColor,
        frameColor: _frameColor,
        onMaskChanged: (c) => setState(() => _maskColor = c),
        onFrameChanged: (c) => setState(() => _frameColor = c),
      ),
      floatingActionButton: showFab
          ? FloatingActionButton.extended(
              onPressed: _capture,
              icon: const Icon(Icons.camera_alt),
              label: const Text('Capture'),
            )
          : null,
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
    );
  }
}

class _CapturePreviewBar extends StatelessWidget {
  const _CapturePreviewBar({
    required this.maskColor,
    required this.frameColor,
    required this.onMaskChanged,
    required this.onFrameChanged,
  });

  final Color maskColor;
  final Color frameColor;
  final ValueChanged<Color> onMaskChanged;
  final ValueChanged<Color> onFrameChanged;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 84,
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      decoration: BoxDecoration(
        color: Colors.black.withValues(alpha: 0.05),
        border: const Border(top: BorderSide(color: Color(0x14000000))),
      ),
      child: Row(
        children: [
          _ColorChip(
            label: 'Mask',
            color: maskColor,
            onTap: () => _showPalette(context, maskColor, onMaskChanged,
                enableOpacity: true),
          ),
          const SizedBox(width: 12),
          _ColorChip(
            label: 'Frame',
            color: frameColor,
            onTap: () => _showPalette(context, frameColor, onFrameChanged),
          ),
          const SizedBox(width: 12),
          const Expanded(
            child: Text(
              'Adjust mask and frame colors in real-time.',
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ],
      ),
    );
  }

  void _showPalette(
      BuildContext context, Color current, ValueChanged<Color> onPick,
      {bool enableOpacity = false}) {
    final List<Color> palette = <Color>[
      Colors.black54,
      Colors.black45,
      Colors.white,
      Colors.teal,
      Colors.blueAccent,
      Colors.amber,
      Colors.redAccent,
      Colors.greenAccent,
      Colors.deepPurpleAccent,
      Colors.pinkAccent,
      Colors.orangeAccent,
      Colors.cyan,
    ];
    showModalBottomSheet<void>(
      context: context,
      builder: (context) {
        double opacity = enableOpacity ? current.a : 1.0;
        return StatefulBuilder(
          builder: (context, setModalState) {
            return SafeArea(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    if (enableOpacity) ...[
                      const Text('Mask Opacity'),
                      Row(
                        children: [
                          Expanded(
                            child: Slider(
                              value: opacity,
                              onChanged: (v) =>
                                  setModalState(() => opacity = v),
                              min: 0.0,
                              max: 0.9,
                              divisions: 9,
                              label: (opacity).toStringAsFixed(1),
                            ),
                          ),
                          Container(
                            padding: const EdgeInsets.symmetric(
                                horizontal: 8, vertical: 4),
                            decoration: BoxDecoration(
                              color: Colors.black12,
                              borderRadius: BorderRadius.circular(6),
                            ),
                            child:
                                Text('${(opacity * 100).toStringAsFixed(0)}%'),
                          ),
                        ],
                      ),
                      const SizedBox(height: 8),
                    ],
                    Wrap(
                      spacing: 12,
                      runSpacing: 12,
                      children: [
                        for (final c in palette)
                          GestureDetector(
                            onTap: () {
                              Navigator.of(context).pop();
                              onPick(enableOpacity
                                  ? c.withValues(alpha: opacity)
                                  : c);
                            },
                            child: Container(
                              width: 40,
                              height: 40,
                              decoration: BoxDecoration(
                                color: enableOpacity
                                    ? c.withValues(alpha: opacity)
                                    : c,
                                shape: BoxShape.circle,
                                border: Border.all(
                                  color: c.computeLuminance() > 0.5
                                      ? Colors.black26
                                      : Colors.white24,
                                ),
                              ),
                            ),
                          ),
                      ],
                    ),
                    const SizedBox(height: 8),
                  ],
                ),
              ),
            );
          },
        );
      },
    );
  }
}

class _ColorChip extends StatelessWidget {
  const _ColorChip(
      {required this.label, required this.color, required this.onTap});

  final String label;
  final Color color;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: onTap,
      borderRadius: BorderRadius.circular(12),
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: const [
            BoxShadow(
                color: Color(0x1A000000), blurRadius: 6, offset: Offset(0, 2)),
          ],
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(
              width: 18,
              height: 18,
              decoration: BoxDecoration(
                color: color,
                borderRadius: BorderRadius.circular(4),
                border: Border.all(color: Colors.black12),
              ),
            ),
            const SizedBox(width: 8),
            Text(label),
          ],
        ),
      ),
    );
  }
}

class _BasicDemo extends StatelessWidget {
  const _BasicDemo(
      {required this.controller,
      required this.onCaptured,
      required this.maskColor,
      required this.frameColor});

  final GuidelineCamController controller;
  final ValueChanged<XFile?> onCaptured;
  final Color maskColor;
  final Color frameColor;

  @override
  Widget build(BuildContext context) {
    return GuidelineCamBuilder(
      controller: controller,
      guideline: GuidelineOverlayConfig(
        shape: GuidelineShape.roundedRect,
        aspectRatio: 1.586,
        frameColor: frameColor,
        maskColor: maskColor,
        borderRadius: 40,
        cornerLength: 0,
      ),
      onCapture: (result) => onCaptured(result.file),
    );
  }
}

class _CustomButtonsDemo extends StatelessWidget {
  const _CustomButtonsDemo(
      {required this.controller,
      required this.onCaptured,
      required this.maskColor,
      required this.frameColor});

  final GuidelineCamController controller;
  final ValueChanged<XFile?> onCaptured;
  final Color maskColor;
  final Color frameColor;

  @override
  Widget build(BuildContext context) {
    return GuidelineCamBuilder(
      controller: controller,
      guideline: GuidelineOverlayConfig(
        shape: GuidelineShape.circle,
        frameColor: frameColor,
        maskColor: maskColor,
      ),
      flashButtonBuilder: (context, flashMode, onPressed) {
        return Container(
          decoration: BoxDecoration(
            color: flashMode == FlashMode.off ? Colors.red : Colors.green,
            borderRadius: BorderRadius.circular(20),
            boxShadow: const [
              BoxShadow(
                  color: Colors.black26, blurRadius: 8, offset: Offset(0, 2)),
            ],
          ),
          child: Material(
            color: Colors.transparent,
            child: InkWell(
              borderRadius: BorderRadius.circular(20),
              onTap: onPressed,
              child: Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Icon(
                      flashMode == FlashMode.off
                          ? Icons.flash_off
                          : Icons.flash_on,
                      color: Colors.white,
                    ),
                    const SizedBox(width: 6),
                    Text(
                      flashMode == FlashMode.off ? 'Flash OFF' : 'Flash ON',
                      style: const TextStyle(
                          color: Colors.white, fontWeight: FontWeight.w600),
                    ),
                  ],
                ),
              ),
            ),
          ),
        );
      },
      cameraSwitchButtonBuilder: (context, lensDirection, onPressed) {
        return FloatingActionButton(
          onPressed: onPressed,
          backgroundColor: Colors.teal,
          child: Icon(
            lensDirection == CameraLensDirection.back
                ? Icons.camera_front
                : Icons.camera_rear,
          ),
        );
      },
      onCapture: (result) => onCaptured(result.file),
    );
  }
}

class _OverlayBuilderDemo extends StatelessWidget {
  const _OverlayBuilderDemo(
      {required this.controller,
      required this.onCaptured,
      required this.maskColor,
      required this.frameColor});

  final GuidelineCamController controller;
  final ValueChanged<XFile?> onCaptured;
  final Color maskColor;
  final Color frameColor;

  @override
  Widget build(BuildContext context) {
    return GuidelineCamBuilder(
      controller: controller,
      guideline: GuidelineOverlayConfig(
        shape: GuidelineShape.oval,
        aspectRatio: 0.75,
        padding: const EdgeInsets.all(80),
        frameColor: frameColor,
        maskColor: maskColor,
      ),
      overlayBuilder: (context, c) {
        return Stack(
          children: [
            Positioned(
              top: 50,
              right: 20,
              child: FloatingActionButton(
                onPressed: () async {
                  final newMode = c.flashMode == FlashMode.off
                      ? FlashMode.always
                      : FlashMode.off;
                  await c.setFlashMode(newMode);
                },
                backgroundColor: c.flashMode == FlashMode.off
                    ? Colors.black54
                    : Colors.amber,
                child: Icon(
                  c.flashMode == FlashMode.off
                      ? Icons.flash_off
                      : Icons.flash_on,
                  color: Colors.white,
                ),
              ),
            ),
            Positioned(
              top: 50,
              left: 20,
              child: FloatingActionButton(
                onPressed: () async {
                  await c.switchCamera();
                },
                backgroundColor: Colors.black54,
                child: const Icon(Icons.switch_camera, color: Colors.white),
              ),
            ),
            Positioned(
              bottom: 24,
              left: 0,
              right: 0,
              child: Center(
                child: FloatingActionButton.large(
                  onPressed: () async {
                    final res = await c.capture();
                    onCaptured(res);
                  },
                  child: const Icon(Icons.camera_alt),
                ),
              ),
            ),
            Positioned(
              top: 12,
              left: 0,
              right: 0,
              child: Center(
                child: Container(
                  padding:
                      const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
                  decoration: BoxDecoration(
                    color: Colors.black54,
                    borderRadius: BorderRadius.circular(16),
                  ),
                  child: Text(
                    'State: ${c.state.name.toUpperCase()}',
                    style: const TextStyle(
                        color: Colors.white, fontWeight: FontWeight.w600),
                  ),
                ),
              ),
            ),
          ],
        );
      },
      onCapture: (result) => onCaptured(result.file),
    );
  }
}

class _MultiNestedDemo extends StatelessWidget {
  const _MultiNestedDemo(
      {required this.controller,
      required this.onCaptured,
      required this.maskColor});

  final GuidelineCamController controller;
  final ValueChanged<XFile?> onCaptured;
  final Color maskColor;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final width = constraints.maxWidth;
        final height = constraints.maxHeight;

        // Responsive rectangles/ovals using absolute bounds based on screen
        final faceOval = ShapeConfig(
          shape: GuidelineShape.oval,
          aspectRatio: 0.75,
          bounds: Rect.fromLTWH(
              width * 0.25, height * 0.10, width * 0.50, height * 0.4),
          frameColor: Colors.lightBlueAccent,
          strokeWidth: 3,
          cornerLength: 0,
        );
        final idCard = ShapeConfig(
          shape: GuidelineShape.roundedRect,
          bounds: Rect.fromLTWH(
              width * 0.15, height * 0.55, width * 0.70, height * 0.275),
          borderRadius: 16,
          frameColor: Colors.greenAccent,
          strokeWidth: 3,
          cornerLength: 0,
          children: [
            ShapeConfig.relativePosition(
              shape: GuidelineShape.rect,
              relativeOffset: const Offset(0.75, 0.5),
              size: const Size(0.3, 0.6),
              frameColor: Colors.white,
              strokeWidth: 2,
            ),
            ShapeConfig.inset(
              shape: GuidelineShape.roundedRect,
              cornerLength: 0,
              insets: const EdgeInsets.fromLTRB(16, 34, 16, 16),
              size: const Size(0.5, 0.2),
              frameColor: Colors.white,
              strokeWidth: 1.5,
            ),
          ],
        );

        return GuidelineCamBuilder(
          controller: controller,
          guideline: GuidelineOverlayConfig(
            shapes: [faceOval, idCard],
            maskColor: maskColor,
          ),
          onCapture: (result) => onCaptured(result.file),
        );
      },
    );
  }
}

class _InstructionDemo extends StatelessWidget {
  const _InstructionDemo(
      {required this.controller,
      required this.onCaptured,
      required this.maskColor,
      required this.frameColor});

  final GuidelineCamController controller;
  final ValueChanged<XFile?> onCaptured;
  final Color maskColor;
  final Color frameColor;

  @override
  Widget build(BuildContext context) {
    return GuidelineCamBuilder(
      controller: controller,
      guideline: GuidelineOverlayConfig(
        shape: GuidelineShape.roundedRect,
        aspectRatio: 1.586,
        frameColor: frameColor,
        maskColor: maskColor,
        showGrid: true,
        debugPaint: true,
      ),
      instructionBuilder: (context, state) {
        String message;
        Color color;
        switch (state) {
          case GuidelineState.initializing:
            message = 'Initializing camera...';
            color = Colors.orange;
            break;
          case GuidelineState.ready:
            message = 'Align the document within the frame.';
            color = Colors.green;
            break;
          case GuidelineState.capturing:
            message = 'Capturing... Hold steady!';
            color = Colors.blue;
            break;
          case GuidelineState.error:
            message = 'An error occurred. Please retry.';
            color = Colors.red;
            break;
        }
        return Center(
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
            decoration: BoxDecoration(
              color: Colors.black.withValues(alpha: 0.6),
              borderRadius: BorderRadius.circular(16),
              border: Border.all(color: color, width: 1.5),
              boxShadow: const [
                BoxShadow(
                    color: Colors.black38, blurRadius: 8, offset: Offset(0, 2)),
              ],
            ),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Container(
                  width: 10,
                  height: 10,
                  decoration:
                      BoxDecoration(color: color, shape: BoxShape.circle),
                ),
                const SizedBox(width: 8),
                Text(
                  message,
                  style: const TextStyle(
                      color: Colors.white, fontWeight: FontWeight.w600),
                ),
              ],
            ),
          ),
        );
      },
      onCapture: (result) => onCaptured(result.file),
    );
  }
}
6
likes
0
points
9
downloads

Publisher

verified publisherlogique.co.id

Weekly Downloads

A lightweight Flutter package that helps capture IDs, documents, or faces with a guided camera overlay.

Repository (GitHub)
View/report issues

Topics

#camera #guideline #overlay #capture #document

Documentation

Documentation

License

unknown (license)

Dependencies

camera, flutter

More

Packages that depend on guideline_cam