import 'dart:ui' show PlatformDispatcher; import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart' show defaultTargetPlatform; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import '../detection/detector_worker.dart'; import '../reminder/reminder.dart'; import 'app_camera_controller.dart'; import 'camera_view_model.dart'; import 'detection_overlay.dart'; import 'frame_analyzer.dart'; /// 主界面:相机预览 + 检测框 overlay + 顶栏(返回/切换摄像头) class CameraScreen extends StatefulWidget { const CameraScreen({super.key}); @override State createState() => _CameraScreenState(); } class _CameraScreenState extends State { CameraViewModel? _viewModel; FrameAnalyzer? _analyzer; AppCameraController? _cameraController; bool _initFailed = false; bool _permissionGranted = false; String? _globalError; String? _initError; @override void initState() { super.initState(); final oldPlatform = PlatformDispatcher.instance.onError; PlatformDispatcher.instance.onError = (error, stack) { setState(() => _globalError = 'Platform: $error'); return oldPlatform?.call(error, stack) ?? false; }; WidgetsBinding.instance.addPostFrameCallback((_) => _init()); // 相机页常亮:野外观察时保持屏幕不熄(离开页面时关闭) WakelockPlus.enable(); } Future _init() async { final granted = await Permission.camera.request().isGranted; if (!mounted) return; setState(() => _permissionGranted = granted); if (!granted) return; // 模型加载/推理在后台 isolate,不阻塞 UI;worker 为 null 时仅预览并提示 final worker = await DetectorWorker.create(); final viewModel = CameraViewModel(reminder: Reminder()); viewModel.setModelReady(worker != null); final analyzer = FrameAnalyzer(worker: worker, viewModel: viewModel); if (!mounted) { analyzer.dispose(); viewModel.dispose(); return; } setState(() { _viewModel = viewModel; _analyzer = analyzer; }); await _startCamera(); } Future _startCamera() async { final analyzer = _analyzer; if (analyzer == null) return; try { final controller = await AppCameraController.create(); if (controller == null) { setState(() { _initFailed = true; _initError = '未找到可用摄像头'; }); return; } await controller.start(analyzer); if (!mounted) { controller.stop(); return; } setState(() { _cameraController = controller; _initFailed = false; _initError = null; }); } catch (e) { if (!mounted) return; setState(() { _initFailed = true; _initError = '$e'; }); } } @override void dispose() { WakelockPlus.disable(); _cameraController?.stop(); _analyzer?.dispose(); _viewModel?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final vm = _viewModel; final camera = _cameraController; return Scaffold( backgroundColor: Colors.black, body: Stack( fit: StackFit.expand, children: [ if (!_permissionGranted) _PermissionGuide(onRequest: () => _init()) else if (vm != null && (camera?.isInitialized ?? false)) // 预览 + 检测框同几何:overlay 作为 CameraPreview 的 child, // 与纹理共享同一 Stack/尺寸,避免比例或裁剪导致的位置偏移 ListenableBuilder( listenable: vm, builder: (context, _) => _ZoomablePreview( controller: camera!.currentController, imageWidthPx: vm.state.imageWidthPx, imageHeightPx: vm.state.imageHeightPx, overlay: DetectionOverlay( results: vm.state.results, // iOS 纹理不旋转显示(_wrapInRotatedBox 仅 Android), // 显示方向 = buffer 原样 = 检测方向,旋转必须为 0; // Android 纹理被 RotatedBox 旋转,需用插件报告的 rotation。 rotation: defaultTargetPlatform == TargetPlatform.iOS ? 0 : vm.state.rotation, imageWidthPx: vm.state.imageWidthPx, imageHeightPx: vm.state.imageHeightPx, ), ), ) else if (camera?.isInitialized ?? false) _ZoomablePreview(controller: camera!.currentController) else const Center( child: Text('相机启动中…', style: TextStyle(color: Colors.white70)), ), // 帧级动态层(横幅/诊断行)单独订阅 viewModel,避免整屏重建 if (vm != null) ListenableBuilder( listenable: vm, builder: (context, _) => _buildDiagnosticsLayer(camera), ), if (_initFailed) Center( child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('相机初始化失败', style: TextStyle(color: Colors.white)), if (_initError != null) Padding( padding: const EdgeInsets.only(top: 8), child: Text( _initError!, maxLines: 3, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: const TextStyle( color: Colors.redAccent, fontSize: 11), ), ), TextButton( onPressed: () { setState(() => _initFailed = false); _startCamera(); }, child: const Text('重试'), ), ], ), ), ), Positioned( top: MediaQuery.of(context).padding.top + 8, left: 0, right: 0, child: _CameraTopBar( onClose: () => Navigator.of(context).pop(), ), ), ], ), ); } Widget _buildDiagnosticsLayer(AppCameraController? camera) { final vm = _viewModel!; return Stack( fit: StackFit.expand, children: [ // 模型未加载时仅显示相机预览,不做检测标注(横幅置于顶栏下方,避免与底部诊断行重叠) if (!vm.state.modelReady) Positioned( left: 16, right: 16, top: MediaQuery.of(context).padding.top + 56, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(8), ), child: Text( '识别模型加载失败:${DetectorWorker.lastLoadError ?? '未知原因'}\n最后步骤:${DetectorWorker.lastLog ?? '-'}', textAlign: TextAlign.center, style: const TextStyle(color: Colors.orange, fontSize: 14), ), ), ), Positioned( left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 8, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( '模型:${vm.state.modelReady ? '已加载' : '未加载'} 帧:${vm.state.framesReceived} 流:${camera?.streamCallbacks ?? 0} 推理:${vm.state.debugDetectCalls}次 异常:${vm.state.debugDetectErrors}次 处理:${vm.state.debugLastMs}ms 最高分:${(vm.state.debugHighestScore * 100).toStringAsFixed(1)}% 图:${vm.state.imageWidthPx}x${vm.state.imageHeightPx} 旋:${vm.state.rotation}', style: const TextStyle(color: Colors.white70, fontSize: 12), ), if (camera != null) Text( 'streaming:${camera.currentController.value.isStreamingImages} ' 'camErr:${camera.currentController.value.errorDescription ?? '无'}', maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.cyanAccent, fontSize: 11), ), if (vm.state.debugLastError != null) Text( vm.state.debugLastError!, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: const TextStyle( color: Colors.redAccent, fontSize: 11), ), if (_globalError != null) Text( _globalError!, maxLines: 3, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: const TextStyle( color: Colors.redAccent, fontSize: 11), ), ], ), ), ], ); } } /// 双指捏合缩放预览;overlay 与纹理同几何(CameraPreview child) class _ZoomablePreview extends StatefulWidget { final CameraController controller; /// 检测框 overlay(随帧更新,作为 CameraPreview 的 child 与纹理同区域) final Widget? overlay; /// 当前帧图像尺寸(用于按 buffer 比例约束预览,保证无拉伸变形) final int imageWidthPx; final int imageHeightPx; const _ZoomablePreview({ required this.controller, this.overlay, this.imageWidthPx = 0, this.imageHeightPx = 0, }); @override State<_ZoomablePreview> createState() => _ZoomablePreviewState(); } class _ZoomablePreviewState extends State<_ZoomablePreview> { double _minZoom = 1.0; double _maxZoom = 1.0; double _currentZoom = 1.0; double _gestureStartZoom = 1.0; @override void initState() { super.initState(); widget.controller.getMinZoomLevel().then((v) { if (mounted) setState(() => _minZoom = v); }); widget.controller.getMaxZoomLevel().then((v) { if (mounted) setState(() => _maxZoom = v); }); } @override Widget build(BuildContext context) { final preview = GestureDetector( onScaleStart: (_) => _gestureStartZoom = _currentZoom, onScaleUpdate: (d) { final target = (_gestureStartZoom * d.scale).clamp(_minZoom, _maxZoom); if ((target - _currentZoom).abs() < 0.01) return; _currentZoom = target; widget.controller.setZoomLevel(target); }, child: CameraPreview(widget.controller, child: widget.overlay), ); final w = widget.imageWidthPx.toDouble(); final h = widget.imageHeightPx.toDouble(); if (w <= 0 || h <= 0) return preview; // 按 buffer 比例约束显示区域:纹理与 overlay 同区域等比显示(无变形) return Center( child: AspectRatio(aspectRatio: w / h, child: preview), ); } } class _CameraTopBar extends StatelessWidget { final VoidCallback onClose; const _CameraTopBar({ required this.onClose, }); @override Widget build(BuildContext context) { return Container( color: Colors.black54, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( tooltip: '返回', icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: onClose, ), ], ), ); } } class _PermissionGuide extends StatelessWidget { final VoidCallback onRequest; const _PermissionGuide({required this.onRequest}); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( '需要相机权限才能进行实时识别', style: TextStyle(color: Colors.white), ), const SizedBox(height: 16), FilledButton(onPressed: onRequest, child: const Text('授权相机')), ], ), ); } }