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

Commit d3d4619

Browse files
committed
Refactored and tested zoom on Android
1 parent b6af039 commit d3d4619

File tree

13 files changed

+663
-23
lines changed

13 files changed

+663
-23
lines changed

packages/camera/camera/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.6.1
2+
3+
* Add zoom support for Android and iOS implementations.
4+
15
## 0.6.0+1
26

37
Updated README to inform users that iOS 10.0+ is needed for use

packages/camera/camera/android/build.gradle

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ buildscript {
99
}
1010

1111
dependencies {
12-
classpath 'com.android.tools.build:gradle:3.3.0'
12+
classpath 'com.android.tools.build:gradle:3.5.0'
1313
}
1414
}
1515

@@ -40,16 +40,16 @@ android {
4040
sourceCompatibility = '1.8'
4141
targetCompatibility = '1.8'
4242
}
43-
dependencies {
44-
implementation 'androidx.annotation:annotation:1.0.0'
45-
implementation 'androidx.core:core:1.0.0'
46-
}
4743
testOptions {
44+
unitTests.includeAndroidResources = true
4845
unitTests.returnDefaultValues = true
4946
}
5047
}
5148

5249
dependencies {
50+
compileOnly 'androidx.annotation:annotation:1.1.0'
5351
testImplementation 'junit:junit:4.12'
5452
testImplementation 'org.mockito:mockito-core:3.5.13'
53+
testImplementation 'androidx.test:core:1.3.0'
54+
testImplementation 'org.robolectric:robolectric:4.3'
5555
}

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import android.app.Activity;
99
import android.content.Context;
1010
import android.graphics.ImageFormat;
11+
import android.graphics.Rect;
1112
import android.graphics.SurfaceTexture;
1213
import android.hardware.camera2.CameraAccessException;
1314
import android.hardware.camera2.CameraCaptureSession;
@@ -19,7 +20,6 @@
1920
import android.hardware.camera2.CaptureRequest;
2021
import android.hardware.camera2.params.OutputConfiguration;
2122
import android.hardware.camera2.params.SessionConfiguration;
22-
import android.hardware.camera2.params.StreamConfigurationMap;
2323
import android.media.CamcorderProfile;
2424
import android.media.Image;
2525
import android.media.ImageReader;
@@ -43,6 +43,7 @@
4343
import java.util.Arrays;
4444
import java.util.HashMap;
4545
import java.util.List;
46+
import java.util.Locale;
4647
import java.util.Map;
4748
import java.util.concurrent.Executors;
4849

@@ -56,19 +57,20 @@ public class Camera {
5657
private final Size captureSize;
5758
private final Size previewSize;
5859
private final boolean enableAudio;
60+
private final Context applicationContext;
61+
private final CamcorderProfile recordingProfile;
62+
private final DartMessenger dartMessenger;
63+
private final CameraZoom cameraZoom;
5964

6065
private CameraDevice cameraDevice;
6166
private CameraCaptureSession cameraCaptureSession;
6267
private ImageReader pictureImageReader;
6368
private ImageReader imageStreamReader;
64-
private DartMessenger dartMessenger;
6569
private CaptureRequest.Builder captureRequestBuilder;
6670
private MediaRecorder mediaRecorder;
6771
private boolean recordingVideo;
6872
private File videoRecordingFile;
69-
private CamcorderProfile recordingProfile;
7073
private int currentOrientation = ORIENTATION_UNKNOWN;
71-
private Context applicationContext;
7274

7375
// Mirrors camera.dart
7476
public enum ResolutionPreset {
@@ -111,18 +113,18 @@ public void onOrientationChanged(int i) {
111113
orientationEventListener.enable();
112114

113115
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName);
114-
StreamConfigurationMap streamConfigurationMap =
115-
characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
116-
//noinspection ConstantConditions
117116
sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
118-
//noinspection ConstantConditions
119117
isFrontFacing =
120118
characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT;
121119
ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset);
122120
recordingProfile =
123121
CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset);
124122
captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
125123
previewSize = computeBestPreviewSize(cameraName, preset);
124+
cameraZoom =
125+
new CameraZoom(
126+
characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE),
127+
characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM));
126128
}
127129

128130
private void prepareMediaRecorder(String outputFilePath) throws IOException {
@@ -215,10 +217,6 @@ private void writeToFile(ByteBuffer buffer, File file) throws IOException {
215217
}
216218
}
217219

218-
SurfaceTextureEntry getFlutterTexture() {
219-
return flutterTexture;
220-
}
221-
222220
public void takePicture(@NonNull final Result result) {
223221
final File outputDir = applicationContext.getCacheDir();
224222
final File file;
@@ -362,7 +360,6 @@ private void createCaptureSessionWithSessionConfig(
362360
}
363361

364362
@TargetApi(VERSION_CODES.LOLLIPOP)
365-
@SuppressWarnings("deprecation")
366363
private void createCaptureSession(
367364
List<Surface> surfaces, CameraCaptureSession.StateCallback callback)
368365
throws CameraAccessException {
@@ -509,6 +506,39 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i
509506
null);
510507
}
511508

509+
public float getMaxZoomLevel() {
510+
return cameraZoom.maxZoom;
511+
}
512+
513+
public float getMinZoomLevel() {
514+
return CameraZoom.DEFAULT_ZOOM_FACTOR;
515+
}
516+
517+
public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException {
518+
float maxZoom = cameraZoom.maxZoom;
519+
float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR;
520+
521+
if (zoom > maxZoom || zoom < minZoom) {
522+
String errorMessage =
523+
String.format(
524+
Locale.ENGLISH,
525+
"Zoom level out of bounds (zoom level should be between %f and %f).",
526+
minZoom,
527+
maxZoom);
528+
result.error("ZOOM_ERROR", errorMessage, null);
529+
return;
530+
}
531+
532+
//Zoom area is calculated relative to sensor area (activeRect)
533+
if (captureRequestBuilder != null) {
534+
final Rect computedZoom = cameraZoom.computeZoom(zoom);
535+
captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom);
536+
cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
537+
}
538+
539+
result.success(null);
540+
}
541+
512542
private void closeCaptureSession() {
513543
if (cameraCaptureSession != null) {
514544
cameraCaptureSession.close();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package io.flutter.plugins.camera;
2+
3+
import android.graphics.Rect;
4+
import androidx.annotation.NonNull;
5+
import androidx.annotation.Nullable;
6+
import androidx.core.math.MathUtils;
7+
8+
public final class CameraZoom {
9+
public static final float DEFAULT_ZOOM_FACTOR = 1.0f;
10+
11+
@NonNull private final Rect cropRegion = new Rect();
12+
@Nullable private final Rect sensorSize;
13+
14+
public final float maxZoom;
15+
public final boolean hasSupport;
16+
17+
public CameraZoom(@Nullable final Rect sensorArraySize, final Float maxZoom) {
18+
this.sensorSize = sensorArraySize;
19+
20+
if (this.sensorSize == null) {
21+
this.maxZoom = DEFAULT_ZOOM_FACTOR;
22+
this.hasSupport = false;
23+
return;
24+
}
25+
26+
this.maxZoom =
27+
((maxZoom == null) || (maxZoom < DEFAULT_ZOOM_FACTOR)) ? DEFAULT_ZOOM_FACTOR : maxZoom;
28+
29+
this.hasSupport = (Float.compare(this.maxZoom, DEFAULT_ZOOM_FACTOR) > 0);
30+
}
31+
32+
public Rect computeZoom(final float zoom) {
33+
if (sensorSize == null || !this.hasSupport) {
34+
return null;
35+
}
36+
37+
final float newZoom = MathUtils.clamp(zoom, DEFAULT_ZOOM_FACTOR, this.maxZoom);
38+
39+
final int centerX = this.sensorSize.width() / 2;
40+
final int centerY = this.sensorSize.height() / 2;
41+
final int deltaX = (int) ((0.5f * this.sensorSize.width()) / newZoom);
42+
final int deltaY = (int) ((0.5f * this.sensorSize.height()) / newZoom);
43+
44+
this.cropRegion.set(centerX - deltaX, centerY - deltaY, centerX + deltaX, centerY + deltaY);
45+
46+
return cropRegion;
47+
}
48+
}

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,49 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
142142
}
143143
break;
144144
}
145+
case "getMaxZoomLevel":
146+
{
147+
assert camera != null;
148+
149+
try {
150+
float maxZoomLevel = camera.getMaxZoomLevel();
151+
result.success(maxZoomLevel);
152+
} catch (Exception e) {
153+
handleException(e, result);
154+
}
155+
break;
156+
}
157+
case "getMinZoomLevel":
158+
{
159+
assert camera != null;
160+
161+
try {
162+
float minZoomLevel = camera.getMinZoomLevel();
163+
result.success(minZoomLevel);
164+
} catch (Exception e) {
165+
handleException(e, result);
166+
}
167+
break;
168+
}
169+
case "setZoomLevel":
170+
{
171+
assert camera != null;
172+
173+
Double zoom = call.argument("zoom");
174+
175+
if (zoom == null) {
176+
result.error(
177+
"ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null);
178+
return;
179+
}
180+
181+
try {
182+
camera.setZoomLevel(result, zoom.floatValue());
183+
} catch (Exception e) {
184+
handleException(e, result);
185+
}
186+
break;
187+
}
145188
case "dispose":
146189
{
147190
if (camera != null) {
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package io.flutter.plugins.camera;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertFalse;
5+
import static org.junit.Assert.assertNotNull;
6+
import static org.junit.Assert.assertNull;
7+
import static org.junit.Assert.assertTrue;
8+
9+
import android.graphics.Rect;
10+
import org.junit.Test;
11+
import org.junit.runner.RunWith;
12+
import org.robolectric.RobolectricTestRunner;
13+
14+
@RunWith(RobolectricTestRunner.class)
15+
public class CameraZoomTest {
16+
17+
@Test
18+
public void ctor_when_parameters_are_valid() {
19+
final Rect sensorSize = new Rect(0, 0, 0, 0);
20+
final Float maxZoom = 4.0f;
21+
final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom);
22+
23+
assertNotNull(cameraZoom);
24+
assertTrue(cameraZoom.hasSupport);
25+
assertEquals(4.0f, cameraZoom.maxZoom, 0);
26+
assertEquals(1.0f, CameraZoom.DEFAULT_ZOOM_FACTOR, 0);
27+
}
28+
29+
@Test
30+
public void ctor_when_sensor_size_is_null() {
31+
final Rect sensorSize = null;
32+
final Float maxZoom = 4.0f;
33+
final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom);
34+
35+
assertNotNull(cameraZoom);
36+
assertFalse(cameraZoom.hasSupport);
37+
assertEquals(cameraZoom.maxZoom, 1.0f, 0);
38+
}
39+
40+
@Test
41+
public void ctor_when_max_zoom_is_null() {
42+
final Rect sensorSize = new Rect(0, 0, 0, 0);
43+
final Float maxZoom = null;
44+
final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom);
45+
46+
assertNotNull(cameraZoom);
47+
assertFalse(cameraZoom.hasSupport);
48+
assertEquals(cameraZoom.maxZoom, 1.0f, 0);
49+
}
50+
51+
@Test
52+
public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() {
53+
final Rect sensorSize = new Rect(0, 0, 0, 0);
54+
final Float maxZoom = 0.5f;
55+
final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom);
56+
57+
assertNotNull(cameraZoom);
58+
assertFalse(cameraZoom.hasSupport);
59+
assertEquals(cameraZoom.maxZoom, 1.0f, 0);
60+
}
61+
62+
@Test
63+
public void setZoom_when_no_support_should_not_set_scaler_crop_region() {
64+
final CameraZoom cameraZoom = new CameraZoom(null, null);
65+
final Rect computedZoom = cameraZoom.computeZoom(2f);
66+
67+
assertNull(computedZoom);
68+
}
69+
70+
@Test
71+
public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_zero() {
72+
final Rect sensorSize = new Rect(0, 0, 0, 0);
73+
final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f);
74+
final Rect computedZoom = cameraZoom.computeZoom(18f);
75+
76+
assertNotNull(computedZoom);
77+
assertEquals(computedZoom.left, 0);
78+
assertEquals(computedZoom.top, 0);
79+
assertEquals(computedZoom.right, 0);
80+
assertEquals(computedZoom.bottom, 0);
81+
}
82+
83+
@Test
84+
public void setZoom_when_sensor_size_is_valid_should_return_crop_region() {
85+
final Rect sensorSize = new Rect(0, 0, 100, 100);
86+
final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f);
87+
final Rect computedZoom = cameraZoom.computeZoom(18f);
88+
89+
assertNotNull(computedZoom);
90+
assertEquals(computedZoom.left, 48);
91+
assertEquals(computedZoom.top, 48);
92+
assertEquals(computedZoom.right, 52);
93+
assertEquals(computedZoom.bottom, 52);
94+
}
95+
96+
@Test
97+
public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() {
98+
final Rect sensorSize = new Rect(0, 0, 100, 100);
99+
final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f);
100+
final Rect computedZoom = cameraZoom.computeZoom(25f);
101+
102+
assertNotNull(computedZoom);
103+
assertEquals(computedZoom.left, 45);
104+
assertEquals(computedZoom.top, 45);
105+
assertEquals(computedZoom.right, 55);
106+
assertEquals(computedZoom.bottom, 55);
107+
}
108+
109+
@Test
110+
public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() {
111+
final Rect sensorSize = new Rect(0, 0, 100, 100);
112+
final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f);
113+
final Rect computedZoom = cameraZoom.computeZoom(0.5f);
114+
115+
assertNotNull(computedZoom);
116+
assertEquals(computedZoom.left, 0);
117+
assertEquals(computedZoom.top, 0);
118+
assertEquals(computedZoom.right, 100);
119+
assertEquals(computedZoom.bottom, 100);
120+
}
121+
}

packages/camera/camera/example/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ buildscript {
55
}
66

77
dependencies {
8-
classpath 'com.android.tools.build:gradle:3.3.0'
8+
classpath 'com.android.tools.build:gradle:3.5.0'
99
}
1010
}
1111

0 commit comments

Comments
 (0)