diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index b768213cd621..47f4874e311a 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.8 + +* Add feature to set exposure compensation value (brightness). + ## 0.5.7+4 * Add `pedantic` to dev_dependency. diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 0fcda278d836..648f342dafa0 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -22,6 +22,7 @@ import android.media.ImageReader; import android.media.MediaRecorder; import android.os.Build; +import android.util.Range; import android.util.Size; import android.view.OrientationEventListener; import android.view.Surface; @@ -40,6 +41,8 @@ import java.util.Map; public class Camera { + private static final int DEFAULT_MIN_COMPENSATION = -10; + private static final int DEFAULT_MAX_COMPENSATION = 10; private final SurfaceTextureEntry flutterTexture; private final CameraManager cameraManager; private final OrientationEventListener orientationEventListener; @@ -320,6 +323,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) { cameraCaptureSession = session; captureRequestBuilder.set( CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); if (onSuccessCallback != null) { onSuccessCallback.run(); @@ -475,6 +479,55 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i null); } + public void applyExposureCompensation(@NonNull final Result result, int value) { + try { + applyExposureCompensationRequest(captureRequestBuilder, value); + + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + + result.success(null); + } catch (Exception e) { + result.error("cameraExposureCompensationFailed", e.getMessage(), null); + } + } + + public double getMaxExposureCompensation() { + Range exposureCompensationRange = getExposureCompensationRange(); + return exposureCompensationRange.getUpper().doubleValue(); + } + + public double getMinExposureCompensation() { + Range exposureCompensationRange = getExposureCompensationRange(); + return exposureCompensationRange.getLower().doubleValue(); + } + + private Range getExposureCompensationRange() { + Range range = Range.create(DEFAULT_MIN_COMPENSATION, DEFAULT_MAX_COMPENSATION); + + try { + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); + range = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + } catch (CameraAccessException e) { + //In case of error we will send default range + e.printStackTrace(); + } + + return range; + } + + private void applyExposureCompensationRequest(CaptureRequest.Builder builderRequest, int value) { + Range exposureCompensationRange = getExposureCompensationRange(); + if (value > exposureCompensationRange.getUpper()) { + builderRequest.set( + CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureCompensationRange.getUpper()); + } else if (value < exposureCompensationRange.getLower()) { + builderRequest.set( + CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureCompensationRange.getLower()); + } else { + builderRequest.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, value); + } + } + private void closeCaptureSession() { if (cameraCaptureSession != null) { cameraCaptureSession.close(); diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index cb58d19a9a02..4d7fce4aaf35 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -123,6 +123,21 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } break; } + case "applyExposureCompensation": + { + camera.applyExposureCompensation(result, call.argument("exposureCompensation")); + break; + } + case "getMaxExposureCompensation": + { + result.success(camera.getMaxExposureCompensation()); + break; + } + case "getMinExposureCompensation": + { + result.success(camera.getMinExposureCompensation()); + break; + } case "dispose": { if (camera != null) { diff --git a/packages/camera/example/lib/main.dart b/packages/camera/example/lib/main.dart index ce8d37457123..5a69177a7647 100644 --- a/packages/camera/example/lib/main.dart +++ b/packages/camera/example/lib/main.dart @@ -37,12 +37,19 @@ void logError(String code, String message) => class _CameraExampleHomeState extends State with WidgetsBindingObserver { + static const double initialExposureCompensation = 0.0; + static const double initialMinExposureCompensation = -10.0; + static const double initialMaxExposureCompensation = 10.0; CameraController controller; String imagePath; String videoPath; VideoPlayerController videoController; VoidCallback videoPlayerListener; bool enableAudio = true; + double exposureCompensation = initialExposureCompensation; + double minExposureCompensation = initialMinExposureCompensation; + double maxExposureCompensation = initialMaxExposureCompensation; + bool isMinMaxExposureCompensationSet = false; @override void initState() { @@ -103,6 +110,7 @@ class _CameraExampleHomeState extends State ), _captureControlRowWidget(), _toggleAudioWidget(), + Center(child: _changeExposureCompensationWidget()), Padding( padding: const EdgeInsets.all(5.0), child: Row( @@ -269,6 +277,45 @@ class _CameraExampleHomeState extends State return Row(children: toggles); } + // Display an exposure compensation slider to change the value of it. + Widget _changeExposureCompensationWidget() { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Slider( + value: exposureCompensation, + min: minExposureCompensation, + max: maxExposureCompensation, + onChanged: (value) { + exposureCompensation = value; + + controller + .applyExposureCompensation( + exposureValue: exposureCompensation.toInt()) + .then((value) async { + //We should get and set the device camera's min and max exposure target bias once if we have not set it yet. + if (!isMinMaxExposureCompensationSet) { + minExposureCompensation = + await controller.getMinExposureCompensation(); + maxExposureCompensation = + await controller.getMaxExposureCompensation(); + isMinMaxExposureCompensationSet = true; + if (mounted) { + setState(() { + /* We should set state to refresh the ui and see the changed min and max values for the slider */ + }); + } + } + }); + if (mounted) { + setState(() { + /* We should set state to refresh the ui and see the changed compensation effect in the camera view */ + }); + } + }, + ), + ); + } + String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); void showInSnackBar(String message) { @@ -287,7 +334,12 @@ class _CameraExampleHomeState extends State // If the controller is updated then update the UI. controller.addListener(() { - if (mounted) setState(() {}); + if (mounted) { + setState(() { + //Reset the exposure compensation when the user switches between cameras. + exposureCompensation = 0; + }); + } if (controller.value.hasError) { showInSnackBar('Camera error ${controller.value.errorDescription}'); } diff --git a/packages/camera/example/test_driver/camera_e2e.dart b/packages/camera/example/test_driver/camera_e2e.dart index a1cc8ad9ca02..53fdba8e5785 100644 --- a/packages/camera/example/test_driver/camera_e2e.dart +++ b/packages/camera/example/test_driver/camera_e2e.dart @@ -235,4 +235,68 @@ void main() { }, skip: !Platform.isAndroid, ); + + // This tests that the minimum exposure compensation is always a negative value. + // Returns whether the minimum value is negative or not. + Future testGettingMinimumExposureCompensation( + CameraController controller) async { + print( + 'Getting minimum exposure compensation of camera ${controller.description.name}'); + + // Get minimum exposure compensation + double minimumExposureCompensation = + await controller.getMinExposureCompensation(); + + // Verify minimum exposure compensation is negative + expect(minimumExposureCompensation, isNegative); + return minimumExposureCompensation < 0; + } + + testWidgets('Get minimum exposure compensation', (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + for (CameraDescription cameraDescription in cameras) { + final CameraController controller = + CameraController(cameraDescription, ResolutionPreset.medium); + await controller.initialize(); + final bool isSuccess = + await testGettingMinimumExposureCompensation(controller); + assert(isSuccess); + await controller.dispose(); + } + }, skip: !Platform.isAndroid); + + // This tests that the maximum exposure compensation is always a positive value. + // Returns whether the maximum value is positive or not. + Future testGettingMaximumExposureCompensation( + CameraController controller) async { + print( + 'Getting maximum exposure compensation of camera ${controller.description.name}'); + + // Get maximum exposure compensation + double maximumExposureCompensation = + await controller.getMaxExposureCompensation(); + + // Verify maximum exposure compensation is positive + expect(maximumExposureCompensation, isPositive); + return maximumExposureCompensation > 0; + } + + testWidgets('Get maximum exposure compensation', (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + for (CameraDescription cameraDescription in cameras) { + final CameraController controller = + CameraController(cameraDescription, ResolutionPreset.medium); + await controller.initialize(); + final bool isSuccess = + await testGettingMaximumExposureCompensation(controller); + assert(isSuccess); + await controller.dispose(); + } + }, skip: !Platform.isAndroid); } diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index 42cdb6d5fdf9..e9b40002d1a0 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -162,6 +162,7 @@ @interface FLTCam : NSObject { } } + /// Set the exposure compensation value + /// + /// Throws a [CameraException] if setting exposure compensation fails. + Future applyExposureCompensation({int exposureValue = 0}) async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController.', + 'applyExposureCompensation was called on uninitialized CameraController', + ); + } + + try { + await _channel.invokeMethod('applyExposureCompensation', + {'exposureCompensation': exposureValue}); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Get the minimum exposure compensation value + /// + /// Throws a [CameraException] if getting minimum exposure + /// target bias fails. + Future getMinExposureCompensation() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController.', + 'getMinExposureCompensation was called on uninitialized CameraController', + ); + } + + try { + return await _channel.invokeMethod('getMinExposureCompensation'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Get the maximum exposure compensation value + /// + /// Throws a [CameraException] if getting maximum exposure + /// target bias fails. + Future getMaxExposureCompensation() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController.', + 'getMaxExposureCompensation was called on uninitialized CameraController', + ); + } + + try { + return await _channel.invokeMethod('getMaxExposureCompensation'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Releases the resources of this camera. @override Future dispose() async { diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml index 26fc4e53a9c8..6b5b45659562 100644 --- a/packages/camera/pubspec.yaml +++ b/packages/camera/pubspec.yaml @@ -2,7 +2,7 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.5.7+4 +version: 0.5.8 homepage: https://github.com/flutter/plugins/tree/master/packages/camera