plugin_scanner_qr 0.4.1
plugin_scanner_qr: ^0.4.1 copied to clipboard
Plugin Flutter para leitura de QR Code no Android usando ML Kit e CameraX. Oferece leitura única em tela cheia e leitura contínua com preview embutido na árvore de widgets (PlatformView), com debounce [...]
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:plugin_scanner_qr/plugin_scanner_qr.dart';
import 'embedded_preview_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// ── Estado geral ────────────────────────────────────────────────────────────
String _platformVersion = 'Carregando…';
String _qrCodeResult = 'Nenhum QR code escaneado ainda';
bool _isLoading = false;
// ── Opções de câmera (modo único e contínuo) ─────────────────────────────────
bool _useFrontCamera = false;
bool _enableTorch = false;
String _orientation = 'portrait';
bool _enableAutoFocus = true;
double _zoomLevel = 0.0;
bool _enableZoom = true;
double _brightness = 0.5;
double _contrast = 0.5;
double _exposureCompensation = 0.0;
final _plugin = PluginScannerQr();
@override
void initState() {
super.initState();
_initPlatformState();
}
Future<void> _initPlatformState() async {
String version;
try {
version = await _plugin.getPlatformVersion() ?? 'Versão desconhecida';
} on PlatformException {
version = 'Falha ao obter versão da plataforma';
}
if (!mounted) return;
setState(() => _platformVersion = version);
}
// ── Leitura única ────────────────────────────────────────────────────────────
Future<void> _scanQRCode() async {
setState(() => _isLoading = true);
try {
if (!await _plugin.isCameraAvailable()) {
_showSnackBar('Câmera não disponível no dispositivo');
return;
}
if (_useFrontCamera && !await _plugin.isFrontCameraAvailable()) {
_showSnackBar('Câmera frontal não disponível');
return;
}
if (!await _plugin.requestCameraPermission()) {
_showSnackBar('Permissão de câmera necessária');
return;
}
final result = await _plugin.scanQRCodeWithOptions(
useFrontCamera: _useFrontCamera,
orientation: _orientation,
enableTorch: _enableTorch,
enableAutoFocus: _enableAutoFocus,
zoomLevel: _zoomLevel,
enableZoom: _enableZoom,
brightness: _brightness,
contrast: _contrast,
exposureCompensation: _exposureCompensation,
);
setState(() {
_qrCodeResult =
result ?? 'Escaneamento cancelado pelo usuário';
});
} on PlatformException catch (e) {
setState(() => _qrCodeResult = 'Erro: ${e.message}');
} finally {
setState(() => _isLoading = false);
}
}
// ── Leitura contínua (preview embutido) ──────────────────────────────────────
/// Abre a tela com o preview embutido — a PlatformView.
///
/// Substitui o antigo modo contínuo em Activity de tela cheia, que cobria toda
/// a UI Flutter.
Future<void> _abrirPreviewEmbutido({bool hybrid = false}) async {
// Via key, e não Navigator.of(context): o context deste State é o do
// MaterialApp, ou seja, está ACIMA do Navigator.
await _navigatorKey.currentState?.push(
MaterialPageRoute<void>(
builder: (_) => EmbeddedPreviewPage(hybridComposition: hybrid),
),
);
}
void _showSnackBar(String message) {
_messengerKey.currentState?.showSnackBar(
SnackBar(content: Text(message)),
);
}
// ── UI ───────────────────────────────────────────────────────────────────────
final _navigatorKey = GlobalKey<NavigatorState>();
final _messengerKey = GlobalKey<ScaffoldMessengerState>();
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navigatorKey,
scaffoldMessengerKey: _messengerKey,
home: Scaffold(
appBar: AppBar(
title: const Text('Scanner QR Code'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Column(
children: [
const Icon(Icons.qr_code_scanner, size: 80, color: Colors.blue),
const SizedBox(height: 8),
Text(
'Plugin Scanner QR Code',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
Text(
'Plataforma: $_platformVersion',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
const SizedBox(height: 16),
// ── Configurações de câmera ──────────────────────────────────────
_SectionCard(
title: 'Configurações da Câmera',
children: [
SwitchListTile(
title: const Text('Câmera frontal'),
value: _useFrontCamera,
onChanged: (v) => setState(() => _useFrontCamera = v),
contentPadding: EdgeInsets.zero,
),
SwitchListTile(
title: const Text('Flash'),
value: _enableTorch,
onChanged: (v) => setState(() => _enableTorch = v),
contentPadding: EdgeInsets.zero,
),
Row(
children: [
const Text('Orientação: '),
const SizedBox(width: 8),
DropdownButton<String>(
value: _orientation,
items: const [
DropdownMenuItem(
value: 'portrait',
child: Text('Vertical'),
),
DropdownMenuItem(
value: 'landscape',
child: Text('Horizontal'),
),
],
onChanged: (v) =>
setState(() => _orientation = v ?? 'portrait'),
),
],
),
SwitchListTile(
title: const Text('Foco automático'),
value: _enableAutoFocus,
onChanged: (v) => setState(() => _enableAutoFocus = v),
contentPadding: EdgeInsets.zero,
),
SwitchListTile(
title: const Text('Zoom manual'),
value: _enableZoom,
onChanged: (v) => setState(() => _enableZoom = v),
contentPadding: EdgeInsets.zero,
),
if (_enableZoom)
_LabeledSlider(
label: 'Zoom',
value: _zoomLevel,
min: 0,
max: 1,
divisions: 10,
display: '${(_zoomLevel * 100).round()}%',
onChanged: (v) => setState(() => _zoomLevel = v),
),
_LabeledSlider(
label: 'Brilho',
value: _brightness,
min: 0,
max: 1,
divisions: 10,
display: '${(_brightness * 100).round()}%',
onChanged: (v) => setState(() => _brightness = v),
),
_LabeledSlider(
label: 'Contraste',
value: _contrast,
min: 0,
max: 1,
divisions: 10,
display: '${(_contrast * 100).round()}%',
onChanged: (v) => setState(() => _contrast = v),
),
_LabeledSlider(
label: 'Exposição',
value: _exposureCompensation,
min: -2,
max: 2,
divisions: 20,
display: _exposureCompensation.toStringAsFixed(1),
onChanged: (v) =>
setState(() => _exposureCompensation = v),
),
],
),
const SizedBox(height: 12),
// ── Botões ───────────────────────────────────────────────────────
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _isLoading ? null : _scanQRCode,
icon: _isLoading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.qr_code_scanner),
label: const Text('Leitura única'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _isLoading ? null : _abrirPreviewEmbutido,
icon: const Icon(Icons.camera_alt),
label: const Text('Preview embutido'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
const SizedBox(height: 8),
// Mesma tela, forçando Hybrid Composition — para comparar num
// aparelho que apresente artefatos com TLHC.
OutlinedButton.icon(
onPressed: _isLoading
? null
: () => _abrirPreviewEmbutido(hybrid: true),
icon: const Icon(Icons.layers),
label: const Text('Preview embutido (Hybrid Composition)'),
),
const SizedBox(height: 16),
// ── Resultado ────────────────────────────────────────────────────
_SectionCard(
title: 'Resultado',
children: [
SelectableText(
_qrCodeResult,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
],
),
),
),
);
}
}
// ── Widgets auxiliares ────────────────────────────────────────────────────────
class _SectionCard extends StatelessWidget {
final String title;
final List<Widget> children;
const _SectionCard({required this.title, required this.children});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
...children,
],
),
);
}
}
class _LabeledSlider extends StatelessWidget {
final String label;
final double value;
final double min;
final double max;
final int divisions;
final String display;
final ValueChanged<double> onChanged;
const _LabeledSlider({
required this.label,
required this.value,
required this.min,
required this.max,
required this.divisions,
required this.display,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
SizedBox(width: 90, child: Text(label)),
Expanded(
child: Slider(
value: value,
min: min,
max: max,
divisions: divisions,
label: display,
onChanged: onChanged,
),
),
SizedBox(width: 50, child: Text(display, textAlign: TextAlign.end)),
],
);
}
}