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

Commit b289e1a

Browse files
authored
[camera] Zoom functionality for Android and iOS (#3315)
* Refactored and tested zoom on Android * Fix merge conflict
1 parent 0c6ed04 commit b289e1a

File tree

13 files changed

+662
-21
lines changed

13 files changed

+662
-21
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.2
2+
3+
* Add zoom support for Android and iOS implementations.
4+
15
## 0.6.1+1
26

37
* Added implementation of the `didFinishProcessingPhoto` on iOS which allows saving image metadata (EXIF) on iOS 11 and up.

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 & 12 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;
@@ -21,7 +22,6 @@
2122
import android.hardware.camera2.TotalCaptureResult;
2223
import android.hardware.camera2.params.OutputConfiguration;
2324
import android.hardware.camera2.params.SessionConfiguration;
24-
import android.hardware.camera2.params.StreamConfigurationMap;
2525
import android.media.CamcorderProfile;
2626
import android.media.Image;
2727
import android.media.ImageReader;
@@ -47,6 +47,7 @@
4747
import java.util.Arrays;
4848
import java.util.HashMap;
4949
import java.util.List;
50+
import java.util.Locale;
5051
import java.util.Map;
5152
import java.util.concurrent.Executors;
5253

@@ -60,19 +61,20 @@ public class Camera {
6061
private final Size captureSize;
6162
private final Size previewSize;
6263
private final boolean enableAudio;
64+
private final Context applicationContext;
65+
private final CamcorderProfile recordingProfile;
66+
private final DartMessenger dartMessenger;
67+
private final CameraZoom cameraZoom;
6368

6469
private CameraDevice cameraDevice;
6570
private CameraCaptureSession cameraCaptureSession;
6671
private ImageReader pictureImageReader;
6772
private ImageReader imageStreamReader;
68-
private DartMessenger dartMessenger;
6973
private CaptureRequest.Builder captureRequestBuilder;
7074
private MediaRecorder mediaRecorder;
7175
private boolean recordingVideo;
7276
private File videoRecordingFile;
73-
private CamcorderProfile recordingProfile;
7477
private int currentOrientation = ORIENTATION_UNKNOWN;
75-
private Context applicationContext;
7678
private FlashMode flashMode;
7779
private PictureCaptureRequest pictureCaptureRequest;
7880

@@ -108,18 +110,18 @@ public void onOrientationChanged(int i) {
108110
orientationEventListener.enable();
109111

110112
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName);
111-
StreamConfigurationMap streamConfigurationMap =
112-
characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
113-
//noinspection ConstantConditions
114113
sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
115-
//noinspection ConstantConditions
116114
isFrontFacing =
117115
characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT;
118116
ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset);
119117
recordingProfile =
120118
CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset);
121119
captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
122120
previewSize = computeBestPreviewSize(cameraName, preset);
121+
cameraZoom =
122+
new CameraZoom(
123+
characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE),
124+
characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM));
123125
}
124126

125127
private void prepareMediaRecorder(String outputFilePath) throws IOException {
@@ -212,10 +214,6 @@ private void writeToFile(ByteBuffer buffer, File file) throws IOException {
212214
}
213215
}
214216

215-
SurfaceTextureEntry getFlutterTexture() {
216-
return flutterTexture;
217-
}
218-
219217
public void takePicture(@NonNull final Result result) {
220218
// Only take 1 picture at a time
221219
if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) {
@@ -620,6 +618,39 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i
620618
null);
621619
}
622620

621+
public float getMaxZoomLevel() {
622+
return cameraZoom.maxZoom;
623+
}
624+
625+
public float getMinZoomLevel() {
626+
return CameraZoom.DEFAULT_ZOOM_FACTOR;
627+
}
628+
629+
public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException {
630+
float maxZoom = cameraZoom.maxZoom;
631+
float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR;
632+
633+
if (zoom > maxZoom || zoom < minZoom) {
634+
String errorMessage =
635+
String.format(
636+
Locale.ENGLISH,
637+
"Zoom level out of bounds (zoom level should be between %f and %f).",
638+
minZoom,
639+
maxZoom);
640+
result.error("ZOOM_ERROR", errorMessage, null);
641+
return;
642+
}
643+
644+
//Zoom area is calculated relative to sensor area (activeRect)
645+
if (captureRequestBuilder != null) {
646+
final Rect computedZoom = cameraZoom.computeZoom(zoom);
647+
captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom);
648+
cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
649+
}
650+
651+
result.success(null);
652+
}
653+
623654
private void closeCaptureSession() {
624655
if (cameraCaptureSession != null) {
625656
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
@@ -158,6 +158,49 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
158158
}
159159
break;
160160
}
161+
case "getMaxZoomLevel":
162+
{
163+
assert camera != null;
164+
165+
try {
166+
float maxZoomLevel = camera.getMaxZoomLevel();
167+
result.success(maxZoomLevel);
168+
} catch (Exception e) {
169+
handleException(e, result);
170+
}
171+
break;
172+
}
173+
case "getMinZoomLevel":
174+
{
175+
assert camera != null;
176+
177+
try {
178+
float minZoomLevel = camera.getMinZoomLevel();
179+
result.success(minZoomLevel);
180+
} catch (Exception e) {
181+
handleException(e, result);
182+
}
183+
break;
184+
}
185+
case "setZoomLevel":
186+
{
187+
assert camera != null;
188+
189+
Double zoom = call.argument("zoom");
190+
191+
if (zoom == null) {
192+
result.error(
193+
"ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null);
194+
return;
195+
}
196+
197+
try {
198+
camera.setZoomLevel(result, zoom.floatValue());
199+
} catch (Exception e) {
200+
handleException(e, result);
201+
}
202+
break;
203+
}
161204
case "dispose":
162205
{
163206
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

packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
33
zipStoreBase=GRADLE_USER_HOME
44
zipStorePath=wrapper/dists
5-
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
5+
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip

0 commit comments

Comments
 (0)