Skip to content

Commit

Permalink
[camerax] Implement setZoomLevel (#4950)
Browse files Browse the repository at this point in the history
Implements `setZoomLevel`.

Fixes flutter/flutter#125371.
Fixes flutter/flutter#115847.
  • Loading branch information
camsim99 committed Oct 25, 2023
1 parent fdfafcd commit f2124f7
Show file tree
Hide file tree
Showing 16 changed files with 287 additions and 18 deletions.
4 changes: 4 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.5.0+20

* Implements `setZoomLevel`.

## 0.5.0+19

* Implements torch flash mode.
Expand Down
4 changes: 0 additions & 4 deletions packages/camera/camera_android_camerax/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ and thus, the plugin will fall back to 480p if configured with a

`setFocusMode` & `setFocusPoint` are unimplemented.

### Zoom configuration \[[Issue #125371][125371]\]

`setZoomLevel` is unimplemented.

### Setting maximum duration and stream options for video capture

Calling `startVideoCapturing` with `VideoCaptureOptions` configured with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,29 @@ public void onFailure(Throwable t) {
},
ContextCompat.getMainExecutor(context));
}

/** Sets the zoom ratio of the specified {@link CameraControl} instance. */
@NonNull
public void setZoomRatio(
@NonNull CameraControl cameraControl,
@NonNull Double ratio,
@NonNull GeneratedCameraXLibrary.Result<Void> result) {
float ratioAsFloat = ratio.floatValue();
ListenableFuture<Void> setZoomRatioFuture = cameraControl.setZoomRatio(ratioAsFloat);

Futures.addCallback(
setZoomRatioFuture,
new FutureCallback<Void>() {
public void onSuccess(Void voidResult) {
result.success(null);
}

public void onFailure(Throwable t) {
result.error(t);
}
},
ContextCompat.getMainExecutor(context));
}
}

/**
Expand Down Expand Up @@ -81,7 +104,8 @@ public CameraControlHostApiImpl(
}

/**
* Sets the context that the {@code ProcessCameraProvider} will use to enable/disable torch mode.
* Sets the context that the {@code ProcessCameraProvider} will use to enable/disable torch mode
* and set the zoom ratio.
*
* <p>If using the camera plugin in an add-to-app context, ensure that a new instance of the
* {@code CameraControl} is fetched via {@code #enableTorch} anytime the context changes.
Expand All @@ -98,4 +122,13 @@ public void enableTorch(
proxy.enableTorch(
Objects.requireNonNull(instanceManager.getInstance(identifier)), torch, result);
}

@Override
public void setZoomRatio(
@NonNull Long identifier,
@NonNull Double ratio,
@NonNull GeneratedCameraXLibrary.Result<Void> result) {
proxy.setZoomRatio(
Objects.requireNonNull(instanceManager.getInstance(identifier)), ratio, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3237,6 +3237,9 @@ public interface CameraControlHostApi {
void enableTorch(
@NonNull Long identifier, @NonNull Boolean torch, @NonNull Result<Void> result);

void setZoomRatio(
@NonNull Long identifier, @NonNull Double ratio, @NonNull Result<Void> result);

/** The codec used by CameraControlHostApi. */
static @NonNull MessageCodec<Object> getCodec() {
return new StandardMessageCodec();
Expand Down Expand Up @@ -3280,6 +3283,41 @@ public void error(Throwable error) {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.CameraControlHostApi.setZoomRatio",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
Number identifierArg = (Number) args.get(0);
Double ratioArg = (Double) args.get(1);
Result<Void> resultCallback =
new Result<Void>() {
public void success(Void result) {
wrapped.add(0, null);
reply.reply(wrapped);
}

public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};

api.setZoomRatio(
(identifierArg == null) ? null : identifierArg.longValue(),
ratioArg,
resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
}
}
/** Generated class from Pigeon that represents Flutter messages that can be called from Java. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,57 @@ public void enableTorch_turnsTorchModeOnAndOffAsExpected() {
}
}

@Test
public void setZoomRatio_setsZoomAsExpected() {
try (MockedStatic<Futures> mockedFutures = Mockito.mockStatic(Futures.class)) {
final CameraControlHostApiImpl cameraControlHostApiImpl =
new CameraControlHostApiImpl(testInstanceManager, mock(Context.class));
final Long cameraControlIdentifier = 33L;
final Double zoomRatio = 0.2D;

@SuppressWarnings("unchecked")
final ListenableFuture<Void> setZoomRatioFuture = mock(ListenableFuture.class);

testInstanceManager.addDartCreatedInstance(cameraControl, cameraControlIdentifier);

when(cameraControl.setZoomRatio(zoomRatio.floatValue())).thenReturn(setZoomRatioFuture);

@SuppressWarnings("unchecked")
final ArgumentCaptor<FutureCallback<Void>> futureCallbackCaptor =
ArgumentCaptor.forClass(FutureCallback.class);

// Test successful behavior.
@SuppressWarnings("unchecked")
final GeneratedCameraXLibrary.Result<Void> successfulMockResult =
mock(GeneratedCameraXLibrary.Result.class);
cameraControlHostApiImpl.setZoomRatio(
cameraControlIdentifier, zoomRatio, successfulMockResult);
mockedFutures.verify(
() -> Futures.addCallback(eq(setZoomRatioFuture), futureCallbackCaptor.capture(), any()));
mockedFutures.clearInvocations();

FutureCallback<Void> successfulSetZoomRatioCallback = futureCallbackCaptor.getValue();

successfulSetZoomRatioCallback.onSuccess(mock(Void.class));
verify(successfulMockResult).success(null);

// Test failed behavior.
@SuppressWarnings("unchecked")
final GeneratedCameraXLibrary.Result<Void> failedMockResult =
mock(GeneratedCameraXLibrary.Result.class);
final Throwable testThrowable = new Throwable();
cameraControlHostApiImpl.setZoomRatio(cameraControlIdentifier, zoomRatio, failedMockResult);
mockedFutures.verify(
() -> Futures.addCallback(eq(setZoomRatioFuture), futureCallbackCaptor.capture(), any()));
mockedFutures.clearInvocations();

FutureCallback<Void> failedSetZoomRatioCallback = futureCallbackCaptor.getValue();

failedSetZoomRatioCallback.onFailure(testThrowable);
verify(failedMockResult).error(testThrowable);
}
}

@Test
public void flutterApiCreate_makesCallToCreateInstanceOnDartSide() {
final CameraControlFlutterApiImpl spyFlutterApi =
Expand Down
16 changes: 4 additions & 12 deletions packages/camera/camera_android_camerax/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -392,12 +392,8 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
style: styleAuto,
onPressed:
() {}, // TODO(camsim99): Add functionality back here.
onLongPress: () {
if (controller != null) {
controller!.setExposurePoint(null);
showInSnackBar('Resetting exposure point');
}
},
onLongPress:
() {}, // TODO(camsim99): Add functionality back here.,
child: const Text('AUTO'),
),
TextButton(
Expand Down Expand Up @@ -470,12 +466,8 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
style: styleAuto,
onPressed:
() {}, // TODO(camsim99): Add functionality back here.
onLongPress: () {
if (controller != null) {
controller!.setFocusPoint(null);
}
showInSnackBar('Resetting focus point');
},
onLongPress:
() {}, // TODO(camsim99): Add functionality back here.
child: const Text('AUTO'),
),
TextButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,17 @@ class AndroidCameraCameraX extends CameraPlatform {
return zoomState.minZoomRatio;
}

/// Set the zoom level for the selected camera.
///
/// The supplied [zoom] value should be between the minimum and the maximum
/// supported zoom level returned by `getMinZoomLevel` and `getMaxZoomLevel`.
/// Throws a `CameraException` when an illegal zoom level is supplied.
@override
Future<void> setZoomLevel(int cameraId, double zoom) async {
final CameraControl cameraControl = await camera!.getCameraControl();
await cameraControl.setZoomRatio(zoom);
}

/// The ui orientation changed.
@override
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() {
Expand Down
25 changes: 25 additions & 0 deletions packages/camera/camera_android_camerax/lib/src/camera_control.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,22 @@ class CameraControl extends JavaObject {
late final _CameraControlHostApiImpl _api;

/// Enables or disables the torch of related [Camera] instance.
///
/// If the torch mode was unable to be changed, an error message will be
/// added to [SystemServices.cameraErrorStreamController].
Future<void> enableTorch(bool torch) async {
return _api.enableTorchFromInstance(this, torch);
}

/// Sets zoom of related [Camera] by ratio.
///
/// Ratio should be between what the `minZoomRatio` and `maxZoomRatio` of the
/// [ZoomState] of the [CameraInfo] instance that is retrievable from the same
/// [Camera] instance; otherwise, an error message will be added to
/// [SystemServices.cameraErrorStreamController].
Future<void> setZoomRatio(double ratio) async {
return _api.setZoomRatioFromInstance(this, ratio);
}
}

/// Host API implementation of [CameraControl].
Expand Down Expand Up @@ -69,6 +82,18 @@ class _CameraControlHostApiImpl extends CameraControlHostApi {
.add(e.message ?? 'The camera was unable to change torch modes.');
}
}

/// Sets zoom of specified [CameraControl] instance by ratio.
Future<void> setZoomRatioFromInstance(
CameraControl instance, double ratio) async {
final int identifier = instanceManager.getIdentifier(instance)!;
try {
await setZoomRatio(identifier, ratio);
} on PlatformException catch (e) {
SystemServices.cameraErrorStreamController.add(e.message ??
'Zoom ratio was unable to be set. If ratio was not out of range, newer value may have been set; otherwise, the camera may be closed.');
}
}
}

/// Flutter API implementation of [CameraControl].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2685,6 +2685,28 @@ class CameraControlHostApi {
return;
}
}

Future<void> setZoomRatio(int arg_identifier, double arg_ratio) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.CameraControlHostApi.setZoomRatio', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel
.send(<Object?>[arg_identifier, arg_ratio]) as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else {
return;
}
}
}

abstract class CameraControlFlutterApi {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,9 @@ abstract class FallbackStrategyHostApi {
abstract class CameraControlHostApi {
@async
void enableTorch(int identifier, bool torch);

@async
void setZoomRatio(int identifier, double ratio);
}

@FlutterApi()
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera_android_camerax/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: camera_android_camerax
description: Android implementation of the camera plugin using the CameraX library.
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
version: 0.5.0+19
version: 0.5.0+20

environment:
sdk: ">=2.19.0 <4.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,22 @@ void main() {
expect(await camera.getMinZoomLevel(55), minZoomRatio);
});

test('setZoomLevel sets zoom ratio as expected', () async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
const int cameraId = 44;
const double zoomRatio = 0.3;
final MockCameraControl mockCameraControl = MockCameraControl();

camera.camera = MockCamera();

when(camera.camera!.getCameraControl())
.thenAnswer((_) async => mockCameraControl);

await camera.setZoomLevel(cameraId, zoomRatio);

verify(mockCameraControl.setZoomRatio(zoomRatio));
});

test(
'onStreamedFrameAvailable emits CameraImageData when picked up from CameraImageData stream controller',
() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,15 @@ class MockCameraControl extends _i1.Mock implements _i3.CameraControl {
returnValue: _i15.Future<void>.value(),
returnValueForMissingStub: _i15.Future<void>.value(),
) as _i15.Future<void>);
@override
_i15.Future<void> setZoomRatio(double? ratio) => (super.noSuchMethod(
Invocation.method(
#setZoomRatio,
[ratio],
),
returnValue: _i15.Future<void>.value(),
returnValueForMissingStub: _i15.Future<void>.value(),
) as _i15.Future<void>);
}

/// A class which mocks [CameraImageData].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,32 @@ void main() {
verify(mockApi.enableTorch(cameraControlIdentifier, enableTorch));
});

test('setZoomRatio makes call on Java side to set zoom ratio', () async {
final MockTestCameraControlHostApi mockApi =
MockTestCameraControlHostApi();
TestCameraControlHostApi.setup(mockApi);

final InstanceManager instanceManager = InstanceManager(
onWeakReferenceRemoved: (_) {},
);

final CameraControl cameraControl = CameraControl.detached(
instanceManager: instanceManager,
);
const int cameraControlIdentifier = 45;

instanceManager.addHostCreatedInstance(
cameraControl,
cameraControlIdentifier,
onCopy: (_) => CameraControl.detached(instanceManager: instanceManager),
);

const double zoom = 0.2;
await cameraControl.setZoomRatio(zoom);

verify(mockApi.setZoomRatio(cameraControlIdentifier, zoom));
});

test('flutterApiCreate makes call to add instance to instance manager', () {
final InstanceManager instanceManager = InstanceManager(
onWeakReferenceRemoved: (_) {},
Expand Down
Loading

0 comments on commit f2124f7

Please sign in to comment.