controller.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import 'dart:async';
  2. import 'package:camera/camera.dart';
  3. import 'package:fis_jsonrpc/rpc.dart';
  4. import 'package:flutter/foundation.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/services.dart';
  7. import 'package:get/get.dart';
  8. import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
  9. import 'package:vitalapp/architecture/storage/storage.dart';
  10. import 'package:vitalapp/architecture/utils/prompt_box.dart';
  11. import 'package:vitalapp/managers/interfaces/cache.dart';
  12. import 'package:fis_common/logger/logger.dart';
  13. import 'package:vitalapp/managers/interfaces/patient.dart';
  14. import 'package:vitalapp/rpc.dart';
  15. import 'package:vitalapp/store/store.dart';
  16. import 'index.dart';
  17. class IdCardScanController extends GetxController with WidgetsBindingObserver {
  18. IdCardScanController();
  19. final state = IdCardScanState();
  20. List<CameraDescription> _cameras = <CameraDescription>[];
  21. List<CameraDescription> get cameras => _cameras;
  22. CameraController? kCameraController;
  23. double _minAvailableExposureOffset = 0.0;
  24. double _maxAvailableExposureOffset = 0.0;
  25. double _minAvailableZoom = 1.0;
  26. double _maxAvailableZoom = 1.0;
  27. final double _currentScale = 1.0;
  28. double _baseScale = 1.0;
  29. // 屏幕上手指数量
  30. int pointers = 0;
  31. /// 开始缩放
  32. void handleScaleStart(ScaleStartDetails details) {
  33. _baseScale = _currentScale;
  34. }
  35. /// 当前捕获帧的人脸列表
  36. List<Face> kFrameFacesResult = [];
  37. /// 当前捕获帧大小
  38. Size kFrameImageSize = Size.zero;
  39. /// 缩放更新
  40. Future<void> handleScaleUpdate(ScaleUpdateDetails details) async {
  41. // When there are not exactly two fingers on screen don't scale
  42. if (kCameraController == null || pointers != 2) {
  43. return;
  44. }
  45. // 屏蔽缩放
  46. // _currentScale = (_baseScale * details.scale)
  47. // .clamp(_minAvailableZoom, _maxAvailableZoom);
  48. // await kCameraController!.setZoomLevel(_currentScale);
  49. }
  50. /// 修改对焦点 [暂不执行]
  51. void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
  52. if (kCameraController == null) {
  53. return;
  54. }
  55. final CameraController cameraController = kCameraController!;
  56. final Offset offset = Offset(
  57. details.localPosition.dx / constraints.maxWidth,
  58. details.localPosition.dy / constraints.maxHeight,
  59. );
  60. cameraController.setExposurePoint(offset);
  61. cameraController.setFocusPoint(offset);
  62. }
  63. /// 初始化相机
  64. Future<void> initAvailableCameras() async {
  65. try {
  66. _cameras = await availableCameras();
  67. if (_cameras.isNotEmpty) {
  68. // state.isCameraReady = true;
  69. }
  70. // print("cameras: ${_cameras.length}");
  71. } on CameraException catch (e) {
  72. logger.e("cameras: ${e.code} ${e.description}");
  73. }
  74. }
  75. /// 启动指定相机
  76. Future<void> openNewCamera(CameraDescription cameraDescription) async {
  77. final CameraController? oldController = kCameraController;
  78. if (oldController != null) {
  79. // `kCameraController` needs to be set to null before getting disposed,
  80. // to avoid a race condition when we use the kCameraController that is being
  81. // disposed. This happens when camera permission dialog shows up,
  82. // which triggers `didChangeAppLifecycleState`, which disposes and
  83. // re-creates the kCameraController.
  84. kCameraController = null;
  85. await oldController.dispose();
  86. }
  87. final CameraController cameraController = CameraController(
  88. cameraDescription,
  89. ResolutionPreset.max,
  90. enableAudio: false,
  91. imageFormatGroup: ImageFormatGroup.jpeg,
  92. );
  93. kCameraController = cameraController;
  94. // If the kCameraController is updated then update the UI.
  95. cameraController.addListener(() {
  96. if (cameraController.value.hasError) {
  97. PromptBox.toast(
  98. "Camera error ${cameraController.value.errorDescription}");
  99. }
  100. });
  101. try {
  102. await cameraController.initialize();
  103. await Future.wait(<Future<Object?>>[
  104. // The exposure mode is currently not supported on the web.
  105. ...!kIsWeb
  106. ? <Future<Object?>>[
  107. cameraController.getMinExposureOffset().then(
  108. (double value) => _minAvailableExposureOffset = value),
  109. cameraController
  110. .getMaxExposureOffset()
  111. .then((double value) => _maxAvailableExposureOffset = value)
  112. ]
  113. : <Future<Object?>>[],
  114. cameraController
  115. .getMaxZoomLevel()
  116. .then((double value) => _maxAvailableZoom = value),
  117. cameraController
  118. .getMinZoomLevel()
  119. .then((double value) => _minAvailableZoom = value),
  120. ]);
  121. } on CameraException catch (e) {
  122. switch (e.code) {
  123. case 'CameraAccessDenied':
  124. PromptBox.toast('You have denied camera access.');
  125. break;
  126. case 'CameraAccessDeniedWithoutPrompt':
  127. // iOS only
  128. PromptBox.toast('Please go to Settings app to enable camera access.');
  129. break;
  130. case 'CameraAccessRestricted':
  131. // iOS only
  132. PromptBox.toast('Camera access is restricted.');
  133. break;
  134. case 'AudioAccessDenied':
  135. PromptBox.toast('You have denied audio access.');
  136. break;
  137. case 'AudioAccessDeniedWithoutPrompt':
  138. // iOS only
  139. PromptBox.toast('Please go to Settings app to enable audio access.');
  140. break;
  141. case 'AudioAccessRestricted':
  142. // iOS only
  143. PromptBox.toast('Audio access is restricted.');
  144. break;
  145. default:
  146. PromptBox.toast('Error: ${e.code}\n${e.description}');
  147. break;
  148. }
  149. }
  150. }
  151. /// 遍历当前相机列表并启动后置相机
  152. void openBackCamera() async {
  153. if (_cameras.isEmpty) {
  154. PromptBox.toast('Error: No cameras found.');
  155. } else {
  156. for (CameraDescription cameraDescription in _cameras) {
  157. if (cameraDescription.lensDirection == CameraLensDirection.back) {
  158. await openNewCamera(cameraDescription);
  159. lockCaptureOrientation();
  160. update();
  161. state.isCameraReady = true;
  162. break;
  163. }
  164. }
  165. }
  166. }
  167. /// 遍历当前相机列表并启动前置相机
  168. void openFrontCamera() async {
  169. if (_cameras.isEmpty) {
  170. PromptBox.toast('Error: No cameras found.');
  171. } else {
  172. for (CameraDescription cameraDescription in _cameras) {
  173. if (cameraDescription.lensDirection == CameraLensDirection.front) {
  174. await openNewCamera(cameraDescription);
  175. lockCaptureOrientation();
  176. update();
  177. state.isCameraReady = true;
  178. break;
  179. }
  180. }
  181. }
  182. }
  183. /// 相机锁定旋转
  184. Future<void> lockCaptureOrientation() async {
  185. final CameraController? cameraController = kCameraController;
  186. if (cameraController == null || !cameraController.value.isInitialized) {
  187. PromptBox.toast('Error: select a camera first.');
  188. return;
  189. }
  190. if (!cameraController.value.isCaptureOrientationLocked) {
  191. try {
  192. await cameraController
  193. .lockCaptureOrientation(DeviceOrientation.landscapeLeft);
  194. } on CameraException catch (e) {
  195. PromptBox.toast('Error: ${e.code}\n${e.description}');
  196. }
  197. } else {
  198. PromptBox.toast('Rotation lock is already enabled.');
  199. }
  200. }
  201. /// 执行一次拍摄
  202. Future<XFile?> takePicture() async {
  203. final CameraController? cameraController = kCameraController;
  204. if (cameraController == null || !cameraController.value.isInitialized) {
  205. PromptBox.toast('Error: select a camera first.');
  206. return null;
  207. }
  208. if (cameraController.value.isTakingPicture) {
  209. // A capture is already pending, do nothing.
  210. return null;
  211. }
  212. try {
  213. final XFile file = await cameraController.takePicture();
  214. return file;
  215. } on CameraException catch (e) {
  216. PromptBox.toast('Error: ${e.code}\n${e.description}');
  217. return null;
  218. }
  219. }
  220. /// 发生拍摄身份证事件
  221. void onCaptureIdCardButtonPressed() {
  222. state.isIdCardScanning = true;
  223. state.processingImageLocalPath = '';
  224. takePicture().then((XFile? file) async {
  225. // imageFile = file;
  226. if (file != null) {
  227. state.processingImageLocalPath = file.path;
  228. try {
  229. final String fileType = file.path.split('.').last;
  230. if (!['png', 'jpg'].contains(fileType)) {
  231. PromptBox.toast('上传的图像类型错误');
  232. return;
  233. }
  234. final url = await rpc.storage.upload(
  235. file,
  236. fileType: fileType,
  237. );
  238. print('⭐⭐⭐⭐⭐⭐⭐⭐ url: $url');
  239. if (url == null || url.isEmpty) {
  240. PromptBox.toast('图像上传超时,请检测网络');
  241. throw Exception('图像上传超时');
  242. }
  243. PatientBaseDTO result = await rpc.patient.getPatientBaseByImageAsync(
  244. GetPatientBaseByImageRequest(
  245. token: Store.user.token,
  246. image: url,
  247. ),
  248. );
  249. state.processingImageLocalPath = '';
  250. /// 用于关闭 ImageDetectingDialog
  251. if (result.isSuccess) {
  252. PromptBox.toast('身份证识别成功');
  253. final idCardScanResult = IdCardScanResult(
  254. success: true,
  255. patientBaseDTO: result,
  256. );
  257. Get.back<IdCardScanResult>(
  258. result: idCardScanResult,
  259. );
  260. } else {
  261. PromptBox.toast('身份证识别失败: ${result.errorMessage}');
  262. }
  263. } catch (e) {
  264. logger.e("getPatientBaseByImageAsync failed: $e", e);
  265. }
  266. }
  267. state.processingImageLocalPath = '';
  268. state.isIdCardScanning = false;
  269. });
  270. }
  271. /// 在 widget 内存中分配后立即调用。
  272. @override
  273. void onInit() async {
  274. // await initCamera();
  275. super.onInit();
  276. WidgetsBinding.instance.addObserver(this);
  277. }
  278. /// 在 onInit() 之后调用 1 帧。这是进入的理想场所
  279. @override
  280. void onReady() async {
  281. super.onReady();
  282. await initAvailableCameras();
  283. openBackCamera();
  284. }
  285. /// 在 [onDelete] 方法之前调用。
  286. @override
  287. void onClose() {
  288. super.onClose();
  289. final cacheManager = Get.find<ICacheManager>();
  290. cacheManager.clearApplicationImageCache();
  291. WidgetsBinding.instance.removeObserver(this);
  292. final CameraController? cameraController = kCameraController;
  293. if (cameraController != null) {
  294. kCameraController = null;
  295. cameraController.dispose();
  296. }
  297. }
  298. /// dispose 释放内存
  299. @override
  300. void dispose() {
  301. super.dispose();
  302. }
  303. @override
  304. void didChangeAppLifecycleState(AppLifecycleState state) async {
  305. super.didChangeAppLifecycleState(state);
  306. final CameraController? cameraController = kCameraController;
  307. // App state changed before we got the chance to initialize.
  308. if (cameraController == null || !cameraController.value.isInitialized) {
  309. return;
  310. }
  311. if (state == AppLifecycleState.inactive) {
  312. cameraController.dispose();
  313. this.state.isCameraReady = false;
  314. } else if (state == AppLifecycleState.resumed) {
  315. await openNewCamera(cameraController.description);
  316. this.state.isCameraReady = true;
  317. }
  318. }
  319. }
  320. class IdCardScanResult {
  321. bool success;
  322. /// 身份证信息
  323. PatientBaseDTO patientBaseDTO;
  324. IdCardScanResult({
  325. required this.success,
  326. required this.patientBaseDTO,
  327. });
  328. }