jjsos_autosos_flutter/lib/widget/photo_grid_picker.dart

290 lines
7.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<CameraView> createState() => _CameraViewState();
}
class _CameraViewState extends State<CameraView>
with WidgetsBindingObserver {
CameraController? _controller;
List<CameraDescription> _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<void> _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<void> _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<void> _switchCamera() async {
if (_cameras.length < 2) return;
_cameraIdx = (_cameraIdx + 1) % _cameras.length;
await _controller?.dispose();
await _initController();
}
Future<void> _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<String> paths;
final ValueChanged<int> 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<List<String>> 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();
}
}