import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:mini_tias/providers/camera_provider.dart'; import 'package:mini_tias/services/raw_capture_service.dart'; import 'package:mini_tias/widgets/camera_preview.dart'; import 'package:mini_tias/widgets/shutter_button.dart'; /// 撮影画面.カメラプレビューとシャッターボタンを表示する. class CaptureScreen extends StatefulWidget { const CaptureScreen({super.key}); @override State createState() => _CaptureScreenState(); } class _CaptureScreenState extends State with WidgetsBindingObserver { String? _previousFileName; bool _timerEnabled = false; int? _countdown; Timer? _countdownTimer; Uint8List? _snapshotBytes; final _rawCaptureService = RawCaptureService(); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().initialize(); }); } @override void dispose() { _countdownTimer?.cancel(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { final cameraProvider = context.read(); if (state == AppLifecycleState.paused) { _cancelCountdown(); cameraProvider.disposeCamera(); } else if (state == AppLifecycleState.resumed) { cameraProvider.initialize(); } } void _onShutterPressed() { final cameraProvider = context.read(); if (cameraProvider.isSaving || _countdown != null) return; if (_timerEnabled) { _startCountdown(); } else { _captureWithSnapshot(); } } Future _captureWithSnapshot() async { // PixelCopy でカメラ領域のスナップショットを瞬時に取得 try { if (!mounted) return; final renderBox = context.findRenderObject() as RenderBox?; if (renderBox != null) { final position = renderBox.localToGlobal(Offset.zero); final size = renderBox.size; final ratio = View.of(context).devicePixelRatio; // localToGlobal は回転後の座標(右下)を返すため,左上を計算する final topLeftX = position.dx - size.width; final topLeftY = position.dy - size.height; final bytes = await _rawCaptureService.capturePreviewFrame( x: (topLeftX * ratio).round(), y: (topLeftY * ratio).round(), width: (size.width * ratio).round(), height: (size.height * ratio).round(), ); setState(() => _snapshotBytes = bytes); } } catch (_) { // 失敗しても撮影は続行 } if (!mounted) return; context.read().takePicture(); } void _startCountdown() { setState(() => _countdown = 3); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_countdown == null) { timer.cancel(); return; } if (_countdown! <= 1) { timer.cancel(); setState(() => _countdown = null); _captureWithSnapshot(); } else { setState(() => _countdown = _countdown! - 1); } }); } void _cancelCountdown() { _countdownTimer?.cancel(); _countdownTimer = null; if (mounted) { setState(() => _countdown = null); } } void _showSaveResult(CameraProvider cameraProvider) { final fileName = cameraProvider.lastSavedFileName; if (fileName != null && fileName != _previousFileName) { _previousFileName = fileName; _snapshotBytes = null; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('保存しました: $fileName'), duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, margin: EdgeInsets.only( bottom: MediaQuery.of(context).size.height / 2, left: 16, right: 16, ), ), ); }); } final error = cameraProvider.errorMessage; if (error != null && !cameraProvider.isInitialized) return; if (error != null && !cameraProvider.isSaving) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(error), backgroundColor: Colors.red), ); }); } } @override Widget build(BuildContext context) { final cameraProvider = context.watch(); _showSaveResult(cameraProvider); if (cameraProvider.permissionDenied) { return _buildPermissionDenied(cameraProvider); } if (cameraProvider.errorMessage != null && !cameraProvider.isInitialized) { return Center(child: Text(cameraProvider.errorMessage!)); } if (!cameraProvider.isInitialized && !cameraProvider.isSaving && _snapshotBytes == null) { return const Center(child: CircularProgressIndicator()); } return Stack( children: [ // 保存中はスナップショットを表示(Transform なし,Kotlin 側で回転済み) if (_snapshotBytes != null && (!cameraProvider.isInitialized || cameraProvider.isSaving)) Positioned.fill( child: Image.memory(_snapshotBytes!, fit: BoxFit.cover), ) // カメラプレビュー else if (cameraProvider.isInitialized && cameraProvider.controller != null) Positioned.fill( child: CameraPreviewWidget(controller: cameraProvider.controller!), ), // 保存中インジケーター if (cameraProvider.isSaving) const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: Colors.white), SizedBox(height: 12), Text( '保存中...', style: TextStyle(color: Colors.white, fontSize: 16), ), ], ), ), // カウントダウン表示 if (_countdown != null) Center( child: Stack( children: [ Text( '$_countdown', style: TextStyle( fontSize: 96, fontWeight: FontWeight.bold, foreground: Paint() ..style = PaintingStyle.stroke ..strokeWidth = 6 ..color = Colors.black, ), ), Text( '$_countdown', style: const TextStyle( color: Colors.white, fontSize: 96, fontWeight: FontWeight.bold, ), ), ], ), ), // タイマー切り替えボタン(左側) Positioned( left: 24, bottom: 36, child: GestureDetector( onTap: () { _cancelCountdown(); setState(() => _timerEnabled = !_timerEnabled); }, child: Stack( alignment: Alignment.center, children: [ Icon( _timerEnabled ? Icons.timer : Icons.timer_off, color: Colors.black, size: 40, ), Icon( _timerEnabled ? Icons.timer : Icons.timer_off, color: _timerEnabled ? Colors.yellow : Colors.grey, size: 32, ), ], ), ), ), // シャッターボタン(中央) Positioned( left: 0, right: 0, bottom: 24, child: Center( child: ShutterButton( onPressed: cameraProvider.isSaving || _countdown != null ? () {} : _onShutterPressed, ), ), ), ], ); } Widget _buildPermissionDenied(CameraProvider cameraProvider) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(cameraProvider.errorMessage ?? 'カメラの権限が必要です'), const SizedBox(height: 16), if (cameraProvider.permissionPermanentlyDenied) ElevatedButton( onPressed: () => cameraProvider.openSettings(), child: const Text('設定画面を開く'), ) else ElevatedButton( onPressed: () => cameraProvider.initialize(), child: const Text('権限を許可する'), ), ], ), ); } }