flutter_integrated_whb/lib/pages/home/study/face_ecognition_page.dart

476 lines
14 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:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart' as ph;
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import 'package:qhd_prevention/tools/tools.dart';
// 在类最上面State 内)加入一个 channel 常量
const MethodChannel _platformChan = MethodChannel('qhd_prevention/permissions');
/// 人脸识别模式
enum FaceMode { setUpdata, study, scan }
class FaceRecognitionPage extends StatefulWidget {
final String studentId;
final Map data;
final FaceMode mode;
const FaceRecognitionPage({
Key? key,
required this.studentId,
required this.data,
this.mode = FaceMode.study,
}) : super(key: key);
@override
_FaceRecognitionPageState createState() => _FaceRecognitionPageState();
}
class _FaceRecognitionPageState extends State<FaceRecognitionPage>
with WidgetsBindingObserver {
CameraController? _cameraController;
Timer? _timer;
int _attempts = 0;
static const int _maxAttempts = 8;
static const Duration _interval = Duration(seconds: 2);
String _errMsg = '';
bool get _isManualMode => widget.mode == FaceMode.setUpdata;
bool _isInitializing = false;
bool _isTaking = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// 延迟到首帧渲染后再请求权限并初始化相机,避免 iOS 在还未准备好时错过系统弹窗
WidgetsBinding.instance.addPostFrameCallback((_) {
_initCameraWithPermission();
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_timer?.cancel();
try {
_cameraController?.dispose();
} catch (_) {}
super.dispose();
}
// 生命周期:后台/前台切换时处理相机释放/恢复
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (_cameraController == null || !_cameraController!.value.isInitialized) {
return;
}
if (state == AppLifecycleState.inactive) {
// 可选:释放相机,节省资源
try {
_cameraController?.dispose();
Navigator.pop(context);
} catch (_) {}
_cameraController = null;
} else if (state == AppLifecycleState.resumed) {
// 恢复时重新初始化相机(并会再次请求权限)
if (!_isInitializing) {
_initCameraWithPermission();
}
}
}
/// 请求 camera 权限iOS优先走原生 AVCaptureDevice.requestAccessAndroid使用 permission_handler
Future<bool> _requestCameraPermission() async {
try {
// iOS使用原生 API 强制唤起系统授权弹窗
if (Platform.isIOS) {
try {
final dynamic res = await _platformChan.invokeMethod('requestCameraAccess');
// 原生返回 true/false
if (res == true) {
debugPrint('[FaceRecognition] iOS native camera access granted');
return true;
} else {
debugPrint('[FaceRecognition] iOS native camera access denied');
// 如果被拒绝(非永久还是永久都返回 false再根据 permission_handler 的状态做后续提示/引导
final status = await ph.Permission.camera.status;
if (status.isPermanentlyDenied) {
await _showPermissionDialog(permanent: true);
} else {
await _showPermissionDialog(permanent: false);
}
return false;
}
} on PlatformException catch (e) {
debugPrint('[FaceRecognition] platform channel error: $e — fallback to permission_handler');
// 如果 platform channel 异常,回退到 permission_handler
}
}
// 非 iOS 或 platform 调用失败时走 permission_handler保证 Android 正常)
final result = await ph.Permission.camera.request();
debugPrint('[FaceRecognition] permission_handler camera result: $result');
if (result.isGranted) return true;
if (result.isPermanentlyDenied) {
await _showPermissionDialog(permanent: true);
return false;
}
if (result.isDenied) {
await _showPermissionDialog(permanent: false);
return false;
}
if (result.isRestricted) {
if (mounted) ToastUtil.showNormal(context, '相机权限受限,无法使用本功能');
return false;
}
if (result.isLimited) {
if (mounted) ToastUtil.showNormal(context, '相机权限受限limited');
return false;
}
return false;
} catch (e) {
debugPrint('[FaceRecognition] permission request error: $e');
if (mounted) ToastUtil.showNormal(context, '请求相机权限时出错');
return false;
}
}
/// 显示权限被拒/永久拒绝对话框(区分 permanent
Future<void> _showPermissionDialog({required bool permanent}) async {
if (!mounted) return;
await CustomAlertDialog.showConfirm(
context,
title: '需要相机权限',
content:
permanent
? '检测到相机权限已被永久拒绝,请到系统设置中打开相机权限以继续使用人脸识别功能。'
: '相机权限被拒绝,是否重试或前往设置打开权限?',
cancelText: '取消',
onConfirm: () async {
try {
final retry = await ph.Permission.camera.request();
debugPrint('[FaceRecognition] retry request result: $retry');
if (retry.isGranted) {
if (mounted) await _initCamera();
} else if (retry.isPermanentlyDenied) {
try {
await ph.openAppSettings();
} catch (e) {
debugPrint('[FaceRecognition] openAppSettings error: $e');
if (mounted) ToastUtil.showNormal(context, '无法打开设置,请手动前往系统设置授权');
}
} else {
if (mounted) ToastUtil.showNormal(context, '相机权限未授予');
}
} catch (e) {
debugPrint('[FaceRecognition] retry request error: $e');
}
},
);
}
/// 初始化:先请求权限,再打开相机
Future<void> _initCameraWithPermission() async {
if (!mounted) return;
final ok = await _requestCameraPermission();
if (!ok) return;
await _initCamera();
}
Future<void> _initCamera() async {
if (_isInitializing) return;
_isInitializing = true;
try {
final cams = await availableCameras();
CameraDescription? front;
try {
front = cams.firstWhere(
(c) => c.lensDirection == CameraLensDirection.front,
);
} catch (_) {
if (cams.isNotEmpty) front = cams.first;
}
if (front == null) {
if (!mounted) return;
ToastUtil.showError(context, '未检测到可用摄像头');
_isInitializing = false;
return;
}
_cameraController = CameraController(
front,
ResolutionPreset.medium,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.yuv420,
);
await _cameraController!.initialize();
// 尽量关闭闪光
try {
await _cameraController!.setFlashMode(FlashMode.off);
} catch (_) {}
if (!mounted) return;
setState(() {});
// 启动自动拍照定时器(若为自动模式)
_timer?.cancel();
_attempts = 0;
if (!_isManualMode) {
_timer = Timer.periodic(_interval, (_) => _captureAndUpload());
}
} catch (e, st) {
debugPrint('[FaceRecognition] init camera error: $e\n$st');
if (mounted) {
ToastUtil.showError(context, '初始化摄像头失败');
}
} finally {
_isInitializing = false;
}
}
/// 自动模式:定时拍照并上传
Future<void> _captureAndUpload() async {
if (_isManualMode) return;
if (_cameraController == null || !_cameraController!.value.isInitialized)
return;
if (_attempts >= _maxAttempts) return _onTimeout();
if (_isTaking) return;
_isTaking = true;
_attempts++;
try {
try {
await _cameraController!.setFlashMode(FlashMode.off);
} catch (_) {}
final XFile pic = await _cameraController!.takePicture();
var res = {};
switch(widget.mode) {
case FaceMode.study:
res = await ApiService.getStudyUserFace(pic.path, widget.data);
break;
case FaceMode.setUpdata:
res = await ApiService.getUpdataUserFace(pic.path, widget.data);
break;
case FaceMode.scan:
res = await ApiService.getScanUserFace(pic.path, widget.data);
break;
}
if (res['result'] == 'success') {
_onSuccess();
} else {
if (!mounted) return;
setState(() {
_errMsg = (res['msg'] ?? '').toString();
});
}
} catch (e, st) {
debugPrint('[FaceRecognition] capture error: $e\n$st');
// 忽略单次异常,等待下一次尝试
} finally {
_isTaking = false;
}
}
/// 手动拍照并上传
Future<void> _captureAndReload() async {
if (_cameraController == null || !_cameraController!.value.isInitialized)
return;
if (_isTaking) return;
_isTaking = true;
_showLoading();
try {
// 再次确认 camera 权限(以防用户运行时撤销)
final status = await ph.Permission.camera.status;
if (!status.isGranted) {
final ok = await _requestCameraPermission();
if (!ok) {
_hideLoading();
_isTaking = false;
return;
}
}
try {
await _cameraController!.setFlashMode(FlashMode.off);
} catch (_) {}
final XFile pic = await _cameraController!.takePicture();
final res = await ApiService.reloadMyFace(pic.path, widget.studentId);
_hideLoading();
if (res['result'] == 'success') {
_onSuccess();
} else {
ToastUtil.showError(context, '验证失败,请重试');
}
} catch (e, st) {
debugPrint('[FaceRecognition] manual capture error: $e\n$st');
_hideLoading();
ToastUtil.showError(context, '拍照失败,请重试');
} finally {
_isTaking = false;
}
}
void _onSuccess() {
_timer?.cancel();
if (widget.mode == FaceMode.setUpdata) {
ToastUtil.showSuccess(context, '已更新人脸信息');
Future.delayed(const Duration(milliseconds: 800), () {
if (mounted) Navigator.of(context).pop(true);
});
return;
}
Future.delayed(const Duration(milliseconds: 800), () {
if (mounted) Navigator.of(context).pop(true);
});
}
void _onTimeout() {
_timer?.cancel();
ToastUtil.showError(context, '人脸超时,请重新识别!');
Future.delayed(const Duration(seconds: 3), () {
if (mounted) Navigator.of(context).pop(false);
});
}
void _showLoading() {
if (!mounted) return;
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
}
void _hideLoading() {
if (!mounted) return;
if (Navigator.canPop(context)) Navigator.pop(context);
}
void _showToast(String msg) {
ToastUtil.showNormal(context, msg);
}
@override
Widget build(BuildContext context) {
if (_cameraController == null || !_cameraController!.value.isInitialized) {
return const Scaffold(
backgroundColor: Colors.white,
body: Center(child: CircularProgressIndicator()),
);
}
final previewSize = _cameraController!.value.previewSize!;
final previewAspect = previewSize.height / previewSize.width;
final radius = (screenWidth(context) - 100) / 2;
return Scaffold(
backgroundColor: Colors.white,
appBar: const MyAppbar(title: '人脸识别'),
body: Stack(
children: [
Positioned.fill(child: Container(color: Colors.white)),
Transform.translate(
offset: const Offset(0, -100),
child: Stack(
children: [
Center(
child: ClipOval(
child: AspectRatio(
aspectRatio: previewAspect,
child: CameraPreview(_cameraController!),
),
),
),
Positioned.fill(
child: CustomPaint(
painter: _WhiteMaskPainter(radius: radius),
),
),
],
),
),
Align(
alignment: Alignment.center,
child: Padding(
padding: const EdgeInsets.only(top: 250),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'请将人脸置于圆圈内',
style: TextStyle(fontSize: 16, color: Colors.black87),
textAlign: TextAlign.center,
),
if (_errMsg.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 6.0),
child: Text(
_errMsg,
style: const TextStyle(fontSize: 14, color: Colors.red),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 20),
if (_isManualMode)
CustomButton(
text: '拍照/上传',
backgroundColor: Colors.blue,
onPressed: _captureAndReload,
),
],
),
),
),
],
),
);
}
}
class _WhiteMaskPainter extends CustomPainter {
final double radius;
_WhiteMaskPainter({required this.radius});
@override
void paint(Canvas canvas, Size size) {
canvas.saveLayer(Offset.zero & size, Paint());
canvas.drawRect(Offset.zero & size, Paint()..color = Colors.white);
canvas.drawCircle(
size.center(Offset.zero),
radius,
Paint()..blendMode = BlendMode.clear,
);
canvas.restore();
}
@override
bool shouldRepaint(covariant CustomPainter old) => false;
}