zp_qr_scanner 1.1.2
zp_qr_scanner: ^1.1.2 copied to clipboard
A high-performance Flutter QR scanner plugin powered by native camera textures on iOS (AVFoundation) and Android (CameraX + ML Kit).
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:zp_qr_scanner/zp_qr_scanner.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
statusBarBrightness: Brightness.dark,
),
);
runApp(
MaterialApp(
title: 'QR Scanner',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0068FF)),
),
home: const ScanPage(),
),
);
}
enum _FramePosition {
upper('Cao', Alignment(0, -0.2)),
center('Giữa', Alignment.center),
lower('Thấp', Alignment(0, 0.3));
const _FramePosition(this.label, this.alignment);
final String label;
final Alignment alignment;
}
class ScanPage extends StatefulWidget {
const ScanPage({super.key});
@override
State<ScanPage> createState() => _ScanPageState();
}
class _ScanPageState extends State<ScanPage> {
bool _isTorchOn = false;
_FramePosition _framePosition = _FramePosition.upper;
void _toggleTorch() {
setState(() => _isTorchOn = !_isTorchOn);
ZpQrScanner.setTorch(_isTorchOn);
}
void _cycleFramePosition() {
final values = _FramePosition.values;
setState(() {
_framePosition = values[(_framePosition.index + 1) % values.length];
});
}
Future<void> _handleScanned(String value) async {
// Hiển thị BottomSheet kết quả giống Zalo
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => _ScanResultSheet(value: value),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// Lớp Scanner Camera với 4 góc nhấp nhô & tự đổi sang màu vàng bám góc QR
QrScannerView(
onScanned: _handleScanned,
scanBoxSize: 260.0,
idleColor: Colors.white,
detectedColor: const Color(0xFFFFD200),
cornerLength: 32.0,
cornerRadius: 8.0,
strokeWidth: 4.5,
scanBoxAlignment: _framePosition.alignment,
),
// Lớp UI Overlay
SafeArea(
child: Column(
children: [
// Top Bar
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Nút đóng (X)
_CircleIconButton(
icon: Icons.close_rounded,
onTap: () {
// Thao tác đóng hoặc thông báo
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đóng quét mã QR'),
duration: Duration(milliseconds: 800),
),
);
},
),
// Pill "Mã QR của tôi"
Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.45),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
width: 0.8,
),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.qr_code_2_rounded,
color: Colors.white,
size: 20,
),
SizedBox(width: 8),
Text(
'Mã QR của tôi',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
),
// Nút Bật / Tắt Flash
_CircleIconButton(
icon:
_isTorchOn
? Icons.flash_on_rounded
: Icons.flash_off_rounded,
color:
_isTorchOn ? const Color(0xFFFFD200) : Colors.white,
onTap: _toggleTorch,
),
],
),
),
const SizedBox(height: 12),
// Subtitle hướng dẫn
Column(
children: [
const Text(
'Quét mọi mã QR',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
shadows: [
Shadow(
blurRadius: 4,
color: Colors.black54,
offset: Offset(0, 1),
),
],
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_BrandTag(
child: RichText(
text: const TextSpan(
children: [
TextSpan(
text: 'V',
style: TextStyle(
color: Colors.redAccent,
fontWeight: FontWeight.w900,
fontSize: 13,
),
),
TextSpan(
text: 'IETQR',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
),
const SizedBox(width: 14),
const Row(
children: [
Icon(
Icons.language_rounded,
color: Colors.white70,
size: 16,
),
SizedBox(width: 4),
Text(
'website',
style: TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(width: 14),
const Text(
'Zalo',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
letterSpacing: 0.3,
),
),
],
),
],
),
const SizedBox(height: 12),
// Đổi vị trí khung quét (demo scanBoxAlignment)
GestureDetector(
onTap: _cycleFramePosition,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.45),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Vị trí khung: ${_framePosition.label}',
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
),
),
const Spacer(),
// Bottom Navigation / Action items
Padding(
padding: const EdgeInsets.only(
bottom: 24,
left: 24,
right: 24,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Nút 1: Ảnh có sẵn
_BottomActionItem(
iconWidget: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.2),
border: Border.all(
color: Colors.white60,
width: 1.5,
),
),
child: const Icon(
Icons.image_outlined,
color: Colors.white,
size: 24,
),
),
label: 'Ảnh có sẵn',
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Tính năng chọn ảnh từ thư viện'),
duration: Duration(milliseconds: 1000),
),
);
},
),
// Nút 2: QR chuyển khoản (ở giữa)
_BottomActionItem(
iconWidget: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black.withValues(alpha: 0.4),
border: Border.all(
color: const Color(0xFFFF4D4F),
width: 2,
),
),
child: const Icon(
Icons.qr_code_scanner_rounded,
color: Color(0xFFFF4D4F),
size: 26,
),
),
label: 'QR chuyển khoản',
onTap: () {},
),
// Nút 3: Gần đây
_BottomActionItem(
iconWidget: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black.withValues(alpha: 0.4),
border: Border.all(
color: Colors.white38,
width: 1.2,
),
),
child: const Icon(
Icons.history_rounded,
color: Colors.white,
size: 24,
),
),
label: 'Gần đây',
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Xem lịch sử quét gần đây'),
duration: Duration(milliseconds: 1000),
),
);
},
),
],
),
),
],
),
),
],
),
);
}
}
class _CircleIconButton extends StatelessWidget {
const _CircleIconButton({
required this.icon,
required this.onTap,
this.color = Colors.white,
});
final IconData icon;
final VoidCallback onTap;
final Color color;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(22),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.45),
shape: BoxShape.circle,
),
child: Icon(icon, color: color, size: 22),
),
);
}
}
class _BrandTag extends StatelessWidget {
const _BrandTag({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4),
),
child: child,
);
}
}
class _BottomActionItem extends StatelessWidget {
const _BottomActionItem({
required this.iconWidget,
required this.label,
required this.onTap,
});
final Widget iconWidget;
final String label;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
iconWidget,
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
shadows: [Shadow(blurRadius: 4, color: Colors.black54)],
),
),
],
),
),
);
}
}
class _ScanResultSheet extends StatelessWidget {
const _ScanResultSheet({required this.value});
final String value;
bool get _isUrl {
final uri = Uri.tryParse(value);
return uri != null && (uri.scheme == 'http' || uri.scheme == 'https');
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Drag handle bar
Center(
child: Container(
width: 38,
height: 4.5,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(3),
),
),
),
const SizedBox(height: 18),
// Khối card xám chứa kết quả
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF4F6F9),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_isUrl ? 'Trang web' : 'Văn bản',
style: const TextStyle(
color: Color(0xFF0068FF),
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8),
SelectableText(
value,
style: const TextStyle(
color: Colors.black87,
fontSize: 16,
height: 1.4,
),
),
],
),
),
const SizedBox(height: 20),
// Hàng nút chức năng (Sao chép + Tuỳ chọn)
Row(
children: [
Expanded(
child: SizedBox(
height: 48,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0068FF),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
icon: const Icon(Icons.copy_rounded, size: 20),
label: const Text(
'Sao chép',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
onPressed: () {
Clipboard.setData(ClipboardData(text: value));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã sao chép vào bộ nhớ tạm'),
duration: Duration(milliseconds: 1200),
),
);
},
),
),
),
const SizedBox(width: 12),
// Nút ba chấm
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFF0F2F5),
borderRadius: BorderRadius.circular(24),
),
child: IconButton(
icon: const Icon(
Icons.more_horiz_rounded,
color: Colors.black87,
),
onPressed: () {
Clipboard.setData(ClipboardData(text: value));
Navigator.pop(context);
},
),
),
],
),
],
),
);
}
}