From a4eb461457726ecfaea8086d4b2eb05d2b7a48f7 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Wed, 4 Oct 2023 14:33:06 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=85=20Wrap=20controller=20methods=20(#?= =?UTF-8?q?200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #199. --- CHANGELOG.md | 7 +- example/pubspec.yaml | 2 +- lib/src/internals/methods.dart | 8 +- lib/src/states/camera_picker_state.dart | 205 +++++++++++++----- .../states/camera_picker_viewer_state.dart | 11 +- pubspec.yaml | 2 +- 6 files changed, 171 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae9c77..5d49d6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,17 @@ that can be found in the LICENSE file. --> See the [Migration Guide](guides/migration_guide.md) for the details of breaking changes between versions. -## Unreleased +## 4.0.3 ### Fixes - Prevent duplicate shooting actions. +### Improvements + +- Provide overall invalid wrapping for controller methods. +- Throw exceptions with more accurate stack traces. + ## 4.0.2 ### Fixes diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 432c6f0..72c0893 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: wechat_camera_picker_demo description: A new Flutter project. -version: 4.0.2+27 +version: 4.0.3+28 publish_to: none environment: diff --git a/lib/src/internals/methods.dart b/lib/src/internals/methods.dart index fecfedb..72c53c5 100644 --- a/lib/src/internals/methods.dart +++ b/lib/src/internals/methods.dart @@ -18,14 +18,14 @@ void realDebugPrint(dynamic message) { void handleErrorWithHandler( Object e, - CameraErrorHandler? handler, { - StackTrace? s, -}) { + StackTrace s, + CameraErrorHandler? handler, +) { if (handler != null) { handler(e, s); return; } - throw e; + Error.throwWithStackTrace(e, s); } T? ambiguate(T value) => value; diff --git a/lib/src/states/camera_picker_state.dart b/lib/src/states/camera_picker_state.dart index 2c6c703..e65cc9f 100644 --- a/lib/src/states/camera_picker_state.dart +++ b/lib/src/states/camera_picker_state.dart @@ -201,6 +201,13 @@ class CameraPickerState extends State CameraPickerTextDelegate get textDelegate => Constants.textDelegate; + /// If controller methods were failed to called for camera descriptions, + /// it will be recorded as invalid and never gets called again. + /// + /// 如果相机实例的某个方法调用失败,该方法会被记录并且不会再被调用。 + final invalidControllerMethods = >{}; + bool retriedAfterInvalidInitialize = false; + @override void initState() { super.initState(); @@ -265,6 +272,31 @@ class CameraPickerState extends State return scale; } + /// Wraps [CameraController] methods with invalid controls. + /// Returns the [fallback] value if invalid and [T] is non-void. + /// + /// 对于 [CameraController] 的方法增加是否无效的控制。 + /// 如果 [T] 是非 void 且方法无效,返回 [fallback]。 + Future wrapControllerMethod( + String key, + Future Function() method, { + CameraDescription? description, + VoidCallback? onError, + T? fallback, + }) async { + description ??= currentCamera; + if (invalidControllerMethods[description]!.contains(key)) { + return fallback!; + } + try { + return await method(); + } catch (e) { + invalidControllerMethods[description]!.add(key); + onError?.call(); + rethrow; + } + } + /// Initialize cameras instances. /// 初始化相机实例 Future initCameras([CameraDescription? cameraDescription]) async { @@ -308,6 +340,7 @@ class CameraPickerState extends State 'No CameraDescription found.', 'No cameras are available in the controller.', ), + StackTrace.current, pickerConfig.onError, ); } @@ -325,8 +358,10 @@ class CameraPickerState extends State index = currentCameraIndex; } // Initialize the controller with the given resolution preset. + final description = cameraDescription ?? cameras[index]; + invalidControllerMethods[description] ??= {}; final CameraController newController = CameraController( - cameraDescription ?? cameras[index], + description, pickerConfig.resolutionPreset, enableAudio: enableAudio, imageFormatGroup: pickerConfig.imageFormatGroup, @@ -352,44 +387,79 @@ class CameraPickerState extends State ..start(); await Future.wait( >[ - newController - .getExposureOffsetStepSize() - .then((double value) => exposureStep = value) - .catchError((_) => exposureStep), - newController - .getMaxExposureOffset() - .then((double value) => maxAvailableExposureOffset = value) - .catchError((_) => maxAvailableExposureOffset), - newController - .getMinExposureOffset() - .then((double value) => minAvailableExposureOffset = value) - .catchError((_) => minAvailableExposureOffset), - newController - .getMaxZoomLevel() - .then((double value) => maxAvailableZoom = value) - .catchError((_) => maxAvailableZoom), - newController - .getMinZoomLevel() - .then((double value) => minAvailableZoom = value) - .catchError((_) => minAvailableZoom), + wrapControllerMethod( + 'getExposureOffsetStepSize', + () => newController.getExposureOffsetStepSize(), + description: description, + fallback: exposureStep, + ).then((value) => exposureStep = value), + wrapControllerMethod( + 'getMaxExposureOffset', + () => newController.getMaxExposureOffset(), + description: description, + fallback: maxAvailableExposureOffset, + ).then((value) => maxAvailableExposureOffset = value), + wrapControllerMethod( + 'getMinExposureOffset', + () => newController.getMinExposureOffset(), + description: description, + fallback: minAvailableExposureOffset, + ).then((value) => minAvailableExposureOffset = value), + wrapControllerMethod( + 'getMaxZoomLevel', + () => newController.getMaxZoomLevel(), + description: description, + fallback: maxAvailableZoom, + ).then((value) => maxAvailableZoom = value), + wrapControllerMethod( + 'getMinZoomLevel', + () => newController.getMinZoomLevel(), + description: description, + fallback: minAvailableZoom, + ).then((value) => minAvailableZoom = value), + wrapControllerMethod( + 'getMinZoomLevel', + () => newController.getMinZoomLevel(), + description: description, + fallback: minAvailableZoom, + ).then((value) => minAvailableZoom = value), if (pickerConfig.lockCaptureOrientation != null) - newController - .lockCaptureOrientation(pickerConfig.lockCaptureOrientation) - .catchError((_) {}), - if (pickerConfig.preferredFlashMode != FlashMode.auto) - newController - .setFlashMode(pickerConfig.preferredFlashMode) - .catchError((_) { - validFlashModes[currentCamera] - ?.remove(pickerConfig.preferredFlashMode); - }), + wrapControllerMethod( + 'lockCaptureOrientation', + () => newController.lockCaptureOrientation( + pickerConfig.lockCaptureOrientation, + ), + description: description, + ), + // Do not set flash modes for the front camera. + if (description.lensDirection != CameraLensDirection.front && + pickerConfig.preferredFlashMode != FlashMode.auto) + wrapControllerMethod( + 'setFlashMode', + () => newController.setFlashMode( + pickerConfig.preferredFlashMode, + ), + description: description, + onError: () { + validFlashModes[description]?.remove( + pickerConfig.preferredFlashMode, + ); + }, + ), ], + eagerError: false, ); stopwatch.stop(); realDebugPrint("${stopwatch.elapsed} for config's update."); innerController = newController; } catch (e, s) { - handleErrorWithHandler(e, pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, pickerConfig.onError); + if (!retriedAfterInvalidInitialize) { + retriedAfterInvalidInitialize = true; + Future.delayed(Duration.zero, initCameras); + } else { + retriedAfterInvalidInitialize = false; + } } finally { safeSetState(() {}); } @@ -449,6 +519,7 @@ class CameraPickerState extends State 'No FlashMode found.', 'No flash modes are available with the camera.', ), + StackTrace.current, pickerConfig.onError, ); return; @@ -467,7 +538,7 @@ class CameraPickerState extends State // Remove the flash mode that throws an exception. validFlashModes[currentCamera]!.remove(nextFlashMode); switchFlashesMode(value); - handleErrorWithHandler(e, pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, pickerConfig.onError); } } @@ -489,7 +560,7 @@ class CameraPickerState extends State try { await controller.setZoomLevel(currentZoom); } catch (e, s) { - handleErrorWithHandler(e, pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, pickerConfig.onError); } } @@ -550,7 +621,7 @@ class CameraPickerState extends State try { await controller.setExposureMode(newMode); } catch (e, s) { - handleErrorWithHandler(e, pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, pickerConfig.onError); } restartExposureModeDisplayTimer(); restartExposureFadeOutTimer(); @@ -582,9 +653,16 @@ class CameraPickerState extends State isFocusPointFadeOut.value = false; try { await Future.wait(>[ + wrapControllerMethod( + 'setExposureOffset', + () => controller.setExposureOffset(0), + ), controller.setExposureOffset(0), if (controller.value.exposureMode == ExposureMode.locked) - controller.setExposureMode(ExposureMode.auto), + wrapControllerMethod( + 'setExposureMode', + () => controller.setExposureMode(ExposureMode.auto), + ), ]); final Offset newPoint = lastExposurePoint.value!.scale( 1 / constraints.maxWidth, @@ -597,13 +675,14 @@ class CameraPickerState extends State controller.setFocusPoint(newPoint), ]); } catch (e, s) { - handleErrorWithHandler(e, pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, pickerConfig.onError); } } /// Update the exposure offset using the exposure controller. /// 使用曝光控制器更新曝光值 Future updateExposureOffset(double value) async { + final previousSliderOffsetValue = currentExposureSliderOffset.value; currentExposureSliderOffset.value = value; // Normalize the new exposure value if exposures have steps. if (exposureStep > 0) { @@ -621,18 +700,26 @@ class CameraPickerState extends State value > maxAvailableExposureOffset) { return; } + final previousOffsetValue = currentExposureOffset.value; currentExposureOffset.value = value; + bool hasError = false; try { realDebugPrint('Updating the exposure offset value: $value'); // Use [CameraPlatform] explicitly to reduce channel calls. - await CameraPlatform.instance.setExposureOffset( - controller.cameraId, - value, + await wrapControllerMethod( + 'setExposureOffset', + () => CameraPlatform.instance.setExposureOffset( + controller.cameraId, + value, + ), ); } catch (e, s) { - handleErrorWithHandler(e, pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, pickerConfig.onError); + hasError = true; + currentExposureSliderOffset.value = previousSliderOffsetValue; + currentExposureOffset.value = previousOffsetValue; } - if (!isFocusPointDisplays.value) { + if (!hasError && !isFocusPointDisplays.value) { isFocusPointDisplays.value = true; } restartExposurePointDisplayTimer(); @@ -683,6 +770,7 @@ class CameraPickerState extends State if (!controller.value.isInitialized) { handleErrorWithHandler( StateError('Camera has not initialized.'), + StackTrace.current, pickerConfig.onError, ); } @@ -693,12 +781,18 @@ class CameraPickerState extends State final ExposureMode previousExposureMode = controller.value.exposureMode; try { await Future.wait(>[ - controller.setFocusMode(FocusMode.locked).catchError((e, s) { - handleErrorWithHandler(e, pickerConfig.onError, s: s); + wrapControllerMethod( + 'setFocusMode', + () => controller.setFocusMode(FocusMode.locked), + ).catchError((e, s) { + handleErrorWithHandler(e, s, pickerConfig.onError); }), if (previousExposureMode != ExposureMode.locked) - controller.setExposureMode(ExposureMode.locked).catchError((e, s) { - handleErrorWithHandler(e, pickerConfig.onError, s: s); + wrapControllerMethod( + 'setExposureMode', + () => controller.setExposureMode(ExposureMode.locked), + ).catchError((e, s) { + handleErrorWithHandler(e, s, pickerConfig.onError); }), ]); final XFile file = await controller.takePicture(); @@ -719,13 +813,19 @@ class CameraPickerState extends State return; } await Future.wait(>[ - controller.setFocusMode(FocusMode.auto), + wrapControllerMethod( + 'setFocusMode', + () => controller.setFocusMode(FocusMode.auto), + ), if (previousExposureMode != ExposureMode.locked) - controller.setExposureMode(previousExposureMode), + wrapControllerMethod( + 'setExposureMode', + () => controller.setExposureMode(previousExposureMode), + ), ]); await controller.resumePreview(); } catch (e, s) { - handleErrorWithHandler(e, pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, pickerConfig.onError); } finally { isControllerBusy = false; safeSetState(() {}); @@ -788,7 +888,7 @@ class CameraPickerState extends State } catch (e, s) { isControllerBusy = false; if (!controller.value.isRecordingVideo) { - handleErrorWithHandler(e, pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, pickerConfig.onError); return; } try { @@ -796,7 +896,7 @@ class CameraPickerState extends State } catch (e, s) { recordCountdownTimer?.cancel(); isShootingButtonAnimate = false; - handleErrorWithHandler(e, pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, pickerConfig.onError); } recordStopwatch.stop(); } finally { @@ -845,7 +945,7 @@ class CameraPickerState extends State await controller.resumePreview(); } } catch (e, s) { - handleErrorWithHandler(e, pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, pickerConfig.onError); handleError(); initCameras(); } finally { @@ -1464,6 +1564,7 @@ class CameraPickerState extends State preview = Stack( children: [ preview, + // Image.asset('assets/1.jpg', fit: BoxFit.cover), Positioned.fill( child: ExcludeSemantics( child: RotatedBox( diff --git a/lib/src/states/camera_picker_viewer_state.dart b/lib/src/states/camera_picker_viewer_state.dart index ca630f7..ac9f1ed 100644 --- a/lib/src/states/camera_picker_viewer_state.dart +++ b/lib/src/states/camera_picker_viewer_state.dart @@ -81,7 +81,7 @@ class CameraPickerViewerState extends State { } catch (e, s) { hasErrorWhenInitializing = true; realDebugPrint('Error when initializing video controller: $e'); - handleErrorWithHandler(e, onError, s: s); + handleErrorWithHandler(e, s, onError); } finally { if (mounted) { setState(() {}); @@ -117,7 +117,7 @@ class CameraPickerViewerState extends State { } } } catch (e, s) { - handleErrorWithHandler(e, onError, s: s); + handleErrorWithHandler(e, s, onError); } } @@ -140,7 +140,7 @@ class CameraPickerViewerState extends State { File(widget.previewXFile.path), ); } catch (e, s) { - handleErrorWithHandler(e, widget.pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, onError); } isSavingEntity = false; if (mounted) { @@ -177,11 +177,12 @@ class CameraPickerViewerState extends State { StateError( 'Permission is not fully granted to save the captured file.', ), - widget.pickerConfig.onError, + StackTrace.current, + onError, ); } catch (e, s) { realDebugPrint('Saving entity failed: $e'); - handleErrorWithHandler(e, widget.pickerConfig.onError, s: s); + handleErrorWithHandler(e, s, onError); } finally { isSavingEntity = false; if (mounted) { diff --git a/pubspec.yaml b/pubspec.yaml index 19d9938..f16f738 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: wechat_camera_picker -version: 4.0.2 +version: 4.0.3 description: | A camera picker for Flutter projects based on WeChat's UI, which is also a separate runnable extension to the