Skip to main content

zp_cccd_scanner 1.0.0 copy "zp_cccd_scanner: ^1.0.0" to clipboard
zp_cccd_scanner: ^1.0.0 copied to clipboard

Flutter plugin quét và nhận diện thẻ CCCD / Căn cước Việt Nam (đủ 3 mẫu thẻ, 2 mặt) bằng camera native trên iOS (AVFoundation + Vision) và Android (CameraX + ML Kit).

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:zp_cccd_scanner/zp_cccd_scanner.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  runApp(
    MaterialApp(
      title: 'CCCD Scanner',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0068FF)),
      ),
      home: const HomePage(),
    ),
  );
}

String typeLabel(CccdCardType t) {
  switch (t) {
    case CccdCardType.cccdBarcode:
      return 'CCCD mã vạch (2016)';
    case CccdCardType.cccdChip:
      return 'CCCD gắn chip (2021)';
    case CccdCardType.canCuoc:
      return 'Thẻ Căn cước (2024)';
  }
}

String sideLabel(CccdSide s) => s == CccdSide.front ? 'Mặt trước' : 'Mặt sau';

String pairLabel(CccdPairStatus s) {
  switch (s) {
    case CccdPairStatus.match:
      return 'Hai mặt cùng một thẻ';
    case CccdPairStatus.sameTypeOnly:
      return 'Cùng mẫu thẻ (mặt sau không in số để đối chiếu)';
    case CccdPairStatus.invalidSides:
      return 'Sai thứ tự mặt thẻ';
    case CccdPairStatus.mismatchType:
      return 'Hai mặt khác mẫu thẻ';
    case CccdPairStatus.mismatchId:
      return 'Hai mặt không cùng một thẻ';
  }
}

/// Kết quả một lần quét kèm thời gian từ lúc mở camera đến khi nhận diện xong
class ScanOutcome {
  const ScanOutcome(this.result, this.elapsed);
  final CccdScanResult result;
  final Duration elapsed;
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool _debug = false;

  Future<ScanOutcome?> _scan(CccdSide side) =>
      Navigator.of(context).push<ScanOutcome>(
        MaterialPageRoute(builder: (_) => ScanPage(side: side, debug: _debug)),
      );

  Future<void> _scanSingle(CccdSide side) async {
    final outcome = await _scan(side);
    if (outcome == null || !mounted) return;
    await Navigator.of(
      context,
    ).push(MaterialPageRoute(builder: (_) => ResultPage(outcomes: [outcome])));
  }

  Future<void> _scanEkyc() async {
    final front = await _scan(CccdSide.front);
    if (front == null || !mounted) return;
    final back = await _scan(CccdSide.back);
    if (back == null || !mounted) return;
    await Navigator.of(context).push(
      MaterialPageRoute(
        builder:
            (_) => ResultPage(
              outcomes: [front, back],
              pairStatus: CccdPairing.check(front.result, back.result),
            ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('CCCD Scanner')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _ActionTile(
            icon: Icons.badge_outlined,
            title: 'Quét mặt trước',
            subtitle: 'Chỉ nhận mặt trước, đưa mặt sau sẽ báo lật thẻ',
            onTap: () => _scanSingle(CccdSide.front),
          ),
          _ActionTile(
            icon: Icons.fingerprint,
            title: 'Quét mặt sau',
            subtitle: 'Chỉ nhận mặt sau, đưa mặt trước sẽ báo lật thẻ',
            onTap: () => _scanSingle(CccdSide.back),
          ),
          _ActionTile(
            icon: Icons.verified_user_outlined,
            title: 'Luồng eKYC',
            subtitle: 'Mặt trước → mặt sau → kiểm tra cùng một thẻ',
            onTap: _scanEkyc,
          ),
          const SizedBox(height: 8),
          SwitchListTile(
            value: _debug,
            onChanged: (v) => setState(() => _debug = v),
            title: const Text('Hiển thị vùng nhận diện (debug)'),
            subtitle: const Text('Vẽ khung các dòng chữ / mã native đọc được'),
          ),
        ],
      ),
    );
  }
}

class _ActionTile extends StatelessWidget {
  const _ActionTile({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.onTap,
  });

  final IconData icon;
  final String title;
  final String subtitle;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: Icon(icon, size: 32),
        title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600)),
        subtitle: Text(subtitle),
        trailing: const Icon(Icons.chevron_right),
        onTap: onTap,
      ),
    );
  }
}

class ScanPage extends StatefulWidget {
  const ScanPage({super.key, required this.side, this.debug = false});

  final CccdSide side;
  final bool debug;

  @override
  State<ScanPage> createState() => _ScanPageState();
}

class _ScanPageState extends State<ScanPage> {
  final _watch = Stopwatch()..start();
  bool _torch = false;

  void _toggleTorch() {
    setState(() => _torch = !_torch);
    ZpCccdScanner.setTorch(_torch);
  }

  Future<void> _onScanned(CccdScanResult r) async {
    Navigator.of(context).pop(ScanOutcome(r, _watch.elapsed));
  }

  @override
  Widget build(BuildContext context) {
    return AnnotatedRegion<SystemUiOverlayStyle>(
      value: SystemUiOverlayStyle.light,
      child: Scaffold(
        backgroundColor: Colors.black,
        body: CccdScannerView(
          side: widget.side,
          onScanned: _onScanned,
          showDebugOverlay: widget.debug,
          overlay: SafeArea(
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
              child: Row(
                children: [
                  _CircleButton(
                    icon: Icons.arrow_back_ios_new_rounded,
                    onTap: () => Navigator.of(context).pop(),
                  ),
                  Expanded(
                    child: Text(
                      'Chụp ${sideLabel(widget.side).toLowerCase()} CCCD',
                      textAlign: TextAlign.center,
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 17,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                  ),
                  _CircleButton(
                    icon:
                        _torch
                            ? Icons.flash_on_rounded
                            : Icons.flash_off_rounded,
                    onTap: _toggleTorch,
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _CircleButton extends StatelessWidget {
  const _CircleButton({required this.icon, required this.onTap});

  final IconData icon;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: onTap,
      customBorder: const CircleBorder(),
      child: Container(
        width: 40,
        height: 40,
        decoration: const BoxDecoration(
          color: Color(0x73000000),
          shape: BoxShape.circle,
        ),
        child: Icon(icon, color: Colors.white, size: 20),
      ),
    );
  }
}

class ResultPage extends StatelessWidget {
  const ResultPage({super.key, required this.outcomes, this.pairStatus});

  final List<ScanOutcome> outcomes;
  final CccdPairStatus? pairStatus;

  @override
  Widget build(BuildContext context) {
    final status = pairStatus;
    return Scaffold(
      appBar: AppBar(title: const Text('Kết quả')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          if (status != null)
            Card(
              color:
                  status == CccdPairStatus.match ||
                          status == CccdPairStatus.sameTypeOnly
                      ? Colors.green.shade50
                      : Colors.red.shade50,
              child: ListTile(
                leading: Icon(
                  status == CccdPairStatus.match ||
                          status == CccdPairStatus.sameTypeOnly
                      ? Icons.check_circle
                      : Icons.error,
                ),
                title: Text(pairLabel(status)),
              ),
            ),
          for (final o in outcomes) _OutcomeCard(outcome: o),
        ],
      ),
    );
  }
}

class _OutcomeCard extends StatelessWidget {
  const _OutcomeCard({required this.outcome});

  final ScanOutcome outcome;

  @override
  Widget build(BuildContext context) {
    final r = outcome.result;
    final image = r.image;
    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          if (image != null) Image.memory(image, fit: BoxFit.cover),
          ListTile(
            title: Text('${sideLabel(r.side)} · ${typeLabel(r.type)}'),
            subtitle: Text(
              'Thời gian nhận diện: '
              '${(outcome.elapsed.inMilliseconds / 1000).toStringAsFixed(1)} s',
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
0
points
792
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter plugin quét và nhận diện thẻ CCCD / Căn cước Việt Nam (đủ 3 mẫu thẻ, 2 mặt) bằng camera native trên iOS (AVFoundation + Vision) và Android (CameraX + ML Kit).

Repository (GitHub)
View/report issues

Topics

#cccd #id-card #ekyc #scanner #camera

License

unknown (license)

Dependencies

flutter

More

Packages that depend on zp_cccd_scanner

Packages that implement zp_cccd_scanner