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 3c1495c commit 6a9ccb4
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 50 deletions.
11 changes: 6 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,17 @@ 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 @@ -17,7 +17,6 @@
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.media.Image;
import android.media.ImageReader;
Expand Down Expand Up @@ -52,21 +51,21 @@ public class Camera {
private final Size captureSize;
private final Size previewSize;
private final boolean enableAudio;
private final DartMessenger dartMessenger;
private final CamcorderProfile recordingProfile;
private final Context applicationContext;
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 CameraCharacteristics cameraCharacteristics;
private Rect activeArraySize;


// Mirrors camera.dart
public enum ResolutionPreset {
Expand Down Expand Up @@ -108,12 +107,9 @@ public void onOrientationChanged(int i) {
};
orientationEventListener.enable();

cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName);
StreamConfigurationMap streamConfigurationMap =
cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
//noinspection ConstantConditions
CameraCharacteristics cameraCharacteristics = cameraManager
.getCameraCharacteristics(cameraName);
sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
//noinspection ConstantConditions
isFrontFacing =
cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)
== CameraMetadata.LENS_FACING_FRONT;
Expand All @@ -122,10 +118,9 @@ public void onOrientationChanged(int i) {
CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset);
captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
previewSize = computeBestPreviewSize(cameraName, preset);

// Store the sensor size to be able to perform zoom and focus calculations.
activeArraySize =
cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
cameraZoom = new CameraZoom(
cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE),
cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM));
}

private void prepareMediaRecorder(String outputFilePath) throws IOException {
Expand Down Expand Up @@ -218,10 +213,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 @@ -515,16 +506,16 @@ public void close() {
}

public float getMaxZoomLevel() {
return cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
return cameraZoom.maxZoom;
}

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

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

if (zoom > maxZoom || zoom < minZoom) {
String errorMessage =
Expand All @@ -538,33 +529,15 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera
}

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

result.success(null);
}

//Calculate zoom area according input image size
private static Rect getZoomRect(float zoom, int imgWidth, int imgHeight) {
int cropWidth = (int) (imgWidth / zoom) + 2 * 64;
int cropHeight = (int) (imgHeight / zoom) + 2 * 64;
// ensure crop w,h divisible by 4 (SZ requirement)
cropWidth -= cropWidth & 3;
cropHeight -= cropHeight & 3;
// crop area for standard frame
int cropWidthStd = cropWidth - 2 * 64;
int cropHeightStd = cropHeight - 2 * 64;

return new Rect(
(imgWidth - cropWidthStd) / 2,
(imgHeight - cropHeightStd) / 2,
(imgWidth + cropWidthStd) / 2,
(imgHeight + cropHeightStd) / 2);
}

public void dispose() {
close();
flutterTexture.release();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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 @@ -144,6 +144,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
}
case "getMaxZoomLevel":
{
assert camera != null;

try {
float maxZoomLevel = camera.getMaxZoomLevel();
result.success(maxZoomLevel);
Expand All @@ -154,6 +156,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
}
case "getMinZoomLevel":
{
assert camera != null;

try {
float minZoomLevel = camera.getMinZoomLevel();
result.success(minZoomLevel);
Expand All @@ -164,6 +168,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
}
case "setZoomLevel":
{
assert camera != null;

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

if (zoom == 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.assertNotNull;
import static org.junit.Assert.assertFalse;
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

0 comments on commit 6a9ccb4

Please sign in to comment.