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

Commit

Permalink
Refactored and tested zoom on Android
Browse files Browse the repository at this point in the history
  • Loading branch information
mvanbeusekom committed Dec 18, 2020
1 parent b6af039 commit d3d4619
Show file tree
Hide file tree
Showing 13 changed files with 663 additions and 23 deletions.
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.1

* Add zoom support for Android and iOS implementations.

## 0.6.0+1

Updated README to inform users that iOS 10.0+ is needed for use
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 @@ -19,7 +20,6 @@
import android.hardware.camera2.CaptureRequest;
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 @@ -43,6 +43,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 @@ -56,19 +57,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;

// Mirrors camera.dart
public enum ResolutionPreset {
Expand Down Expand Up @@ -111,18 +113,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 @@ -215,10 +217,6 @@ private void writeToFile(ByteBuffer buffer, File file) throws IOException {
}
}

SurfaceTextureEntry getFlutterTexture() {
return flutterTexture;
}

public void takePicture(@NonNull final Result result) {
final File outputDir = applicationContext.getCacheDir();
final File file;
Expand Down Expand Up @@ -362,7 +360,6 @@ private void createCaptureSessionWithSessionConfig(
}

@TargetApi(VERSION_CODES.LOLLIPOP)
@SuppressWarnings("deprecation")
private void createCaptureSession(
List<Surface> surfaces, CameraCaptureSession.StateCallback callback)
throws CameraAccessException {
Expand Down Expand Up @@ -509,6 +506,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 @@ -142,6 +142,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
Loading

0 comments on commit d3d4619

Please sign in to comment.