From c9f7e2155024206a6b8d5a3fc115910a3587b5e4 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Thu, 24 Dec 2020 15:23:12 +0100 Subject: [PATCH 01/10] Added platform interface methods for setting auto exposure. --- .../camera_platform_interface/lib/src/types/exposure_mode.dart | 0 .../camera_platform_interface/test/types/exposure_mode_test.dart | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart create mode 100644 packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart diff --git a/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart b/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart new file mode 100644 index 000000000000..e69de29bb2d1 From 532b22ff7b71a3eba0fd8eb1dd0911ecb00621dd Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Thu, 24 Dec 2020 15:24:49 +0100 Subject: [PATCH 02/10] Added platform interface methods for setting auto exposure. --- .../camera/example/ios/Flutter/.last_build_id | 1 + .../example/ios/Flutter/Flutter.podspec | 18 ++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../camera_platform_interface/CHANGELOG.md | 4 + .../lib/src/events/camera_event.dart | 24 ++- .../method_channel/method_channel_camera.dart | 59 ++++++ .../platform_interface/camera_platform.dart | 46 ++++- .../lib/src/types/exposure_mode.dart | 36 ++++ .../lib/src/types/types.dart | 1 + .../camera_platform_interface/pubspec.yaml | 2 +- .../test/camera_platform_interface_test.dart | 78 ++++++++ .../test/events/camera_event_test.dart | 67 +++++-- .../method_channel_camera_test.dart | 172 +++++++++++++++++- .../test/types/exposure_mode_test.dart | 32 ++++ 15 files changed, 529 insertions(+), 27 deletions(-) create mode 100644 packages/camera/camera/example/ios/Flutter/.last_build_id create mode 100644 packages/camera/camera/example/ios/Flutter/Flutter.podspec create mode 100644 packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/camera/camera/example/ios/Flutter/.last_build_id b/packages/camera/camera/example/ios/Flutter/.last_build_id new file mode 100644 index 000000000000..1d0b0dc32be3 --- /dev/null +++ b/packages/camera/camera/example/ios/Flutter/.last_build_id @@ -0,0 +1 @@ +5490cb309144ac61a67edda5c46bb18b \ No newline at end of file diff --git a/packages/camera/camera/example/ios/Flutter/Flutter.podspec b/packages/camera/camera/example/ios/Flutter/Flutter.podspec new file mode 100644 index 000000000000..5ca30416bac0 --- /dev/null +++ b/packages/camera/camera/example/ios/Flutter/Flutter.podspec @@ -0,0 +1,18 @@ +# +# NOTE: This podspec is NOT to be published. It is only used as a local source! +# + +Pod::Spec.new do |s| + s.name = 'Flutter' + s.version = '1.0.0' + s.summary = 'High-performance, high-fidelity mobile apps.' + s.description = <<-DESC +Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS. + DESC + s.homepage = 'https://flutter.io' + s.license = { :type => 'MIT' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } + s.ios.deployment_target = '8.0' + s.vendored_frameworks = 'Flutter.framework' +end diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index ea9821e841f9..916d79c70ec1 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.5 + +- Added interface to support automatic exposure. + ## 1.0.4 - Added the torch option to the FlashMode enum, which when implemented indicates the flash light should be turned on continuously. diff --git a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart index ab3d45545f23..590713d04e8b 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import '../../camera_platform_interface.dart'; + /// Generic Event coming from the native side of Camera. /// /// All [CameraEvent]s contain the `cameraId` that originated the event. This @@ -45,6 +47,12 @@ class CameraInitializedEvent extends CameraEvent { /// The height of the preview in pixels. final double previewHeight; + /// The default exposure mode + final ExposureMode exposureMode; + + /// Whether setting exposure points is supported. + final bool exposurePointSupported; + /// Build a CameraInitialized event triggered from the camera represented by /// `cameraId`. /// @@ -54,6 +62,8 @@ class CameraInitializedEvent extends CameraEvent { int cameraId, this.previewWidth, this.previewHeight, + this.exposureMode, + this.exposurePointSupported, ) : super(cameraId); /// Converts the supplied [Map] to an instance of the [CameraInitializedEvent] @@ -61,6 +71,8 @@ class CameraInitializedEvent extends CameraEvent { CameraInitializedEvent.fromJson(Map json) : previewWidth = json['previewWidth'], previewHeight = json['previewHeight'], + exposureMode = deserializeExposureMode(json['exposureMode']), + exposurePointSupported = json['exposurePointSupported'], super(json['cameraId']); /// Converts the [CameraInitializedEvent] instance into a [Map] instance that @@ -69,6 +81,8 @@ class CameraInitializedEvent extends CameraEvent { 'cameraId': cameraId, 'previewWidth': previewWidth, 'previewHeight': previewHeight, + 'exposureMode': serializeExposureMode(exposureMode), + 'exposurePointSupported': exposurePointSupported, }; @override @@ -78,11 +92,17 @@ class CameraInitializedEvent extends CameraEvent { other is CameraInitializedEvent && runtimeType == other.runtimeType && previewWidth == other.previewWidth && - previewHeight == other.previewHeight; + previewHeight == other.previewHeight && + exposureMode == other.exposureMode && + exposurePointSupported == other.exposurePointSupported; @override int get hashCode => - super.hashCode ^ previewWidth.hashCode ^ previewHeight.hashCode; + super.hashCode ^ + previewWidth.hashCode ^ + previewHeight.hashCode ^ + exposureMode.hashCode ^ + exposurePointSupported.hashCode; } /// An event fired when the resolution preset of the camera has changed. diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index 3bf996fedb19..a8ecf2f21332 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_platform_interface/src/utils/utils.dart'; @@ -185,6 +186,62 @@ class MethodChannelCamera extends CameraPlatform { }, ); + @override + Future setExposureMode(int cameraId, ExposureMode mode) => + _channel.invokeMethod( + 'setExposureMode', + { + 'cameraId': cameraId, + 'mode': serializeExposureMode(mode), + }, + ); + + @override + Future setExposurePoint(int cameraId, Point point) { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + return _channel.invokeMethod( + 'setExposurePoint', + { + 'cameraId': cameraId, + 'reset': point == null, + 'x': point?.x, + 'y': point?.y, + }, + ); + } + + @override + Future getMinExposureOffset(int cameraId) => + _channel.invokeMethod( + 'getMinExposureOffset', + {'cameraId': cameraId}, + ); + + @override + Future getMaxExposureOffset(int cameraId) => + _channel.invokeMethod( + 'getMaxExposureOffset', + {'cameraId': cameraId}, + ); + + @override + Future getExposureOffsetStepSize(int cameraId) => + _channel.invokeMethod( + 'getExposureOffsetStepSize', + {'cameraId': cameraId}, + ); + + @override + Future setExposureOffset(int cameraId, double offset) => + _channel.invokeMethod( + 'setExposureOffset', + { + 'cameraId': cameraId, + 'offset': offset, + }, + ); + @override Future getMaxZoomLevel(int cameraId) => _channel.invokeMethod( 'getMaxZoomLevel', @@ -265,6 +322,8 @@ class MethodChannelCamera extends CameraPlatform { cameraId, call.arguments['previewWidth'], call.arguments['previewHeight'], + deserializeExposureMode(call.arguments['exposureMode']), + call.arguments['exposurePointSupported'], )); break; case 'resolution_changed': diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index 6f96079dc55c..292e3b8ac4ff 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -3,9 +3,11 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; +import 'package:camera_platform_interface/src/types/exposure_mode.dart'; import 'package:cross_file/cross_file.dart'; import 'package:flutter/widgets.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -113,6 +115,48 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('setFlashMode() is not implemented.'); } + /// Sets the exposure mode for taking pictures. + Future setExposureMode(int cameraId, ExposureMode mode) { + throw UnimplementedError('setExposureMode() is not implemented.'); + } + + /// Sets the exposure point for automatically determining the exposure value. + Future setExposurePoint(int cameraId, Point point) { + throw UnimplementedError('setExposurePoint() is not implemented.'); + } + + /// Gets the minimum supported exposure offset for the selected camera in EV units. + Future getMinExposureOffset(int cameraId) { + throw UnimplementedError('getMinExposureOffset() is not implemented.'); + } + + /// Gets the maximum supported exposure offset for the selected camera in EV units. + Future getMaxExposureOffset(int cameraId) { + throw UnimplementedError('getMaxExposureOffset() is not implemented.'); + } + + /// Gets the supported step size for exposure offset for the selected camera in EV units. + /// + /// Returns 0 when the camera supports using a free value without stepping. + Future getExposureOffsetStepSize(int cameraId) { + throw UnimplementedError('getMinExposureOffset() is not implemented.'); + } + + /// Sets the exposure offset for the selected camera. + /// + /// The supplied [offset] value should be in EV units. 1 EV unit represents a + /// doubling in brightness. It should be between the minimum and maximum offsets + /// obtained through `getMinExposureOffset` and `getMaxExposureOffset` respectively. + /// Throws a `CameraException` when an illegal offset is supplied. + /// + /// When the supplied [offset] value does not align with the step size obtained + /// through `getExposureStepSize`, it will automatically be rounded to the nearest step. + /// + /// Returns the (rounded) offset value that was set. + Future setExposureOffset(int cameraId, double offset) { + throw UnimplementedError('setExposureOffset() is not implemented.'); + } + /// Gets the maximum supported zoom level for the selected camera. Future getMaxZoomLevel(int cameraId) { throw UnimplementedError('getMaxZoomLevel() is not implemented.'); @@ -126,7 +170,7 @@ abstract class CameraPlatform extends PlatformInterface { /// Set the zoom level for the selected camera. /// /// The supplied [zoom] value should be between 1.0 and the maximum supported - /// zoom level returned by the `getMaxZoomLevel`. Throws an `CameraException` + /// zoom level returned by the `getMaxZoomLevel`. Throws a `CameraException` /// when an illegal zoom level is supplied. Future setZoomLevel(int cameraId, double zoom) { throw UnimplementedError('setZoomLevel() is not implemented.'); diff --git a/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart index e69de29bb2d1..836f53826479 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart @@ -0,0 +1,36 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The possible exposure modes that can be set for a camera. +enum ExposureMode { + /// Automatically determine exposure settings. + auto, + + /// Lock the currently determined exposure settings. + locked, +} + +/// Returns the exposure mode as a String. +String serializeExposureMode(ExposureMode exposureMode) { + switch (exposureMode) { + case ExposureMode.locked: + return 'locked'; + case ExposureMode.auto: + return 'auto'; + default: + throw ArgumentError('Unknown ExposureMode value'); + } +} + +/// Returns the exposure mode for a given String. +ExposureMode deserializeExposureMode(String str) { + switch (str) { + case "locked": + return ExposureMode.locked; + case "auto": + return ExposureMode.auto; + default: + throw ArgumentError('"$str" is not a valid ExposureMode value'); + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart index 3a89a1021e95..bab430eb5a69 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/types.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -6,3 +6,4 @@ export 'camera_description.dart'; export 'resolution_preset.dart'; export 'camera_exception.dart'; export 'flash_mode.dart'; +export 'exposure_mode.dart'; diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 8cb643e84ca6..998fd616d7aa 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the camera plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.0.4 +version: 1.0.5 dependencies: flutter: diff --git a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart index 7a6fc344503f..574fa45e7b81 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -186,6 +186,84 @@ void main() { ); }); + test( + 'Default implementation of setExposureMode() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setExposureMode(1, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setExposurePoint() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setExposurePoint(1, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getMinExposureOffset() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getMinExposureOffset(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getMaxExposureOffset() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getMaxExposureOffset(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getExposureOffsetStepSize() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getExposureOffsetStepSize(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setExposureOffset() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setExposureOffset(1, null), + throwsUnimplementedError, + ); + }); + test( 'Default implementation of startVideoRecording() should throw unimplemented error', () { diff --git a/packages/camera/camera_platform_interface/test/events/camera_event_test.dart b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart index 01b03b8e93a0..1e28fa689383 100644 --- a/packages/camera/camera_platform_interface/test/events/camera_event_test.dart +++ b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/types/exposure_mode.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -10,11 +11,14 @@ void main() { group('CameraInitializedEvent tests', () { test('Constructor should initialize all properties', () { - final event = CameraInitializedEvent(1, 1024, 640); + final event = + CameraInitializedEvent(1, 1024, 640, ExposureMode.auto, true); expect(event.cameraId, 1); expect(event.previewWidth, 1024); expect(event.previewHeight, 640); + expect(event.exposureMode, ExposureMode.auto); + expect(event.exposurePointSupported, true); }); test('fromJson should initialize all properties', () { @@ -22,57 +26,92 @@ void main() { 'cameraId': 1, 'previewWidth': 1024.0, 'previewHeight': 640.0, + 'exposureMode': 'auto' }); expect(event.cameraId, 1); expect(event.previewWidth, 1024); expect(event.previewHeight, 640); + expect(event.exposureMode, ExposureMode.auto); }); test('toJson should return a map with all fields', () { - final event = CameraInitializedEvent(1, 1024, 640); + final event = + CameraInitializedEvent(1, 1024, 640, ExposureMode.auto, true); final jsonMap = event.toJson(); - expect(jsonMap.length, 3); + expect(jsonMap.length, 5); expect(jsonMap['cameraId'], 1); expect(jsonMap['previewWidth'], 1024); expect(jsonMap['previewHeight'], 640); + expect(jsonMap['exposureMode'], 'auto'); + expect(jsonMap['exposurePointSupported'], true); }); test('equals should return true if objects are the same', () { - final firstEvent = CameraInitializedEvent(1, 1024, 640); - final secondEvent = CameraInitializedEvent(1, 1024, 640); + final firstEvent = + CameraInitializedEvent(1, 1024, 640, ExposureMode.auto, true); + final secondEvent = + CameraInitializedEvent(1, 1024, 640, ExposureMode.auto, true); expect(firstEvent == secondEvent, true); }); test('equals should return false if cameraId is different', () { - final firstEvent = CameraInitializedEvent(1, 1024, 640); - final secondEvent = CameraInitializedEvent(2, 1024, 640); + final firstEvent = + CameraInitializedEvent(1, 1024, 640, ExposureMode.auto, true); + final secondEvent = + CameraInitializedEvent(2, 1024, 640, ExposureMode.auto, true); expect(firstEvent == secondEvent, false); }); test('equals should return false if previewWidth is different', () { - final firstEvent = CameraInitializedEvent(1, 1024, 640); - final secondEvent = CameraInitializedEvent(1, 2048, 640); + final firstEvent = + CameraInitializedEvent(1, 1024, 640, ExposureMode.auto, true); + final secondEvent = + CameraInitializedEvent(1, 2048, 640, ExposureMode.auto, true); expect(firstEvent == secondEvent, false); }); test('equals should return false if previewHeight is different', () { - final firstEvent = CameraInitializedEvent(1, 1024, 640); - final secondEvent = CameraInitializedEvent(1, 1024, 980); + final firstEvent = + CameraInitializedEvent(1, 1024, 640, ExposureMode.auto, true); + final secondEvent = + CameraInitializedEvent(1, 1024, 980, ExposureMode.auto, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if exposureMode is different', () { + final firstEvent = + CameraInitializedEvent(1, 1024, 640, ExposureMode.auto, true); + final secondEvent = + CameraInitializedEvent(1, 1024, 640, ExposureMode.locked, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if exposurePointSupported is different', + () { + final firstEvent = + CameraInitializedEvent(1, 1024, 640, ExposureMode.auto, true); + final secondEvent = + CameraInitializedEvent(1, 1024, 640, ExposureMode.auto, false); expect(firstEvent == secondEvent, false); }); test('hashCode should match hashCode of all properties', () { - final event = CameraInitializedEvent(1, 1024, 640); - final expectedHashCode = event.cameraId.hashCode ^ + final event = + CameraInitializedEvent(1, 1024, 640, ExposureMode.auto, true); + final expectedHashCode = event.cameraId ^ event.previewWidth.hashCode ^ - event.previewHeight.hashCode; + event.previewHeight.hashCode ^ + event.exposureMode.hashCode ^ + event.exposurePointSupported.hashCode; expect(event.hashCode, expectedHashCode); }); diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 82b01015e4f4..3413355af9f1 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math'; import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; @@ -118,8 +119,13 @@ void main() { // Act Future initializeFuture = camera.initializeCamera(cameraId); - camera.cameraEventStreamController - .add(CameraInitializedEvent(cameraId, 1920, 1080)); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + )); await initializeFuture; // Assert @@ -151,8 +157,13 @@ void main() { ResolutionPreset.high, ); Future initializeFuture = camera.initializeCamera(cameraId); - camera.cameraEventStreamController - .add(CameraInitializedEvent(cameraId, 1920, 1080)); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + )); await initializeFuture; // Act @@ -188,8 +199,13 @@ void main() { ResolutionPreset.high, ); Future initializeFuture = camera.initializeCamera(cameraId); - camera.cameraEventStreamController - .add(CameraInitializedEvent(cameraId, 1920, 1080)); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + )); await initializeFuture; }); @@ -200,7 +216,13 @@ void main() { final streamQueue = StreamQueue(eventStream); // Emit test events - final event = CameraInitializedEvent(cameraId, 3840, 2160); + final event = CameraInitializedEvent( + cameraId, + 3840, + 2160, + ExposureMode.auto, + true, + ); await camera.handleMethodCall( MethodCall('initialized', event.toJson()), cameraId); @@ -304,8 +326,15 @@ void main() { ResolutionPreset.high, ); Future initializeFuture = camera.initializeCamera(cameraId); - camera.cameraEventStreamController - .add(CameraInitializedEvent(cameraId, 1920, 1080)); + camera.cameraEventStreamController.add( + CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + ), + ); await initializeFuture; }); @@ -496,6 +525,131 @@ void main() { ]); }); + test('Should set the exposure mode', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setExposureMode': null}, + ); + + // Act + await camera.setExposureMode(cameraId, ExposureMode.auto); + await camera.setExposureMode(cameraId, ExposureMode.locked); + + // Assert + expect(channel.log, [ + isMethodCall('setExposureMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setExposureMode', + arguments: {'cameraId': cameraId, 'mode': 'locked'}), + ]); + }); + + test('Should set the exposure point', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setExposurePoint': null}, + ); + + // Act + await camera.setExposurePoint(cameraId, Point(0.5, 0.5)); + await camera.setExposurePoint(cameraId, null); + + // Assert + expect(channel.log, [ + isMethodCall('setExposurePoint', arguments: { + 'cameraId': cameraId, + 'x': 0.5, + 'y': 0.5, + 'reset': false + }), + isMethodCall('setExposurePoint', arguments: { + 'cameraId': cameraId, + 'x': null, + 'y': null, + 'reset': true + }), + ]); + }); + + test('Should get the min exposure offset', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getMinExposureOffset': 2.0}, + ); + + // Act + final minExposureOffset = await camera.getMinExposureOffset(cameraId); + + // Assert + expect(minExposureOffset, 2.0); + expect(channel.log, [ + isMethodCall('getMinExposureOffset', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the max exposure offset', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getMaxExposureOffset': 2.0}, + ); + + // Act + final maxExposureOffset = await camera.getMaxExposureOffset(cameraId); + + // Assert + expect(maxExposureOffset, 2.0); + expect(channel.log, [ + isMethodCall('getMaxExposureOffset', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the exposure offset step size', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getExposureOffsetStepSize': 0.25}, + ); + + // Act + final stepSize = await camera.getExposureOffsetStepSize(cameraId); + + // Assert + expect(stepSize, 0.25); + expect(channel.log, [ + isMethodCall('getExposureOffsetStepSize', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the exposure offset', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setExposureOffset': 0.6}, + ); + + // Act + final actualOffset = await camera.setExposureOffset(cameraId, 0.5); + + // Assert + expect(actualOffset, 0.6); + expect(channel.log, [ + isMethodCall('setExposureOffset', arguments: { + 'cameraId': cameraId, + 'offset': 0.5, + }), + ]); + }); + test('Should build a texture widget as preview widget', () async { // Act Widget widget = camera.buildPreview(cameraId); diff --git a/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart b/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart index e69de29bb2d1..c34c1d7b4157 100644 --- a/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart +++ b/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart @@ -0,0 +1,32 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/types/exposure_mode.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('ExposureMode should contain 2 options', () { + final values = ExposureMode.values; + + expect(values.length, 2); + }); + + test("ExposureMode enum should have items in correct index", () { + final values = ExposureMode.values; + + expect(values[0], ExposureMode.auto); + expect(values[1], ExposureMode.locked); + }); + + test("serializeExposureMode() should serialize correctly", () { + expect(serializeExposureMode(ExposureMode.auto), "auto"); + expect(serializeExposureMode(ExposureMode.locked), "locked"); + }); + + test("deserializeExposureMode() should deserialize correctly", () { + expect(deserializeExposureMode('auto'), ExposureMode.auto); + expect(deserializeExposureMode('locked'), ExposureMode.locked); + }); +} From 1844b2424f17e1b8b96184fc802f0dee2ff6dfe1 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Thu, 24 Dec 2020 15:27:43 +0100 Subject: [PATCH 03/10] Remove workspace files --- .../camera/example/ios/Flutter/.last_build_id | 1 - .../camera/example/ios/Flutter/Flutter.podspec | 18 ------------------ .../xcshareddata/IDEWorkspaceChecks.plist | 8 -------- .../xcshareddata/IDEWorkspaceChecks.plist | 8 -------- 4 files changed, 35 deletions(-) delete mode 100644 packages/camera/camera/example/ios/Flutter/.last_build_id delete mode 100644 packages/camera/camera/example/ios/Flutter/Flutter.podspec delete mode 100644 packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/camera/camera/example/ios/Flutter/.last_build_id b/packages/camera/camera/example/ios/Flutter/.last_build_id deleted file mode 100644 index 1d0b0dc32be3..000000000000 --- a/packages/camera/camera/example/ios/Flutter/.last_build_id +++ /dev/null @@ -1 +0,0 @@ -5490cb309144ac61a67edda5c46bb18b \ No newline at end of file diff --git a/packages/camera/camera/example/ios/Flutter/Flutter.podspec b/packages/camera/camera/example/ios/Flutter/Flutter.podspec deleted file mode 100644 index 5ca30416bac0..000000000000 --- a/packages/camera/camera/example/ios/Flutter/Flutter.podspec +++ /dev/null @@ -1,18 +0,0 @@ -# -# NOTE: This podspec is NOT to be published. It is only used as a local source! -# - -Pod::Spec.new do |s| - s.name = 'Flutter' - s.version = '1.0.0' - s.summary = 'High-performance, high-fidelity mobile apps.' - s.description = <<-DESC -Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS. - DESC - s.homepage = 'https://flutter.io' - s.license = { :type => 'MIT' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } - s.ios.deployment_target = '8.0' - s.vendored_frameworks = 'Flutter.framework' -end diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - From fd8dd401ec7a4c477be53352fcfa0ad4ff35df54 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Thu, 24 Dec 2020 15:51:09 +0100 Subject: [PATCH 04/10] Added auto exposure implementations for Android and iOS --- packages/camera/camera/CHANGELOG.md | 8 + .../io/flutter/plugins/camera/Camera.java | 363 ++++++++++++++-- .../flutter/plugins/camera/DartMessenger.java | 17 +- .../plugins/camera/MethodCallHandlerImpl.java | 68 +++ .../plugins/camera/PictureCaptureRequest.java | 4 +- .../plugins/camera/types/ExposureMode.java | 25 ++ .../plugins/camera/types/FlashMode.java | 26 +- .../plugins/camera/DartMessengerTest.java | 4 +- .../camera/PictureCaptureRequestTest.java | 12 +- .../camera/types/ExposureModeTest.java | 33 ++ .../plugins/camera/types/FlashModeTest.java | 8 + packages/camera/camera/example/lib/main.dart | 316 +++++++++++--- .../camera/camera/ios/Classes/CameraPlugin.m | 121 +++++- packages/camera/camera/lib/camera.dart | 1 + .../camera/lib/src/camera_controller.dart | 176 +++++++- packages/camera/camera/pubspec.yaml | 7 +- packages/camera/camera/test/camera_test.dart | 401 +++++++++++++++++- .../camera/camera/test/camera_value_test.dart | 30 +- 18 files changed, 1478 insertions(+), 142 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index b407b83e35db..a80b8286362a 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.6.4 + +* Adds auto exposure support for Android and iOS implementations. + +## 0.6.3+1 + +* Fixes flash & torch modes not working on some Android devices. + ## 0.6.3 * Adds torch mode as a flash mode for Android and iOS implementations. 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 cae666d6742a..e2c8ff3dc1f6 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 @@ -12,6 +12,7 @@ import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCaptureSession.CaptureCallback; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; @@ -20,6 +21,7 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.MeteringRectangle; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.media.CamcorderProfile; @@ -29,13 +31,19 @@ import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.Looper; +import android.util.Range; +import android.util.Rational; import android.util.Size; import android.view.OrientationEventListener; import android.view.Surface; import androidx.annotation.NonNull; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.PictureCaptureRequest.State; import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.ExposureMode; import io.flutter.plugins.camera.types.FlashMode; import io.flutter.plugins.camera.types.ResolutionPreset; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; @@ -76,7 +84,10 @@ public class Camera { private File videoRecordingFile; private int currentOrientation = ORIENTATION_UNKNOWN; private FlashMode flashMode; + private ExposureMode exposureMode; private PictureCaptureRequest pictureCaptureRequest; + private MeteringRectangle aeMeteringRectangle; + private int exposureOffset; public Camera( final Activity activity, @@ -96,6 +107,8 @@ public Camera( this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); this.applicationContext = activity.getApplicationContext(); this.flashMode = FlashMode.auto; + this.exposureMode = ExposureMode.auto; + this.exposureOffset = 0; orientationEventListener = new OrientationEventListener(activity.getApplicationContext()) { @Override @@ -155,14 +168,15 @@ public void onOpened(@NonNull CameraDevice device) { cameraDevice = device; try { startPreview(); + dartMessenger.sendCameraInitializedEvent( + previewSize.getWidth(), + previewSize.getHeight(), + exposureMode, + isExposurePointSupported()); } catch (CameraAccessException e) { dartMessenger.sendCameraErrorEvent(e.getMessage()); close(); - return; } - - dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), previewSize.getHeight()); } @Override @@ -246,7 +260,7 @@ public void takePicture(@NonNull final Result result) { }, null); - runPicturePreCapture(); + runPictureAutoFocus(); } private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = @@ -256,18 +270,15 @@ public void onCaptureCompleted( @NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { - assert (pictureCaptureRequest != null); - switch (pictureCaptureRequest.getState()) { - case awaitingPreCapture: - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - // Some devices might return null here, in which case we will also continue. - if (aeState == null - || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED - || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - runPictureCapture(); - } - break; - } + processCapture(result); + } + + @Override + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + processCapture(partialResult); } @Override @@ -289,11 +300,54 @@ public void onCaptureFailed( } pictureCaptureRequest.error("captureFailure", reason, null); } + + private void processCapture(CaptureResult result) { + if (pictureCaptureRequest == null) { + return; + } + + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + switch (pictureCaptureRequest.getState()) { + case focusing: + if (afState == null) { + return; + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // Some devices might return null here, in which case we will also continue. + if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + runPictureCapture(); + } else { + runPicturePreCapture(); + } + } + break; + case preCapture: + // Some devices might return null here, in which case we will also continue. + if (aeState == null + || aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE + || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED + || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { + pictureCaptureRequest.setState(State.waitingPreCaptureReady); + } + break; + case waitingPreCaptureReady: + if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { + runPictureCapture(); + } + } + } }; + private void runPictureAutoFocus() { + assert (pictureCaptureRequest != null); + pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing); + lockAutoFocus(); + } + private void runPicturePreCapture() { assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.awaitingPreCapture); + pictureCaptureRequest.setState(PictureCaptureRequest.State.preCapture); captureRequestBuilder.set( CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, @@ -331,7 +385,47 @@ private void runPictureCapture() { CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); break; } - cameraCaptureSession.capture(captureBuilder.build(), pictureCaptureCallback, null); + cameraCaptureSession.stopRepeating(); + cameraCaptureSession.capture( + captureBuilder.build(), + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + unlockAutoFocus(); + } + }, + null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + private void lockAutoFocus() { + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + try { + cameraCaptureSession.capture(captureRequestBuilder.build(), pictureCaptureCallback, null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + private void unlockAutoFocus() { + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + initPreviewCaptureBuilder(); + try { + cameraCaptureSession.capture(captureRequestBuilder.build(), null, null); + } catch (CameraAccessException ignored) { + } + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); + try { + cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), pictureCaptureCallback, null); } catch (CameraAccessException e) { pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } @@ -377,7 +471,10 @@ public void onConfigured(@NonNull CameraCaptureSession session) { } cameraCaptureSession = session; initPreviewCaptureBuilder(); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), + pictureCaptureCallback, + new Handler(Looper.getMainLooper())); if (onSuccessCallback != null) { onSuccessCallback.run(); } @@ -516,30 +613,216 @@ public void resumeVideoRecording(@NonNull final Result result) { public void setFlashMode(@NonNull final Result result, FlashMode mode) throws CameraAccessException { // Get the flash availability - Boolean flashAvailable; - try { - flashAvailable = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - } catch (CameraAccessException e) { - result.error("setFlashModeFailed", e.getMessage(), null); - return; - } + Boolean flashAvailable = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + // Check if flash is available. if (flashAvailable == null || !flashAvailable) { result.error("setFlashModeFailed", "Device does not have flash capabilities", null); return; } + + // If switching directly from torch to auto or on, make sure we turn off the torch. + if (flashMode == FlashMode.torch && mode != FlashMode.torch && mode != FlashMode.off) { + this.flashMode = FlashMode.off; + initPreviewCaptureBuilder(); + this.cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), + new CaptureCallback() { + private boolean isFinished = false; + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult captureResult) { + if (isFinished) { + return; + } + + updateFlash(mode); + result.success(null); + isFinished = true; + } + + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + if (isFinished) { + return; + } + + result.error("setFlashModeFailed", "Could not set flash mode.", null); + isFinished = true; + } + }, + null); + } else { + updateFlash(mode); + result.success(null); + } + } + + private void updateFlash(FlashMode mode) { // Get flash - this.flashMode = mode; + flashMode = mode; + initPreviewCaptureBuilder(); + try { + cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), pictureCaptureCallback, null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + public void setExposureMode(@NonNull final Result result, ExposureMode mode) + throws CameraAccessException { + this.exposureMode = mode; + initPreviewCaptureBuilder(); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + result.success(null); + } + + public void setExposurePoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if exposure point functionality is available. + if (!isExposurePointSupported()) { + result.error( + "setExposurePointFailed", "Device does not have exposure point capabilities", null); + return; + } + // Check if we are doing a reset or not + if (x == null || y == null) { + x = 0.5; + y = 0.5; + } + // Get the current exposure point boundaries. + Size maxBoundaries = getExposureBoundaries(); + if (maxBoundaries == null) { + result.error("setExposurePointFailed", "Could not determine max region boundaries", null); + return; + } + // Interpolate the target coordinate + int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1))); + int targetY = (int) Math.round(y * ((double) (maxBoundaries.getHeight() - 1))); + // Determine the dimensions of the metering triangle (1th of the viewport) + int targetWidth = (int) Math.round(((double) maxBoundaries.getWidth()) / 10d); + int targetHeight = (int) Math.round(((double) maxBoundaries.getHeight()) / 10d); + // Adjust target coordinate to represent top-left corner of metering rectangle + targetX -= targetWidth / 2; + targetY -= targetHeight / 2; + // Adjust target coordinate as to not fall out of bounds + if (targetX < 0) targetX = 0; + if (targetY < 0) targetY = 0; + int maxTargetX = maxBoundaries.getWidth() - 1 - targetWidth; + int maxTargetY = maxBoundaries.getHeight() - 1 - targetHeight; + if (targetX > maxTargetX) targetX = maxTargetX; + if (targetY > maxTargetY) targetY = maxTargetY; + // Set the metering rectangle + aeMeteringRectangle = new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1); + // Apply it initPreviewCaptureBuilder(); this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); result.success(null); } + @SuppressLint("NewApi") + private Size getExposureBoundaries() throws CameraAccessException { + // Check if the device supports distortion correction + boolean supportsDistortionCorrection = false; + if (android.os.Build.VERSION.SDK_INT >= VERSION_CODES.P) { + int[] availableDistortionCorrectionModes = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + if (availableDistortionCorrectionModes == null) + availableDistortionCorrectionModes = new int[0]; + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + supportsDistortionCorrection = nonOffModesSupported > 0; + } + // No distortion correction support + if (!supportsDistortionCorrection) { + return cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + } + // Get the current distortion correction mode + Integer distortionCorrectionMode = + captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + } else { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + } + return rect == null ? null : new Size(rect.width(), rect.height()); + } + + private boolean isExposurePointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + return supportedRegions != null && supportedRegions > 0; + } + + public double getMinExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double minStepped = range == null ? 0 : range.getLower(); + double stepSize = getExposureOffsetStepSize(); + return minStepped * stepSize; + } + + public double getMaxExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double maxStepped = range == null ? 0 : range.getUpper(); + double stepSize = getExposureOffsetStepSize(); + return maxStepped * stepSize; + } + + public double getExposureOffsetStepSize() throws CameraAccessException { + Rational stepSize = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + return stepSize == null ? 0.0 : stepSize.doubleValue(); + } + + public void setExposureOffset(@NonNull final Result result, double offset) + throws CameraAccessException { + // Set the exposure offset + double stepSize = getExposureOffsetStepSize(); + exposureOffset = (int) (offset / stepSize); + // Apply it + initPreviewCaptureBuilder(); + this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + result.success(offset); + } + private void initPreviewCaptureBuilder() { captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO); + // Applying flash modes switch (flashMode) { case off: captureRequestBuilder.set( @@ -563,6 +846,24 @@ private void initPreviewCaptureBuilder() { captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); break; } + // Applying auto exposure + if (aeMeteringRectangle != null) { + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[] {aeMeteringRectangle}); + } + switch (exposureMode) { + case locked: + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + break; + case auto: + default: + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + break; + } + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); + // Applying auto focus + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); } public void startPreview() throws CameraAccessException { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index 49f9d9a76de0..2fee13816b51 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -4,6 +4,7 @@ import androidx.annotation.Nullable; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.camera.types.ExposureMode; import java.util.HashMap; import java.util.Map; @@ -20,13 +21,23 @@ enum EventType { channel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); } - void sendCameraInitializedEvent(Integer previewWidth, Integer previewHeight) { + void sendCameraInitializedEvent( + Integer previewWidth, + Integer previewHeight, + ExposureMode exposureMode, + Boolean exposurePointSupported) { + assert (previewWidth != null); + assert (previewHeight != null); + assert (exposureMode != null); + assert (exposurePointSupported != null); this.send( EventType.INITIALIZED, new HashMap() { { - if (previewWidth != null) put("previewWidth", previewWidth.doubleValue()); - if (previewHeight != null) put("previewHeight", previewHeight.doubleValue()); + put("previewWidth", previewWidth.doubleValue()); + put("previewHeight", previewHeight.doubleValue()); + put("exposureMode", exposureMode.toString()); + put("exposurePointSupported", exposurePointSupported); } }); } 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 704504176518..78a10010f90b 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 @@ -10,6 +10,7 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; +import io.flutter.plugins.camera.types.ExposureMode; import io.flutter.plugins.camera.types.FlashMode; import io.flutter.view.TextureRegistry; import java.util.HashMap; @@ -138,6 +139,73 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } break; } + case "setExposureMode": + { + String modeStr = call.argument("mode"); + ExposureMode mode = ExposureMode.getValueForString(modeStr); + if (mode == null) { + result.error("setExposureModeFailed", "Unknown exposure mode " + modeStr, null); + return; + } + try { + camera.setExposureMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposurePoint": + { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setExposurePoint(result, x, y); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinExposureOffset": + { + try { + result.success(camera.getMinExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMaxExposureOffset": + { + try { + result.success(camera.getMaxExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getExposureOffsetStepSize": + { + try { + result.success(camera.getExposureOffsetStepSize()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureOffset": + { + try { + camera.setExposureOffset(result, call.argument("offset")); + } catch (Exception e) { + handleException(e, result); + } + break; + } case "startImageStream": { try { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index e365f071d9a8..1103b8583ad6 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -7,7 +7,9 @@ class PictureCaptureRequest { enum State { idle, - awaitingPreCapture, + focusing, + preCapture, + waitingPreCaptureReady, capturing, finished, error, diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java new file mode 100644 index 000000000000..8066f59d2b14 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java @@ -0,0 +1,25 @@ +package io.flutter.plugins.camera.types; + +// Mirrors exposure_mode.dart +public enum ExposureMode { + auto("auto"), + locked("locked"); + + private final String strValue; + + ExposureMode(String strValue) { + this.strValue = strValue; + } + + public static ExposureMode getValueForString(String modeStr) { + for (ExposureMode value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java index 99d4915b3a6a..ee6fe489511f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -2,16 +2,26 @@ // Mirrors flash_mode.dart public enum FlashMode { - off, - auto, - always, - torch; + off("off"), + auto("auto"), + always("always"), + torch("torch"); + + private final String strValue; + + FlashMode(String strValue) { + this.strValue = strValue; + } public static FlashMode getValueForString(String modeStr) { - try { - return valueOf(modeStr); - } catch (IllegalArgumentException | NullPointerException e) { - return null; + for (FlashMode value : values()) { + if (value.strValue.equals(modeStr)) return value; } + return null; + } + + @Override + public String toString() { + return strValue; } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index a689f2b6128f..52d9829f7d8f 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -58,7 +58,7 @@ public void sendCameraErrorEvent_includesErrorDescriptions() { @Test public void sendCameraInitializedEvent_includesPreviewSize() { - dartMessenger.sendCameraInitializedEvent(0, 0); + dartMessenger.sendCameraInitializedEvent(0, 0, ExposureMode.auto, true); List sentMessages = fakeBinaryMessenger.getMessages(); assertEquals(1, sentMessages.size()); @@ -66,6 +66,8 @@ public void sendCameraInitializedEvent_includesPreviewSize() { assertEquals("initialized", call.method); assertEquals(0, (double) call.argument("previewWidth"), 0); assertEquals(0, (double) call.argument("previewHeight"), 0); + assertEquals("ExposureMode auto", call.argument("exposureMode"), "auto"); + assertEquals("exposurePointSupported", call.argument("exposurePointSupported"), true); } @Test diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java index 2b6aa0f25fcf..2356b306c6c4 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -20,11 +20,15 @@ public void state_is_idle_by_default() { @Test public void setState_sets_state() { PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.awaitingPreCapture); + req.setState(PictureCaptureRequest.State.focusing); + assertEquals("State is focusing", req.getState(), PictureCaptureRequest.State.focusing); + req.setState(PictureCaptureRequest.State.preCapture); + assertEquals("State is preCapture", req.getState(), PictureCaptureRequest.State.preCapture); + req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); assertEquals( - "State is awaitingPreCapture", + "State is waitingPreCaptureReady", req.getState(), - PictureCaptureRequest.State.awaitingPreCapture); + PictureCaptureRequest.State.waitingPreCaptureReady); req.setState(PictureCaptureRequest.State.capturing); assertEquals( "State is awaitingPreCapture", req.getState(), PictureCaptureRequest.State.capturing); @@ -49,7 +53,7 @@ public void isFinished_is_true_When_state_is_finished_or_error() { // Test false states req.setState(PictureCaptureRequest.State.idle); assertFalse(req.isFinished()); - req.setState(PictureCaptureRequest.State.awaitingPreCapture); + req.setState(PictureCaptureRequest.State.preCapture); assertFalse(req.isFinished()); req.setState(PictureCaptureRequest.State.capturing); assertFalse(req.isFinished()); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java new file mode 100644 index 000000000000..28d2343cedcd --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java @@ -0,0 +1,33 @@ +package io.flutter.plugins.camera.types; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ExposureModeTest { + + @Test + public void getValueForString_returns_correct_values() { + assertEquals( + "Returns ExposureMode.auto for 'auto'", + ExposureMode.getValueForString("auto"), + ExposureMode.auto); + assertEquals( + "Returns ExposureMode.locked for 'locked'", + ExposureMode.getValueForString("locked"), + ExposureMode.locked); + } + + @Test + public void getValueForString_returns_null_for_nonexistant_value() { + assertEquals( + "Returns null for 'nonexistant'", ExposureMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returns_correct_value() { + assertEquals("Returns 'auto' for ExposureMode.auto", ExposureMode.auto.toString(), "auto"); + assertEquals( + "Returns 'locked' for ExposureMode.locked", ExposureMode.locked.toString(), "locked"); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java index d2674e8c7e06..bba01836545a 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java @@ -27,4 +27,12 @@ public void getValueForString_returns_null_for_nonexistant_value() { assertEquals( "Returns null for 'nonexistant'", FlashMode.getValueForString("nonexistant"), null); } + + @Test + public void toString_returns_correct_value() { + assertEquals("Returns 'off' for FlashMode.off", FlashMode.off.toString(), "off"); + assertEquals("Returns 'auto' for FlashMode.auto", FlashMode.auto.toString(), "auto"); + assertEquals("Returns 'always' for FlashMode.always", FlashMode.always.toString(), "always"); + assertEquals("Returns 'torch' for FlashMode.torch", FlashMode.torch.toString(), "torch"); + } } diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index ee8e2c259b3d..c4fa1c5ed01e 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -35,13 +35,20 @@ void logError(String code, String message) => print('Error: $code\nError Message: $message'); class _CameraExampleHomeState extends State - with WidgetsBindingObserver { + with WidgetsBindingObserver, TickerProviderStateMixin { CameraController controller; XFile imageFile; XFile videoFile; VideoPlayerController videoController; VoidCallback videoPlayerListener; bool enableAudio = true; + double _minAvailableExposureOffset = 0.0; + double _maxAvailableExposureOffset = 0.0; + double _currentExposureOffset = 0.0; + AnimationController _flashModeControlRowAnimationController; + Animation _flashModeControlRowAnimation; + AnimationController _exposureModeControlRowAnimationController; + Animation _exposureModeControlRowAnimation; double _minAvailableZoom; double _maxAvailableZoom; double _currentScale = 1.0; @@ -54,11 +61,29 @@ class _CameraExampleHomeState extends State void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + _flashModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _flashModeControlRowAnimation = CurvedAnimation( + parent: _flashModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _exposureModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _exposureModeControlRowAnimation = CurvedAnimation( + parent: _exposureModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); + _flashModeControlRowAnimationController.dispose(); + _exposureModeControlRowAnimationController.dispose(); super.dispose(); } @@ -108,8 +133,7 @@ class _CameraExampleHomeState extends State ), ), _captureControlRowWidget(), - _flashModeRowWidget(), - _toggleAudioWidget(), + _modeControlRowWidget(), Padding( padding: const EdgeInsets.all(5.0), child: Row( @@ -142,11 +166,15 @@ class _CameraExampleHomeState extends State child: Listener( onPointerDown: (_) => _pointers++, onPointerUp: (_) => _pointers--, - child: GestureDetector( - onScaleStart: _handleScaleStart, - onScaleUpdate: _handleScaleUpdate, - child: CameraPreview(controller), - ), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return GestureDetector( + onScaleStart: _handleScaleStart, + onScaleUpdate: _handleScaleUpdate, + onTapDown: (details) => onViewFinderTap(details, constraints), + child: CameraPreview(controller), + ); + }), ), ); } @@ -168,27 +196,6 @@ class _CameraExampleHomeState extends State await controller.setZoomLevel(_currentScale); } - /// Toggle recording audio - Widget _toggleAudioWidget() { - return Padding( - padding: const EdgeInsets.only(left: 25), - child: Row( - children: [ - const Text('Enable Audio:'), - Switch( - value: enableAudio, - onChanged: (bool value) { - enableAudio = value; - if (controller != null) { - onNewCameraSelected(controller.description); - } - }, - ), - ], - ), - ); - } - /// Display the thumbnail of the captured image or video. Widget _thumbnailWidget() { return Expanded( @@ -223,49 +230,156 @@ class _CameraExampleHomeState extends State ); } - /// Display a bar with buttons to change the flash mode - Widget _flashModeRowWidget() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, - children: [ - IconButton( - icon: const Icon(Icons.flash_off), - color: controller?.value?.flashMode == FlashMode.off - ? Colors.orange - : Colors.blue, - onPressed: controller != null - ? () => onFlashModeButtonPressed(FlashMode.off) - : null, - ), - IconButton( - icon: const Icon(Icons.flash_auto), - color: controller?.value?.flashMode == FlashMode.auto - ? Colors.orange - : Colors.blue, - onPressed: controller != null - ? () => onFlashModeButtonPressed(FlashMode.auto) - : null, + /// Display a bar with buttons to change the flash and exposure modes + Widget _modeControlRowWidget() { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + icon: Icon(Icons.flash_on), + color: Colors.blue, + onPressed: controller != null ? onFlashModeButtonPressed : null, + ), + IconButton( + icon: Icon(Icons.exposure), + color: Colors.blue, + onPressed: + controller != null ? onExposureModeButtonPressed : null, + ), + IconButton( + icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), + color: Colors.blue, + onPressed: controller != null ? onAudioModeButtonPressed : null, + ), + ], ), - IconButton( - icon: const Icon(Icons.flash_on), - color: controller?.value?.flashMode == FlashMode.always - ? Colors.orange - : Colors.blue, - onPressed: controller != null - ? () => onFlashModeButtonPressed(FlashMode.always) - : null, + _flashModeControlRowWidget(), + _exposureModeControlRowWidget(), + ], + ); + } + + Widget _flashModeControlRowWidget() { + return SizeTransition( + sizeFactor: _flashModeControlRowAnimation, + child: ClipRect( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + icon: Icon(Icons.flash_off), + color: controller?.value?.flashMode == FlashMode.off + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.off) + : null, + ), + IconButton( + icon: Icon(Icons.flash_auto), + color: controller?.value?.flashMode == FlashMode.auto + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.auto) + : null, + ), + IconButton( + icon: Icon(Icons.flash_on), + color: controller?.value?.flashMode == FlashMode.always + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.always) + : null, + ), + IconButton( + icon: Icon(Icons.highlight), + color: controller?.value?.flashMode == FlashMode.torch + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.torch) + : null, + ), + ], ), - IconButton( - icon: const Icon(Icons.highlight), - color: controller?.value?.flashMode == FlashMode.torch - ? Colors.orange - : Colors.blue, - onPressed: controller != null - ? () => onFlashModeButtonPressed(FlashMode.torch) - : null, + ), + ); + } + + Widget _exposureModeControlRowWidget() { + return SizeTransition( + sizeFactor: _exposureModeControlRowAnimation, + child: ClipRect( + child: Container( + color: Colors.grey.shade50, + child: Column( + children: [ + Center( + child: Text("Exposure Mode"), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + FlatButton( + child: Text('AUTO'), + textColor: + controller?.value?.exposureMode == ExposureMode.auto + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.auto) + : null, + onLongPress: () { + if (controller != null) controller.setExposurePoint(null); + showInSnackBar('Resetting exposure point'); + }, + ), + FlatButton( + child: Text('LOCKED'), + textColor: + controller?.value?.exposureMode == ExposureMode.locked + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.locked) + : null, + ), + ], + ), + Center( + child: Text("Exposure Offset"), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + Text(_minAvailableExposureOffset.toString()), + Slider( + value: _currentExposureOffset, + min: _minAvailableExposureOffset, + max: _maxAvailableExposureOffset, + label: _currentExposureOffset.toString(), + onChanged: _minAvailableExposureOffset == + _maxAvailableExposureOffset + ? null + : setExposureOffset, + ), + Text(_maxAvailableExposureOffset.toString()), + ], + ), + ], + ), ), - ], + ), ); } @@ -353,6 +467,13 @@ class _CameraExampleHomeState extends State _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(message))); } + void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { + controller.setExposurePoint(Offset( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + )); + } + void onNewCameraSelected(CameraDescription cameraDescription) async { if (controller != null) { await controller.dispose(); @@ -373,6 +494,8 @@ class _CameraExampleHomeState extends State try { await controller.initialize(); + _minAvailableExposureOffset = await controller.getMinExposureOffset(); + _maxAvailableExposureOffset = await controller.getMaxExposureOffset(); _maxAvailableZoom = await controller.getMaxZoomLevel(); _minAvailableZoom = await controller.getMinZoomLevel(); } on CameraException catch (e) { @@ -397,13 +520,45 @@ class _CameraExampleHomeState extends State }); } - void onFlashModeButtonPressed(FlashMode mode) { + void onFlashModeButtonPressed() { + if (_flashModeControlRowAnimationController.value == 1) { + _flashModeControlRowAnimationController.reverse(); + } else { + _flashModeControlRowAnimationController.forward(); + _exposureModeControlRowAnimationController.reverse(); + } + } + + void onExposureModeButtonPressed() { + if (_exposureModeControlRowAnimationController.value == 1) { + _exposureModeControlRowAnimationController.reverse(); + } else { + _exposureModeControlRowAnimationController.forward(); + _flashModeControlRowAnimationController.reverse(); + } + } + + void onAudioModeButtonPressed() { + enableAudio = !enableAudio; + if (controller != null) { + onNewCameraSelected(controller.description); + } + } + + void onSetFlashModeButtonPressed(FlashMode mode) { setFlashMode(mode).then((_) { if (mounted) setState(() {}); showInSnackBar('Flash mode set to ${mode.toString().split('.').last}'); }); } + void onSetExposureModeButtonPressed(ExposureMode mode) { + setExposureMode(mode).then((_) { + if (mounted) setState(() {}); + showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}'); + }); + } + void onVideoRecordButtonPressed() { startVideoRecording().then((_) { if (mounted) setState(() {}); @@ -502,6 +657,27 @@ class _CameraExampleHomeState extends State } } + Future setExposureMode(ExposureMode mode) async { + try { + await controller.setExposureMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setExposureOffset(double offset) async { + setState(() { + _currentExposureOffset = offset; + }); + try { + offset = await controller.setExposureOffset(offset); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + Future _startVideoPlayer() async { final VideoPlayerController vController = VideoPlayerController.file(File(videoFile.path)); diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index d54695233bdb..005ec38fcdfa 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -176,6 +176,45 @@ static AVCaptureFlashMode getAVCaptureFlashModeForFlashMode(FlashMode mode) { } } +// Mirrors ExposureMode in camera.dart +typedef enum { + ExposureModeAuto, + ExposureModeLocked, + +} ExposureMode; + +static NSString *getStringForExposureMode(ExposureMode mode) { + switch (mode) { + case ExposureModeAuto: + return @"auto"; + case ExposureModeLocked: + return @"locked"; + } + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown string for exposure mode"] + }]; + @throw error; +} + +static ExposureMode getExposureModeForString(NSString *mode) { + if ([mode isEqualToString:@"auto"]) { + return ExposureModeAuto; + } else if ([mode isEqualToString:@"locked"]) { + return ExposureModeLocked; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown exposure mode %@", mode] + }]; + @throw error; + } +} + // Mirrors ResolutionPreset in camera.dart typedef enum { veryLow, @@ -243,6 +282,7 @@ @interface FLTCam : NSObject *)messenger { if (!_isStreamingImages) { FlutterEventChannel *eventChannel = @@ -1063,7 +1159,10 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re [methodChannel invokeMethod:@"initialized" arguments:@{ @"previewWidth" : @(_camera.previewSize.width), - @"previewHeight" : @(_camera.previewSize.height) + @"previewHeight" : @(_camera.previewSize.height), + @"exposureMode" : getStringForExposureMode([_camera exposureMode]), + @"exposurePointSupported" : + @([_camera.captureDevice isExposurePointOfInterestSupported]), }]; [_camera start]; result(nil); @@ -1098,6 +1197,26 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re [_camera setZoomLevel:zoom Result:result]; } else if ([@"setFlashMode" isEqualToString:call.method]) { [_camera setFlashModeWithResult:result mode:call.arguments[@"mode"]]; + } else if ([@"setExposureMode" isEqualToString:call.method]) { + [_camera setExposureModeWithResult:result mode:call.arguments[@"mode"]]; + } else if ([@"setExposurePoint" isEqualToString:call.method]) { + BOOL reset = ((NSNumber *)call.arguments[@"reset"]).boolValue; + double x = 0.5; + double y = 0.5; + if (!reset) { + x = ((NSNumber *)call.arguments[@"x"]).doubleValue; + y = ((NSNumber *)call.arguments[@"y"]).doubleValue; + } + [_camera setExposurePointWithResult:result x:x y:y]; + } else if ([@"getMinExposureOffset" isEqualToString:call.method]) { + result(@(_camera.captureDevice.minExposureTargetBias)); + } else if ([@"getMaxExposureOffset" isEqualToString:call.method]) { + result(@(_camera.captureDevice.maxExposureTargetBias)); + } else if ([@"getExposureOffsetStepSize" isEqualToString:call.method]) { + result(@(0.0)); + } else if ([@"setExposureOffset" isEqualToString:call.method]) { + [_camera setExposureOffsetWithResult:result + offset:((NSNumber *)call.arguments[@"offset"]).doubleValue]; } else { result(FlutterMethodNotImplemented); } diff --git a/packages/camera/camera/lib/camera.dart b/packages/camera/camera/lib/camera.dart index 6c6214e96951..55e7aa9444aa 100644 --- a/packages/camera/camera/lib/camera.dart +++ b/packages/camera/camera/lib/camera.dart @@ -12,5 +12,6 @@ export 'package:camera_platform_interface/camera_platform_interface.dart' CameraException, CameraLensDirection, FlashMode, + ExposureMode, ResolutionPreset, XFile; diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 1d7aed755f42..c1f44bc9630a 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -3,12 +3,14 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math'; import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:pedantic/pedantic.dart'; final MethodChannel _channel = const MethodChannel('plugins.flutter.io/camera'); @@ -37,6 +39,8 @@ class CameraValue { this.isStreamingImages, bool isRecordingPaused, this.flashMode, + this.exposureMode, + this.exposurePointSupported, }) : _isRecordingPaused = isRecordingPaused; /// Creates a new camera controller state for an uninitialized controller. @@ -48,6 +52,7 @@ class CameraValue { isStreamingImages: false, isRecordingPaused: false, flashMode: FlashMode.auto, + exposurePointSupported: false, ); /// True after [CameraController.initialize] has completed successfully. @@ -91,6 +96,12 @@ class CameraValue { /// The flash mode the camera is currently set to. final FlashMode flashMode; + /// The exposure mode the camera is currently set to. + final ExposureMode exposureMode; + + /// Whether setting the exposure point is supported. + final bool exposurePointSupported; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -104,6 +115,8 @@ class CameraValue { Size previewSize, bool isRecordingPaused, FlashMode flashMode, + ExposureMode exposureMode, + bool exposurePointSupported, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -114,6 +127,9 @@ class CameraValue { isStreamingImages: isStreamingImages ?? this.isStreamingImages, isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, flashMode: flashMode ?? this.flashMode, + exposureMode: exposureMode ?? this.exposureMode, + exposurePointSupported: + exposurePointSupported ?? this.exposurePointSupported, ); } @@ -125,7 +141,9 @@ class CameraValue { 'errorDescription: $errorDescription, ' 'previewSize: $previewSize, ' 'isStreamingImages: $isStreamingImages, ' - 'flashMode: $flashMode)'; + 'flashMode: $flashMode, ' + 'exposureMode: $exposureMode, ' + 'exposurePointSupported: $exposurePointSupported)'; } } @@ -184,25 +202,34 @@ class CameraController extends ValueNotifier { ); } try { + Completer _initializeCompleter = Completer(); + _cameraId = await CameraPlatform.instance.createCamera( description, resolutionPreset, enableAudio: enableAudio, ); - final previewSize = - CameraPlatform.instance.onCameraInitialized(_cameraId).map((event) { - return Size( - event.previewWidth, - event.previewHeight, - ); - }).first; + unawaited(CameraPlatform.instance + .onCameraInitialized(_cameraId) + .first + .then((event) { + _initializeCompleter.complete(event); + })); await CameraPlatform.instance.initializeCamera(_cameraId); value = value.copyWith( isInitialized: true, - previewSize: await previewSize, + previewSize: await _initializeCompleter.future + .then((CameraInitializedEvent event) => Size( + event.previewWidth, + event.previewHeight, + )), + exposureMode: await _initializeCompleter.future + .then((event) => event.exposureMode), + exposurePointSupported: await _initializeCompleter.future + .then((event) => event.exposurePointSupported), ); } on PlatformException catch (e) { throw CameraException(e.code, e.message); @@ -532,6 +559,137 @@ class CameraController extends ValueNotifier { } } + /// Sets the exposure mode for taking pictures. + Future setExposureMode(ExposureMode mode) async { + try { + await CameraPlatform.instance.setExposureMode(_cameraId, mode); + value = value.copyWith(exposureMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure point for automatically determining the exposure value. + Future setExposurePoint(Offset point) async { + if (point != null && + (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { + throw ArgumentError( + 'The values of point should be anywhere between (0,0) and (1,1).'); + } + try { + await CameraPlatform.instance.setExposurePoint( + _cameraId, + point == null + ? null + : Point( + point.dx, + point.dy, + ), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the minimum supported exposure offset for the selected camera in EV units. + Future getMinExposureOffset() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'getMinExposureOffset was called on uninitialized CameraController', + ); + } + + try { + return CameraPlatform.instance.getMinExposureOffset(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the maximum supported exposure offset for the selected camera in EV units. + Future getMaxExposureOffset() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'getMaxExposureOffset was called on uninitialized CameraController', + ); + } + + try { + return CameraPlatform.instance.getMaxExposureOffset(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the supported step size for exposure offset for the selected camera in EV units. + /// + /// Returns 0 when the camera supports using a free value without stepping. + Future getExposureOffsetStepSize() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'getExposureOffsetStepSize was called on uninitialized CameraController', + ); + } + + try { + return CameraPlatform.instance.getExposureOffsetStepSize(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure offset for the selected camera. + /// + /// The supplied [offset] value should be in EV units. 1 EV unit represents a + /// doubling in brightness. It should be between the minimum and maximum offsets + /// obtained through `getMinExposureOffset` and `getMaxExposureOffset` respectively. + /// Throws a `CameraException` when an illegal offset is supplied. + /// + /// When the supplied [offset] value does not align with the step size obtained + /// through `getExposureStepSize`, it will automatically be rounded to the nearest step. + /// + /// Returns the (rounded) offset value that was set. + Future setExposureOffset(double offset) async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'setExposureOffset was called on uninitialized CameraController', + ); + } + + // Check if offset is in range + List range = + await Future.wait([getMinExposureOffset(), getMaxExposureOffset()]); + if (offset < range[0] || offset > range[1]) { + throw CameraException( + "exposureOffsetOutOfBounds", + "The provided exposure offset was outside the supported range for this device.", + ); + } + + // Round to the closest step if needed + double stepSize = await getExposureOffsetStepSize(); + if (stepSize > 0) { + double inv = 1.0 / stepSize; + double roundedOffset = (offset * inv).roundToDouble() / inv; + if (roundedOffset > range[1]) { + roundedOffset = (offset * inv).floorToDouble() / inv; + } else if (roundedOffset < range[0]) { + roundedOffset = (offset * inv).ceilToDouble() / inv; + } + offset = roundedOffset; + } + + try { + return CameraPlatform.instance.setExposureOffset(_cameraId, offset); + } 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/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 1bccbd4d45df..144ecc8df60d 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -2,14 +2,15 @@ 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.6.3 +version: 0.6.4 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: flutter: sdk: flutter - camera_platform_interface: ^1.0.4 - + #TODO(BeMacized): Replace with reference to pub.dev version once updated platform interface has been published. + camera_platform_interface: + path: ../camera_platform_interface dev_dependencies: path_provider: ^0.5.0 video_player: ^0.10.0 diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 43dec7374901..4b806107fb5b 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math'; import 'dart:ui'; import 'package:camera/camera.dart'; @@ -26,7 +27,13 @@ get mockAvailableCameras => [ get mockInitializeCamera => 13; -get mockOnCameraInitializedEvent => CameraInitializedEvent(13, 75, 75); +get mockOnCameraInitializedEvent => CameraInitializedEvent( + 13, + 75, + 75, + ExposureMode.auto, + true, + ); get mockOnCameraClosingEvent => null; @@ -603,6 +610,398 @@ void main() { 'This is a test error message', ))); }); + + test('setExposureMode() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.setExposureMode(ExposureMode.auto); + + verify(CameraPlatform.instance + .setExposureMode(cameraController.cameraId, ExposureMode.auto)) + .called(1); + }); + + test('setExposureMode() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .setExposureMode(cameraController.cameraId, ExposureMode.auto)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.setExposureMode(ExposureMode.auto), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('setExposurePoint() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.setExposurePoint(Offset(0.5, 0.5)); + + verify(CameraPlatform.instance.setExposurePoint( + cameraController.cameraId, Point(0.5, 0.5))) + .called(1); + }); + + test('setExposurePoint() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance.setExposurePoint( + cameraController.cameraId, Point(0.5, 0.5))) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.setExposurePoint(Offset(0.5, 0.5)), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('getMinExposureOffset() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.getMinExposureOffset(); + + verify(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .called(1); + }); + + test('getMinExposureOffset() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.getMinExposureOffset(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('getMaxExposureOffset() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.getMaxExposureOffset(); + + verify(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .called(1); + }); + + test('getMaxExposureOffset() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.getMaxExposureOffset(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('getExposureOffsetStepSize() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.getExposureOffsetStepSize(); + + verify(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .called(1); + }); + + test( + 'getExposureOffsetStepSize() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.getExposureOffsetStepSize(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('setExposureOffset() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.0); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 2.0); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 1.0); + + await cameraController.setExposureOffset(1.0); + + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.0)) + .called(1); + }); + + test('setExposureOffset() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.0); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 2.0); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 1.0); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.0)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.setExposureOffset(1.0), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test( + 'setExposureOffset() throws $CameraException when offset is out of bounds', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.0); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 2.0); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 1.0); + + expect( + cameraController.setExposureOffset(3.0), + throwsA(isA().having( + (error) => error.description, + 'exposureOffsetOutOfBounds', + 'The provided exposure offset was outside the supported range for this device.', + ))); + expect( + cameraController.setExposureOffset(-2.0), + throwsA(isA().having( + (error) => error.description, + 'exposureOffsetOutOfBounds', + 'The provided exposure offset was outside the supported range for this device.', + ))); + + await cameraController.setExposureOffset(2.0); + await cameraController.setExposureOffset(-1.0); + await cameraController.setExposureOffset(-0.0); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 2.0)) + .called(1); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -1.0)) + .called(1); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.0)) + .called(1); + }); + + test('setExposureOffset() rounds offset to nearest step', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.0); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 1.0); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 0.4); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.0)) + .thenAnswer((_) async => 1.0); + + await cameraController.setExposureOffset(1.0); + await cameraController.setExposureOffset(-1.0); + await cameraController.setExposureOffset(0.1); + await cameraController.setExposureOffset(0.2); + await cameraController.setExposureOffset(0.3); + await cameraController.setExposureOffset(0.4); + await cameraController.setExposureOffset(0.5); + await cameraController.setExposureOffset(0.6); + await cameraController.setExposureOffset(0.7); + await cameraController.setExposureOffset(-0.1); + await cameraController.setExposureOffset(-0.2); + await cameraController.setExposureOffset(-0.3); + await cameraController.setExposureOffset(-0.4); + await cameraController.setExposureOffset(-0.5); + await cameraController.setExposureOffset(-0.6); + await cameraController.setExposureOffset(-0.7); + + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.8)) + .called(3); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -0.8)) + .called(3); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.0)) + .called(2); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.4)) + .called(4); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -0.4)) + .called(4); + }); }); } diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index 06b327cb1c29..d9193e212ea9 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -13,15 +13,16 @@ void main() { group('camera_value', () { test('Can be created', () { var cameraValue = const CameraValue( - isInitialized: false, - errorDescription: null, - previewSize: Size(10, 10), - isRecordingPaused: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - flashMode: FlashMode.auto, - ); + isInitialized: false, + errorDescription: null, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + exposurePointSupported: true); expect(cameraValue, isA()); expect(cameraValue.isInitialized, isFalse); @@ -31,6 +32,9 @@ void main() { expect(cameraValue.isRecordingVideo, isFalse); expect(cameraValue.isTakingPicture, isFalse); expect(cameraValue.isStreamingImages, isFalse); + expect(cameraValue.flashMode, FlashMode.auto); + expect(cameraValue.exposureMode, ExposureMode.auto); + expect(cameraValue.exposurePointSupported, true); }); test('Can be created as uninitialized', () { @@ -44,6 +48,9 @@ void main() { expect(cameraValue.isRecordingVideo, isFalse); expect(cameraValue.isTakingPicture, isFalse); expect(cameraValue.isStreamingImages, isFalse); + expect(cameraValue.flashMode, FlashMode.auto); + expect(cameraValue.exposureMode, null); + expect(cameraValue.exposurePointSupported, false); }); test('Can be copied with isInitialized', () { @@ -59,6 +66,8 @@ void main() { expect(cameraValue.isTakingPicture, isFalse); expect(cameraValue.isStreamingImages, isFalse); expect(cameraValue.flashMode, FlashMode.auto); + expect(cameraValue.exposureMode, null); + expect(cameraValue.exposurePointSupported, false); }); test('Has aspectRatio after setting size', () { @@ -97,10 +106,11 @@ void main() { isTakingPicture: false, isStreamingImages: false, flashMode: FlashMode.auto, + exposurePointSupported: true, ); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: null, exposurePointSupported: true)'); }); }); } From 2b98bdd24c5f61f3d8b547122f6ed315646ab17b Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Fri, 25 Dec 2020 13:16:03 +0100 Subject: [PATCH 05/10] iOS fix for setting the exposure point --- .../camera/camera/ios/Classes/CameraPlugin.m | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 005ec38fcdfa..87661f4a5de0 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -824,15 +824,19 @@ - (void)setExposureModeWithResult:(FlutterResult)result mode:(NSString *)modeStr return; } _exposureMode = mode; - [self applyExposureMode]; + [self applyExposureMode:mode]; result(nil); } -- (void)applyExposureMode { +- (void)applyExposureMode:(ExposureMode)mode { [_captureDevice lockForConfiguration:nil]; - switch (_exposureMode) { + switch (mode) { case ExposureModeLocked: - [_captureDevice setExposureMode:AVCaptureExposureModeLocked]; + if (mode == _exposureMode) { + [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; + } else { + [_captureDevice setExposureMode:AVCaptureExposureModeLocked]; + } break; case ExposureModeAuto: if ([_captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { @@ -853,10 +857,10 @@ - (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y return; } [_captureDevice lockForConfiguration:nil]; - [_captureDevice setExposurePointOfInterest:CGPointMake(x, y)]; + [_captureDevice setExposurePointOfInterest:CGPointMake(y, 1 - x)]; [_captureDevice unlockForConfiguration]; // Retrigger auto exposure - [self applyExposureMode]; + [self applyExposureMode:_exposureMode]; result(nil); } From 06f436f08fbf52dffe64eb9d07ad2d888fdc7d71 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Fri, 25 Dec 2020 13:21:48 +0100 Subject: [PATCH 06/10] Removed unnecessary check --- packages/camera/camera/ios/Classes/CameraPlugin.m | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 87661f4a5de0..816792e2fc1d 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -824,19 +824,15 @@ - (void)setExposureModeWithResult:(FlutterResult)result mode:(NSString *)modeStr return; } _exposureMode = mode; - [self applyExposureMode:mode]; + [self applyExposureMode]; result(nil); } -- (void)applyExposureMode:(ExposureMode)mode { +- (void)applyExposureMode { [_captureDevice lockForConfiguration:nil]; - switch (mode) { + switch (_exposureMode) { case ExposureModeLocked: - if (mode == _exposureMode) { - [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; - } else { - [_captureDevice setExposureMode:AVCaptureExposureModeLocked]; - } + [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; break; case ExposureModeAuto: if ([_captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { @@ -860,7 +856,7 @@ - (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y [_captureDevice setExposurePointOfInterest:CGPointMake(y, 1 - x)]; [_captureDevice unlockForConfiguration]; // Retrigger auto exposure - [self applyExposureMode:_exposureMode]; + [self applyExposureMode]; result(nil); } From 14c6ab654d8dce6050026e4e1404f497a0514931 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 29 Dec 2020 11:22:03 +0100 Subject: [PATCH 07/10] Update platform interface dependency --- packages/camera/camera/pubspec.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 144ecc8df60d..da67be5404e1 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -8,9 +8,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: flutter: sdk: flutter - #TODO(BeMacized): Replace with reference to pub.dev version once updated platform interface has been published. - camera_platform_interface: - path: ../camera_platform_interface + camera_platform_interface: ^1.2.0 dev_dependencies: path_provider: ^0.5.0 video_player: ^0.10.0 From a52f690c42a937c909810d86e6835872bd1b4be3 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 29 Dec 2020 17:13:52 +0100 Subject: [PATCH 08/10] Implement PR feedback --- .../io/flutter/plugins/camera/Camera.java | 67 +++++-------- .../flutter/plugins/camera/CameraRegions.java | 55 +++++++++++ .../plugins/camera/CameraRegionsTest.java | 99 +++++++++++++++++++ .../plugins/camera/DartMessengerTest.java | 1 + packages/camera/camera/pubspec.yaml | 2 +- 5 files changed, 181 insertions(+), 43 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java 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 689df4e3afa6..90fd21340162 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 @@ -86,7 +86,7 @@ public class Camera { private FlashMode flashMode; private ExposureMode exposureMode; private PictureCaptureRequest pictureCaptureRequest; - private MeteringRectangle aeMeteringRectangle; + private CameraRegions cameraRegions; private int exposureOffset; public Camera( @@ -167,6 +167,7 @@ public void open() throws CameraAccessException { public void onOpened(@NonNull CameraDevice device) { cameraDevice = device; try { + cameraRegions = new CameraRegions(getRegionBoundaries()); startPreview(); dartMessenger.sendCameraInitializedEvent( previewSize.getWidth(), @@ -702,30 +703,14 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) x = 0.5; y = 0.5; } - // Get the current exposure point boundaries. - Size maxBoundaries = getExposureBoundaries(); + // Get the current region boundaries. + Size maxBoundaries = getRegionBoundaries(); if (maxBoundaries == null) { result.error("setExposurePointFailed", "Could not determine max region boundaries", null); return; } - // Interpolate the target coordinate - int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1))); - int targetY = (int) Math.round(y * ((double) (maxBoundaries.getHeight() - 1))); - // Determine the dimensions of the metering triangle (1th of the viewport) - int targetWidth = (int) Math.round(((double) maxBoundaries.getWidth()) / 10d); - int targetHeight = (int) Math.round(((double) maxBoundaries.getHeight()) / 10d); - // Adjust target coordinate to represent top-left corner of metering rectangle - targetX -= targetWidth / 2; - targetY -= targetHeight / 2; - // Adjust target coordinate as to not fall out of bounds - if (targetX < 0) targetX = 0; - if (targetY < 0) targetY = 0; - int maxTargetX = maxBoundaries.getWidth() - 1 - targetWidth; - int maxTargetY = maxBoundaries.getHeight() - 1 - targetHeight; - if (targetX > maxTargetX) targetX = maxTargetX; - if (targetY > maxTargetY) targetY = maxTargetY; // Set the metering rectangle - aeMeteringRectangle = new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1); + cameraRegions.setAutoExposureMeteringRectangleFromPoint(x, y); // Apply it initPreviewCaptureBuilder(); this.cameraCaptureSession.setRepeatingRequest( @@ -733,25 +718,23 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) result.success(null); } - @SuppressLint("NewApi") - private Size getExposureBoundaries() throws CameraAccessException { - // Check if the device supports distortion correction - boolean supportsDistortionCorrection = false; - if (android.os.Build.VERSION.SDK_INT >= VERSION_CODES.P) { - int[] availableDistortionCorrectionModes = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); - if (availableDistortionCorrectionModes == null) - availableDistortionCorrectionModes = new int[0]; - long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); - supportsDistortionCorrection = nonOffModesSupported > 0; - } + @TargetApi(VERSION_CODES.P) + private boolean supportsDistortionCorrection() throws CameraAccessException { + int[] availableDistortionCorrectionModes = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + return nonOffModesSupported > 0; + } + + private Size getRegionBoundaries() throws CameraAccessException { // No distortion correction support - if (!supportsDistortionCorrection) { + if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { return cameraManager .getCameraCharacteristics(cameraDevice.getId()) .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); @@ -850,10 +833,10 @@ private void initPreviewCaptureBuilder() { break; } // Applying auto exposure - if (aeMeteringRectangle != null) { - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[] {aeMeteringRectangle}); - } + MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); switch (exposureMode) { case locked: captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java new file mode 100644 index 000000000000..5c58b0741af7 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java @@ -0,0 +1,55 @@ +package io.flutter.plugins.camera; + +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; + +public final class CameraRegions { + private MeteringRectangle aeMeteringRectangle; + private Size maxBoundaries; + + public CameraRegions(Size maxBoundaries) { + assert (maxBoundaries != null); + assert (maxBoundaries.getWidth() > 0); + assert (maxBoundaries.getHeight() > 0); + this.maxBoundaries = maxBoundaries; + setAutoExposureMeteringRectangleFromPoint(0.5, 0.5); + } + + public MeteringRectangle getAEMeteringRectangle() { + return aeMeteringRectangle; + } + + public Size getMaxBoundaries() { + return this.maxBoundaries; + } + + public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { + this.aeMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); + } + + public MeteringRectangle getMeteringRectangleForPoint(Size maxBoundaries, double x, double y) { + assert (x >= 0 && x <= 1); + assert (y >= 0 && y <= 1); + assert (maxBoundaries != null); + + // Interpolate the target coordinate + int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1))); + int targetY = (int) Math.round(y * ((double) (maxBoundaries.getHeight() - 1))); + // Determine the dimensions of the metering triangle (10th of the viewport) + int targetWidth = (int) Math.round(((double) maxBoundaries.getWidth()) / 10d); + int targetHeight = (int) Math.round(((double) maxBoundaries.getHeight()) / 10d); + // Adjust target coordinate to represent top-left corner of metering rectangle + targetX -= targetWidth / 2; + targetY -= targetHeight / 2; + // Adjust target coordinate as to not fall out of bounds + if (targetX < 0) targetX = 0; + if (targetY < 0) targetY = 0; + int maxTargetX = maxBoundaries.getWidth() - 1 - targetWidth; + int maxTargetY = maxBoundaries.getHeight() - 1 - targetHeight; + if (targetX > maxTargetX) targetX = maxTargetX; + if (targetY > maxTargetY) targetY = maxTargetY; + + // Build the metering rectangle + return new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java new file mode 100644 index 000000000000..56f8a20a5644 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java @@ -0,0 +1,99 @@ +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; + +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CameraRegionsTest { + + CameraRegions cameraRegions; + + @Before + public void setUp() { + this.cameraRegions = new CameraRegions(new Size(100, 50)); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_x_upper_bound() { + cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 1.5, 0); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_x_lower_bound() { + cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), -0.5, 0); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_y_upper_bound() { + cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 0, 1.5); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { + cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 0, -0.5); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_null_boundaries() { + cameraRegions.getMeteringRectangleForPoint(null, 0, -0); + } + + @Test + public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() { + MeteringRectangle r; + // Center + r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.5, 0.5); + assertEquals(new MeteringRectangle(45, 23, 10, 5, 1), r); + + // Top left + r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.0, 0.0); + assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), r); + + // Bottom right + r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 1.0, 1.0); + assertEquals(new MeteringRectangle(89, 44, 10, 5, 1), r); + + // Top left + r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.0, 1.0); + assertEquals(new MeteringRectangle(0, 44, 10, 5, 1), r); + + // Top right + r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 1.0, 0.0); + assertEquals(new MeteringRectangle(89, 0, 10, 5, 1), r); + } + + @Test(expected = AssertionError.class) + public void constructor_should_throw_for_null_boundaries() { + new CameraRegions(null); + } + + @Test(expected = AssertionError.class) + public void constructor_should_throw_for_0_width_boundary() { + new CameraRegions(new Size(0, 50)); + } + + @Test(expected = AssertionError.class) + public void constructor_should_throw_for_0_height_boundary() { + new CameraRegions(new Size(100, 0)); + } + + @Test + public void constructor_should_initialize() { + CameraRegions cr = new CameraRegions(new Size(100, 50)); + assertEquals(new Size(100, 50), cr.getMaxBoundaries()); + assertEquals(new MeteringRectangle(45, 23, 10, 5, 1), cr.getAEMeteringRectangle()); + } + + @Test + public void setAutoExposureMeteringRectangleFromPoin_should_set_aeMeteringRectangle_for_point() { + CameraRegions cr = new CameraRegions(new Size(100, 50)); + cr.setAutoExposureMeteringRectangleFromPoint(0, 0); + assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), cr.getAEMeteringRectangle()); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index 52d9829f7d8f..f91bf82c7063 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -7,6 +7,7 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.StandardMethodCodec; +import io.flutter.plugins.camera.types.ExposureMode; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index da67be5404e1..054440025d47 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -9,6 +9,7 @@ dependencies: flutter: sdk: flutter camera_platform_interface: ^1.2.0 + pedantic: ^1.8.0 dev_dependencies: path_provider: ^0.5.0 video_player: ^0.10.0 @@ -16,7 +17,6 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.8.0 mockito: ^4.1.3 plugin_platform_interface: ^1.0.3 From 9a88bca6910b0e5255ec106c2b9a473fca44da18 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 29 Dec 2020 18:10:40 +0100 Subject: [PATCH 09/10] Restore test --- packages/camera/camera/test/camera_test.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 50ad899f7d98..5a4a7fc8771b 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -27,7 +27,8 @@ get mockAvailableCameras => [ get mockInitializeCamera => 13; -get mockOnCameraInitializedEvent => CameraInitializedEvent(13, 75, 75); +get mockOnCameraInitializedEvent => + CameraInitializedEvent(13, 75, 75, ExposureMode.auto, true); get mockOnCameraClosingEvent => null; @@ -1034,7 +1035,8 @@ class MockCameraPlatform extends Mock : Future.value(mockTakePicture); @override - Future startVideoRecording(int cameraId) => + Future startVideoRecording(int cameraId, + {Duration maxVideoDuration}) => Future.value(mockVideoRecordingXFile); } From 589f44231bf9f84910c86864bd483ba7f6acd4b4 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Wed, 30 Dec 2020 10:26:47 +0100 Subject: [PATCH 10/10] Small improvements for exposure point resetting --- .../io/flutter/plugins/camera/Camera.java | 2 +- .../flutter/plugins/camera/CameraRegions.java | 14 +++++++----- .../plugins/camera/CameraRegionsTest.java | 22 ++++++++++++------- 3 files changed, 24 insertions(+), 14 deletions(-) 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 90fd21340162..d57a737c09f6 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 @@ -836,7 +836,7 @@ private void initPreviewCaptureBuilder() { MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); captureRequestBuilder.set( CaptureRequest.CONTROL_AE_REGIONS, - new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); + aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); switch (exposureMode) { case locked: captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java index 5c58b0741af7..2285f67ad25c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java @@ -8,11 +8,9 @@ public final class CameraRegions { private Size maxBoundaries; public CameraRegions(Size maxBoundaries) { - assert (maxBoundaries != null); - assert (maxBoundaries.getWidth() > 0); - assert (maxBoundaries.getHeight() > 0); + assert (maxBoundaries == null || maxBoundaries.getWidth() > 0); + assert (maxBoundaries == null || maxBoundaries.getHeight() > 0); this.maxBoundaries = maxBoundaries; - setAutoExposureMeteringRectangleFromPoint(0.5, 0.5); } public MeteringRectangle getAEMeteringRectangle() { @@ -23,6 +21,10 @@ public Size getMaxBoundaries() { return this.maxBoundaries; } + public void resetAutoExposureMeteringRectangle() { + this.aeMeteringRectangle = null; + } + public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { this.aeMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); } @@ -30,7 +32,9 @@ public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { public MeteringRectangle getMeteringRectangleForPoint(Size maxBoundaries, double x, double y) { assert (x >= 0 && x <= 1); assert (y >= 0 && y <= 1); - assert (maxBoundaries != null); + if (maxBoundaries == null) + throw new IllegalStateException( + "Functionality for managing metering rectangles is unavailable as this CameraRegions instance was initialized with null boundaries."); // Interpolate the target coordinate int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1))); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java index 56f8a20a5644..ca66918e2493 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java @@ -1,6 +1,8 @@ package io.flutter.plugins.camera; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; @@ -39,7 +41,7 @@ public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 0, -0.5); } - @Test(expected = AssertionError.class) + @Test(expected = IllegalStateException.class) public void getMeteringRectangleForPoint_should_throw_for_null_boundaries() { cameraRegions.getMeteringRectangleForPoint(null, 0, -0); } @@ -68,11 +70,6 @@ public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() assertEquals(new MeteringRectangle(89, 0, 10, 5, 1), r); } - @Test(expected = AssertionError.class) - public void constructor_should_throw_for_null_boundaries() { - new CameraRegions(null); - } - @Test(expected = AssertionError.class) public void constructor_should_throw_for_0_width_boundary() { new CameraRegions(new Size(0, 50)); @@ -87,13 +84,22 @@ public void constructor_should_throw_for_0_height_boundary() { public void constructor_should_initialize() { CameraRegions cr = new CameraRegions(new Size(100, 50)); assertEquals(new Size(100, 50), cr.getMaxBoundaries()); - assertEquals(new MeteringRectangle(45, 23, 10, 5, 1), cr.getAEMeteringRectangle()); + assertNull(cr.getAEMeteringRectangle()); } @Test - public void setAutoExposureMeteringRectangleFromPoin_should_set_aeMeteringRectangle_for_point() { + public void setAutoExposureMeteringRectangleFromPoint_should_set_aeMeteringRectangle_for_point() { CameraRegions cr = new CameraRegions(new Size(100, 50)); cr.setAutoExposureMeteringRectangleFromPoint(0, 0); assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), cr.getAEMeteringRectangle()); } + + @Test + public void resetAutoExposureMeteringRectangle_should_reset_aeMeteringRectangle() { + CameraRegions cr = new CameraRegions(new Size(100, 50)); + cr.setAutoExposureMeteringRectangleFromPoint(0, 0); + assertNotNull(cr.getAEMeteringRectangle()); + cr.resetAutoExposureMeteringRectangle(); + assertNull(cr.getAEMeteringRectangle()); + } }