Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[camera] Zoom functionality for Android and iOS #3315

Merged
merged 3 commits into from
Dec 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.2

* Add zoom support for Android and iOS implementations.

## 0.6.1+1

* Added implementation of the `didFinishProcessingPhoto` on iOS which allows saving image metadata (EXIF) on iOS 11 and up.
Expand Down
10 changes: 5 additions & 5 deletions packages/camera/camera/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.android.tools.build:gradle:3.5.0'
}
}

Expand Down Expand Up @@ -40,16 +40,16 @@ android {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}
dependencies {
implementation 'androidx.annotation:annotation:1.0.0'
implementation 'androidx.core:core:1.0.0'
}
testOptions {
unitTests.includeAndroidResources = true
unitTests.returnDefaultValues = true
}
}

dependencies {
compileOnly 'androidx.annotation:annotation:1.1.0'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:3.5.13'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'org.robolectric:robolectric:4.3'
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import android.app.Activity;
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
Expand All @@ -21,7 +22,6 @@
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.params.SessionConfiguration;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.media.Image;
import android.media.ImageReader;
Expand All @@ -47,6 +47,7 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executors;

Expand All @@ -60,19 +61,20 @@ public class Camera {
private final Size captureSize;
private final Size previewSize;
private final boolean enableAudio;
private final Context applicationContext;
private final CamcorderProfile recordingProfile;
private final DartMessenger dartMessenger;
private final CameraZoom cameraZoom;

private CameraDevice cameraDevice;
private CameraCaptureSession cameraCaptureSession;
private ImageReader pictureImageReader;
private ImageReader imageStreamReader;
private DartMessenger dartMessenger;
private CaptureRequest.Builder captureRequestBuilder;
private MediaRecorder mediaRecorder;
private boolean recordingVideo;
private File videoRecordingFile;
private CamcorderProfile recordingProfile;
private int currentOrientation = ORIENTATION_UNKNOWN;
private Context applicationContext;
private FlashMode flashMode;
private PictureCaptureRequest pictureCaptureRequest;

Expand Down Expand Up @@ -108,18 +110,18 @@ public void onOrientationChanged(int i) {
orientationEventListener.enable();

CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName);
StreamConfigurationMap streamConfigurationMap =
characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
//noinspection ConstantConditions
sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
//noinspection ConstantConditions
isFrontFacing =
characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT;
ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset);
recordingProfile =
CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset);
captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
previewSize = computeBestPreviewSize(cameraName, preset);
cameraZoom =
new CameraZoom(
characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE),
characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM));
}

private void prepareMediaRecorder(String outputFilePath) throws IOException {
Expand Down Expand Up @@ -212,10 +214,6 @@ private void writeToFile(ByteBuffer buffer, File file) throws IOException {
}
}

SurfaceTextureEntry getFlutterTexture() {
return flutterTexture;
}

public void takePicture(@NonNull final Result result) {
// Only take 1 picture at a time
if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) {
Expand Down Expand Up @@ -620,6 +618,39 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i
null);
}

public float getMaxZoomLevel() {
return cameraZoom.maxZoom;
}

public float getMinZoomLevel() {
return CameraZoom.DEFAULT_ZOOM_FACTOR;
}

public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException {
float maxZoom = cameraZoom.maxZoom;
float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR;

if (zoom > maxZoom || zoom < minZoom) {
String errorMessage =
String.format(
Locale.ENGLISH,
"Zoom level out of bounds (zoom level should be between %f and %f).",
minZoom,
maxZoom);
result.error("ZOOM_ERROR", errorMessage, null);
return;
}

//Zoom area is calculated relative to sensor area (activeRect)
if (captureRequestBuilder != null) {
final Rect computedZoom = cameraZoom.computeZoom(zoom);
captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom);
cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
}

result.success(null);
}

private void closeCaptureSession() {
if (cameraCaptureSession != null) {
cameraCaptureSession.close();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.flutter.plugins.camera;

import android.graphics.Rect;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.math.MathUtils;

public final class CameraZoom {
public static final float DEFAULT_ZOOM_FACTOR = 1.0f;

@NonNull private final Rect cropRegion = new Rect();
@Nullable private final Rect sensorSize;

public final float maxZoom;
public final boolean hasSupport;

public CameraZoom(@Nullable final Rect sensorArraySize, final Float maxZoom) {
this.sensorSize = sensorArraySize;

if (this.sensorSize == null) {
this.maxZoom = DEFAULT_ZOOM_FACTOR;
this.hasSupport = false;
return;
}

this.maxZoom =
((maxZoom == null) || (maxZoom < DEFAULT_ZOOM_FACTOR)) ? DEFAULT_ZOOM_FACTOR : maxZoom;

this.hasSupport = (Float.compare(this.maxZoom, DEFAULT_ZOOM_FACTOR) > 0);
}

public Rect computeZoom(final float zoom) {
if (sensorSize == null || !this.hasSupport) {
return null;
}

final float newZoom = MathUtils.clamp(zoom, DEFAULT_ZOOM_FACTOR, this.maxZoom);

final int centerX = this.sensorSize.width() / 2;
final int centerY = this.sensorSize.height() / 2;
final int deltaX = (int) ((0.5f * this.sensorSize.width()) / newZoom);
final int deltaY = (int) ((0.5f * this.sensorSize.height()) / newZoom);

this.cropRegion.set(centerX - deltaX, centerY - deltaY, centerX + deltaX, centerY + deltaY);

return cropRegion;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,49 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
}
break;
}
case "getMaxZoomLevel":
{
assert camera != null;

try {
float maxZoomLevel = camera.getMaxZoomLevel();
result.success(maxZoomLevel);
} catch (Exception e) {
handleException(e, result);
}
break;
}
case "getMinZoomLevel":
{
assert camera != null;

try {
float minZoomLevel = camera.getMinZoomLevel();
result.success(minZoomLevel);
} catch (Exception e) {
handleException(e, result);
}
break;
}
case "setZoomLevel":
{
assert camera != null;

Double zoom = call.argument("zoom");

if (zoom == null) {
result.error(
"ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null);
return;
}

try {
camera.setZoomLevel(result, zoom.floatValue());
} catch (Exception e) {
handleException(e, result);
}
break;
}
case "dispose":
{
if (camera != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package io.flutter.plugins.camera;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import android.graphics.Rect;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

@RunWith(RobolectricTestRunner.class)
public class CameraZoomTest {

@Test
public void ctor_when_parameters_are_valid() {
final Rect sensorSize = new Rect(0, 0, 0, 0);
final Float maxZoom = 4.0f;
final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom);

assertNotNull(cameraZoom);
assertTrue(cameraZoom.hasSupport);
assertEquals(4.0f, cameraZoom.maxZoom, 0);
assertEquals(1.0f, CameraZoom.DEFAULT_ZOOM_FACTOR, 0);
}

@Test
public void ctor_when_sensor_size_is_null() {
final Rect sensorSize = null;
final Float maxZoom = 4.0f;
final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom);

assertNotNull(cameraZoom);
assertFalse(cameraZoom.hasSupport);
assertEquals(cameraZoom.maxZoom, 1.0f, 0);
}

@Test
public void ctor_when_max_zoom_is_null() {
final Rect sensorSize = new Rect(0, 0, 0, 0);
final Float maxZoom = null;
final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom);

assertNotNull(cameraZoom);
assertFalse(cameraZoom.hasSupport);
assertEquals(cameraZoom.maxZoom, 1.0f, 0);
}

@Test
public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() {
final Rect sensorSize = new Rect(0, 0, 0, 0);
final Float maxZoom = 0.5f;
final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom);

assertNotNull(cameraZoom);
assertFalse(cameraZoom.hasSupport);
assertEquals(cameraZoom.maxZoom, 1.0f, 0);
}

@Test
public void setZoom_when_no_support_should_not_set_scaler_crop_region() {
final CameraZoom cameraZoom = new CameraZoom(null, null);
final Rect computedZoom = cameraZoom.computeZoom(2f);

assertNull(computedZoom);
}

@Test
public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_zero() {
final Rect sensorSize = new Rect(0, 0, 0, 0);
final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f);
final Rect computedZoom = cameraZoom.computeZoom(18f);

assertNotNull(computedZoom);
assertEquals(computedZoom.left, 0);
assertEquals(computedZoom.top, 0);
assertEquals(computedZoom.right, 0);
assertEquals(computedZoom.bottom, 0);
}

@Test
public void setZoom_when_sensor_size_is_valid_should_return_crop_region() {
final Rect sensorSize = new Rect(0, 0, 100, 100);
final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f);
final Rect computedZoom = cameraZoom.computeZoom(18f);

assertNotNull(computedZoom);
assertEquals(computedZoom.left, 48);
assertEquals(computedZoom.top, 48);
assertEquals(computedZoom.right, 52);
assertEquals(computedZoom.bottom, 52);
}

@Test
public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() {
final Rect sensorSize = new Rect(0, 0, 100, 100);
final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f);
final Rect computedZoom = cameraZoom.computeZoom(25f);

assertNotNull(computedZoom);
assertEquals(computedZoom.left, 45);
assertEquals(computedZoom.top, 45);
assertEquals(computedZoom.right, 55);
assertEquals(computedZoom.bottom, 55);
}

@Test
public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() {
final Rect sensorSize = new Rect(0, 0, 100, 100);
final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f);
final Rect computedZoom = cameraZoom.computeZoom(0.5f);

assertNotNull(computedZoom);
assertEquals(computedZoom.left, 0);
assertEquals(computedZoom.top, 0);
assertEquals(computedZoom.right, 100);
assertEquals(computedZoom.bottom, 100);
}
}
2 changes: 1 addition & 1 deletion packages/camera/camera/example/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.android.tools.build:gradle:3.5.0'
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
Loading