import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:uuid/uuid.dart'; /// 自定义相机预览 + 拍照队列 /// 对应 Android CheckPhotoActivity(多张拍照取证) class CameraView extends StatefulWidget { final void Function(String path) onCaptured; const CameraView({super.key, required this.onCaptured}); @override State createState() => _CameraViewState(); } class _CameraViewState extends State with WidgetsBindingObserver { CameraController? _controller; List _cameras = []; int _cameraIdx = 0; bool _initializing = true; String? _error; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _setup(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _controller?.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { final c = _controller; if (c == null || !c.value.isInitialized) return; if (state == AppLifecycleState.inactive || state == AppLifecycleState.paused) { c.dispose(); } else if (state == AppLifecycleState.resumed) { _setup(); } } Future _setup() async { setState(() { _initializing = true; _error = null; }); try { final cam = await Permission.camera.request(); if (!cam.isGranted) { setState(() { _initializing = false; _error = '未授予相机权限'; }); return; } _cameras = await availableCameras(); if (_cameras.isEmpty) { setState(() { _initializing = false; _error = '未检测到摄像头'; }); return; } _cameraIdx = _cameras.indexWhere( (c) => c.lensDirection == CameraLensDirection.back, ); if (_cameraIdx < 0) _cameraIdx = 0; await _initController(); } catch (e) { setState(() { _initializing = false; _error = '相机初始化失败: $e'; }); } } Future _initController() async { final c = CameraController( _cameras[_cameraIdx], ResolutionPreset.high, enableAudio: false, ); await c.initialize(); if (!mounted) { await c.dispose(); return; } setState(() { _controller = c; _initializing = false; }); } Future _switchCamera() async { if (_cameras.length < 2) return; _cameraIdx = (_cameraIdx + 1) % _cameras.length; await _controller?.dispose(); await _initController(); } Future _takePicture() async { final c = _controller; if (c == null || !c.value.isInitialized) return; if (c.value.isTakingPicture) return; try { EasyLoading.show(status: '拍照中…'); final xfile = await c.takePicture(); EasyLoading.dismiss(); // 复制到应用缓存目录(防止临时文件被清理) final dir = await getTemporaryDirectory(); final out = '${dir.photos_cache_path()}/${const Uuid().v4()}.jpg'; await File(xfile.path).copy(out); widget.onCaptured(out); } catch (e) { EasyLoading.dismiss(); EasyLoading.showError('拍照失败: $e'); } } @override Widget build(BuildContext context) { if (_error != null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.no_photography, size: 64, color: Colors.white), const SizedBox(height: 8), Text(_error!, style: const TextStyle(color: Colors.white)), TextButton(onPressed: _setup, child: const Text('重试')), ], ), ); } if (_initializing || _controller == null) { return const Center( child: CircularProgressIndicator(color: Colors.white), ); } return Stack( fit: StackFit.expand, children: [ CameraPreview(_controller!), // 顶部切换镜头 Positioned( top: 16, right: 16, child: SafeArea( child: IconButton( icon: const Icon(Icons.cameraswitch, color: Colors.white), onPressed: _switchCamera, ), ), ), // 底部拍照按钮 Positioned( left: 0, right: 0, bottom: 32, child: Center( child: GestureDetector( onTap: _takePicture, child: Container( width: 72, height: 72, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 4), ), child: const Center( child: Icon(Icons.circle, color: Colors.white, size: 56), ), ), ), ), ), ], ); } } extension _DirPath on Directory { String photos_cache_path() { final p = path; if (p.endsWith('/')) return '${p}photos'; return '$p/photos'; } } /// 底部照片缩略图横排(可删除) class PhotoThumbStrip extends StatelessWidget { final List paths; final ValueChanged onRemove; final VoidCallback? onAdd; const PhotoThumbStrip({ super.key, required this.paths, required this.onRemove, this.onAdd, }); @override Widget build(BuildContext context) { return SizedBox( height: 88, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), itemCount: paths.length + 1, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (ctx, i) { if (i == paths.length) { return InkWell( onTap: onAdd, child: Container( width: 72, height: 72, decoration: BoxDecoration( color: Colors.black12, borderRadius: BorderRadius.circular(6), ), child: const Icon(Icons.add_a_photo, color: Colors.white70), ), ); } return Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.file( File(paths[i]), width: 72, height: 72, fit: BoxFit.cover, ), ), Positioned( top: 0, right: 0, child: GestureDetector( onTap: () => onRemove(i), child: Container( decoration: const BoxDecoration( color: Colors.black54, shape: BoxShape.circle, ), padding: const EdgeInsets.all(2), child: const Icon( Icons.close, size: 14, color: Colors.white, ), ), ), ), ], ); }, ), ); } } /// 备选:从相册选择(用 image_picker) class AlbumPicker { static final _picker = ImagePicker(); static Future> pick({int max = 9}) async { final files = await _picker.pickMultiImage( imageQuality: 85, maxWidth: 1920, ); if (files.isEmpty) return const []; return files.take(max).map((f) => f.path).toList(); } }