diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 68188d6510ff..b3ae7bface73 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.1 + +* Added functions to pause and resume the camera preview + ## 0.9.0 * Complete rewrite of Android plugin to fix many capture, focus, flash, orientation and exposure issues. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 4724d22a1bcd..da54cffdf51a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -125,6 +125,8 @@ class Camera private MediaRecorder mediaRecorder; /** True when recording video. */ private boolean recordingVideo; + /** True when the preview is paused. */ + private boolean pausedPreview; private File captureFile; @@ -424,8 +426,10 @@ private void refreshPreviewCaptureSession( } try { - captureSession.setRepeatingRequest( - previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + if (!pausedPreview) { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + } if (onSuccessCallback != null) { onSuccessCallback.run(); @@ -830,33 +834,36 @@ public void setFocusMode(final Result result, @NonNull FocusMode newMode) { * For focus mode an extra step of actually locking/unlocking the * focus has to be done, in order to ensure it goes into the correct state. */ - switch (newMode) { - case locked: - // Perform a single focus trigger. - lockAutoFocus(); - if (captureSession == null) { - Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); - return; - } - - // Set AF state to idle again. - previewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - - try { - captureSession.setRepeatingRequest( - previewRequestBuilder.build(), null, backgroundHandler); - } catch (CameraAccessException e) { - if (result != null) { - result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + if (!pausedPreview) { + switch (newMode) { + case locked: + // Perform a single focus trigger. + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } + lockAutoFocus(); + + // Set AF state to idle again. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + if (result != null) { + result.error( + "setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + } + return; } - return; - } - break; - case auto: - // Cancel current AF trigger and set AF to idle again. - unlockAutoFocus(); - break; + break; + case auto: + // Cancel current AF trigger and set AF to idle again. + unlockAutoFocus(); + break; + } } if (result != null) { @@ -962,6 +969,19 @@ public void unlockCaptureOrientation() { cameraFeatures.getSensorOrientation().unlockCaptureOrientation(); } + /** Pause the preview from dart. */ + public void pausePreview() throws CameraAccessException { + this.pausedPreview = true; + this.captureSession.stopRepeating(); + } + + /** Resume the preview from dart. */ + public void resumePreview() throws CameraAccessException { + this.pausedPreview = false; + this.refreshPreviewCaptureSession( + null, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; Log.i(TAG, "startPreview"); @@ -1018,8 +1038,8 @@ public void onError(String errorCode, String errorMessage) { private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { imageStreamReader.setOnImageAvailableListener( reader -> { - // Use acquireNextImage since image reader is only for one image. Image img = reader.acquireNextImage(); + // Use acquireNextImage since image reader is only for one image. if (img == null) return; List> planes = new ArrayList<>(); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 893785f1a58f..80608ef3bd2b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -339,6 +339,26 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } break; } + case "pausePreview": + { + try { + camera.pausePreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "resumePreview": + { + try { + camera.resumePreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } case "dispose": { if (camera != null) { diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 2314aecbece3..364f59d81356 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -530,7 +530,16 @@ class _CameraExampleHomeState extends State cameraController.value.isRecordingVideo ? onStopButtonPressed : null, - ) + ), + IconButton( + icon: const Icon(Icons.pause_presentation), + color: + cameraController != null && cameraController.value.isPreviewPaused + ? Colors.red + : Colors.blue, + onPressed: + cameraController == null ? null : onPausePreviewButtonPressed, + ), ], ); } @@ -747,6 +756,23 @@ class _CameraExampleHomeState extends State }); } + Future onPausePreviewButtonPressed() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isPreviewPaused) { + await cameraController.resumePreview(); + } else { + await cameraController.pausePreview(); + } + + if (mounted) setState(() {}); + } + void onPauseButtonPressed() { pauseVideoRecording().then((_) { if (mounted) setState(() {}); diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index d88eb45945fe..1f5e369a9b11 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -330,6 +330,7 @@ @interface FLTCam : NSObject isRecordingVideo && _isRecordingPaused; @@ -150,6 +159,8 @@ class CameraValue { DeviceOrientation? deviceOrientation, Optional? lockedCaptureOrientation, Optional? recordingOrientation, + bool? isPreviewPaused, + Optional? previewPauseOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -172,6 +183,10 @@ class CameraValue { recordingOrientation: recordingOrientation == null ? this.recordingOrientation : recordingOrientation.orNull, + isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, ); } @@ -190,7 +205,9 @@ class CameraValue { 'focusPointSupported: $focusPointSupported, ' 'deviceOrientation: $deviceOrientation, ' 'lockedCaptureOrientation: $lockedCaptureOrientation, ' - 'recordingOrientation: $recordingOrientation)'; + 'recordingOrientation: $recordingOrientation, ' + 'isPreviewPaused: $isPreviewPaused, ' + 'previewPausedOrientation: $previewPauseOrientation)'; } } @@ -325,6 +342,35 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.prepareForVideoRecording(); } + /// Pauses the current camera preview + Future pausePreview() async { + if (value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.pausePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: true, + previewPauseOrientation: Optional.of(this.value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resumes the current camera preview + Future resumePreview() async { + if (!value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.resumePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: false, previewPauseOrientation: Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Captures an image and returns the file where it was saved. /// /// Throws a [CameraException] if the capture fails. diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index 1df9f8e2e393..6a15896bfa47 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -71,7 +71,8 @@ class CameraPreview extends StatelessWidget { DeviceOrientation _getApplicableOrientation() { return controller.value.isRecordingVideo ? controller.value.recordingOrientation! - : (controller.value.lockedCaptureOrientation ?? + : (controller.value.previewPauseOrientation ?? + controller.value.lockedCaptureOrientation ?? controller.value.deviceOrientation); } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index a7c6a61a4ef2..0d2e7303536b 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.0 +version: 0.9.1 environment: sdk: ">=2.12.0 <3.0.0" @@ -20,7 +20,7 @@ flutter: pluginClass: CameraPlugin dependencies: - camera_platform_interface: ^2.0.0 + camera_platform_interface: ^2.1.0 flutter: sdk: flutter pedantic: ^1.10.0 diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 8275461192b4..14afddaea070 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -113,6 +113,12 @@ class FakeController extends ValueNotifier @override Future unlockCaptureOrientation() async {} + + @override + Future pausePreview() async {} + + @override + Future resumePreview() async {} } void main() { diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index e0378cca2cb9..4718d8943c34 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -29,6 +29,8 @@ void main() { lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, focusPointSupported: true, + isPreviewPaused: false, + previewPauseOrientation: DeviceOrientation.portraitUp, ); expect(cameraValue, isA()); @@ -46,6 +48,8 @@ void main() { expect( cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp); expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.isPreviewPaused, false); + expect(cameraValue.previewPauseOrientation, DeviceOrientation.portraitUp); }); test('Can be created as uninitialized', () { @@ -66,6 +70,8 @@ void main() { expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); + expect(cameraValue.isPreviewPaused, isFalse); + expect(cameraValue.previewPauseOrientation, null); }); test('Can be copied with isInitialized', () { @@ -87,6 +93,8 @@ void main() { expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); + expect(cameraValue.isPreviewPaused, isFalse); + expect(cameraValue.previewPauseOrientation, null); }); test('Has aspectRatio after setting size', () { @@ -117,25 +125,26 @@ void main() { test('toString() works as expected', () { var cameraValue = const CameraValue( - isInitialized: false, - errorDescription: null, - previewSize: Size(10, 10), - isRecordingPaused: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - flashMode: FlashMode.auto, - exposureMode: ExposureMode.auto, - focusMode: FocusMode.auto, - exposurePointSupported: true, - focusPointSupported: true, - deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: DeviceOrientation.portraitUp, - recordingOrientation: DeviceOrientation.portraitUp, - ); + isInitialized: false, + errorDescription: null, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + focusMode: FocusMode.auto, + exposurePointSupported: true, + focusPointSupported: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + isPreviewPaused: true, + previewPauseOrientation: DeviceOrientation.portraitUp); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp)'); }); }); }