Skip to content

Commit ae9eab3

Browse files
abrahambotrosZack Story
authored andcommitted
(android): Android tap-to-focus and improved (continuous) auto-focus (react-native-camera#575)
* Android tap-to-focus and improved (continuous) auto-focus Tap-to-focus - On tap, compute focus area around motion event's location, and pass this to the camera parameters as the new focus area. - Adds RCTCameraUtils.java file, so far with only a single function that helps compute the focus area from a motion event. This file can serve as a location for utility constants and functions for the rest of the app, where such things can be extracted out. Improved (continuous) auto-focus - Use FOCUS_MODE_CONTINUOUS_PICTURE/VIDEO when possible to enable continuous auto-focus; fall back to FOCUS_MODE_AUTO otherwise, if able. Other changes - Update README to specify differences between iOS and Android for focus and zoom functionality. - Update AndroidManifest with more thorough list of permissions and features. - Update Example package.json react and react-native dependencies to match root package's package.json. * Example: default empty onFocusChanged callback - Enables default tap-to-focus behavior in Example app, facilitating testing of focus features in the Example app
1 parent 20b0721 commit ae9eab3

File tree

5 files changed

+179
-28
lines changed

5 files changed

+179
-28
lines changed

Example/android/app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
1111
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
1212

13+
<uses-permission android:name="android.permission.CAMERA" />
14+
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
15+
<uses-permission android:name="android.permission.RECORD_VIDEO"/>
16+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
17+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
18+
19+
<uses-feature android:name="android.hardware.camera" android:required="false" />
20+
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
21+
1322
<uses-sdk
1423
android:minSdkVersion="16"
1524
android:targetSdkVersion="22" />

Example/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
"start": "node node_modules/react-native/local-cli/cli.js start"
77
},
88
"dependencies": {
9-
"react": "~15.3.0",
10-
"react-native": "^0.34.0",
9+
"react": ">=15.4.0",
10+
"react-native": ">=0.40",
1111
"react-native-camera": "file:../"
1212
}
1313
}

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,24 +260,32 @@ Values:
260260

261261
Use the `torchMode` property to specify the camera torch mode.
262262

263-
#### `onFocusChanged: Event { nativeEvent: { touchPoint: { x, y } }`
263+
#### `iOS` `onFocusChanged: Event { nativeEvent: { touchPoint: { x, y } }`
264264

265-
Called when a touch focus gesture has been made.
265+
iOS: Called when a touch focus gesture has been made.
266266
By default, `onFocusChanged` is not defined and tap-to-focus is disabled.
267267

268-
#### `defaultOnFocusComponent`
268+
Android: This callback is not yet implemented. However, Android will
269+
automatically do tap-to-focus if the device supports auto-focus; there is
270+
currently no way to manage this from javascript.
271+
272+
#### `iOS` `defaultOnFocusComponent`
269273

270274
Values:
271275
`true` (default)
272276
`false`
273277

274278
If `defaultOnFocusComponent` set to false, default internal implementation of visual feedback for tap-to-focus gesture will be disabled.
275279

276-
#### `onZoomChanged: Event { nativeEvent: { velocity, zoomFactor } }`
280+
#### `iOS` `onZoomChanged: Event { nativeEvent: { velocity, zoomFactor } }`
277281

278-
Called when focus has changed.
282+
iOS: Called when focus has changed.
279283
By default, `onZoomChanged` is not defined and pinch-to-zoom is disabled.
280284

285+
Android: This callback is not yet implemented. However, Android will
286+
automatically handle pinch-to-zoom; there is currently no way to manage this
287+
from javascript.
288+
281289
#### `iOS` `keepAwake`
282290

283291
If set to `true`, the device will not sleep while the camera preview is visible. This mimics the behavior of the default camera app, which keeps the device awake while open.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.lwansbrough.RCTCamera;
2+
3+
import android.graphics.Rect;
4+
import android.graphics.RectF;
5+
import android.hardware.Camera;
6+
import android.view.MotionEvent;
7+
8+
public class RCTCameraUtils {
9+
private static final int FOCUS_AREA_MOTION_EVENT_EDGE_LENGTH = 100;
10+
private static final int FOCUS_AREA_WEIGHT = 1000;
11+
12+
/**
13+
* Computes a Camera.Area corresponding to the new focus area to focus the camera on. This is
14+
* done by deriving a square around the center of a MotionEvent pointer (with side length equal
15+
* to FOCUS_AREA_MOTION_EVENT_EDGE_LENGTH), then transforming this rectangle's/square's
16+
* coordinates into the (-1000, 1000) coordinate system used for camera focus areas.
17+
*
18+
* Also note that we operate on RectF instances for the most part, to avoid any integer
19+
* division rounding errors going forward. We only round at the very end for playing into
20+
* the final focus areas list.
21+
*
22+
* @throws RuntimeException if unable to compute valid intersection between MotionEvent region
23+
* and SurfaceTexture region.
24+
*/
25+
protected static Camera.Area computeFocusAreaFromMotionEvent(final MotionEvent event, final int surfaceTextureWidth, final int surfaceTextureHeight) {
26+
// Get position of first touch pointer.
27+
final int pointerId = event.getPointerId(0);
28+
final int pointerIndex = event.findPointerIndex(pointerId);
29+
final float centerX = event.getX(pointerIndex);
30+
final float centerY = event.getY(pointerIndex);
31+
32+
// Build event rect. Note that coordinates increase right and down, such that left <= right
33+
// and top <= bottom.
34+
final RectF eventRect = new RectF(
35+
centerX - FOCUS_AREA_MOTION_EVENT_EDGE_LENGTH, // left
36+
centerY - FOCUS_AREA_MOTION_EVENT_EDGE_LENGTH, // top
37+
centerX + FOCUS_AREA_MOTION_EVENT_EDGE_LENGTH, // right
38+
centerY + FOCUS_AREA_MOTION_EVENT_EDGE_LENGTH // bottom
39+
);
40+
41+
// Intersect this rect with the rect corresponding to the full area of the parent surface
42+
// texture, making sure we are not placing any amount of the eventRect outside the parent
43+
// surface's area.
44+
final RectF surfaceTextureRect = new RectF(
45+
(float) 0, // left
46+
(float) 0, // top
47+
(float) surfaceTextureWidth, // right
48+
(float) surfaceTextureHeight // bottom
49+
);
50+
final boolean intersectSuccess = eventRect.intersect(surfaceTextureRect);
51+
if (!intersectSuccess) {
52+
throw new RuntimeException(
53+
"MotionEvent rect does not intersect with SurfaceTexture rect; unable to " +
54+
"compute focus area"
55+
);
56+
}
57+
58+
// Transform into (-1000, 1000) focus area coordinate system. See
59+
// https://developer.android.com/reference/android/hardware/Camera.Area.html.
60+
// Note that if this is ever changed to a Rect instead of RectF, be cautious of integer
61+
// division rounding!
62+
final RectF focusAreaRect = new RectF(
63+
(eventRect.left / surfaceTextureWidth) * 2000 - 1000, // left
64+
(eventRect.top / surfaceTextureHeight) * 2000 - 1000, // top
65+
(eventRect.right / surfaceTextureWidth) * 2000 - 1000, // right
66+
(eventRect.bottom / surfaceTextureHeight) * 2000 - 1000 // bottom
67+
);
68+
Rect focusAreaRectRounded = new Rect();
69+
focusAreaRect.round(focusAreaRectRounded);
70+
return new Camera.Area(focusAreaRectRounded, FOCUS_AREA_WEIGHT);
71+
}
72+
}

android/src/main/java/com/lwansbrough/RCTCamera/RCTCameraViewFinder.java

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package com.lwansbrough.RCTCamera;
66

77
import android.content.Context;
8+
import android.graphics.Rect;
89
import android.graphics.SurfaceTexture;
910
import android.hardware.Camera;
1011
import android.view.MotionEvent;
@@ -16,6 +17,7 @@
1617
import com.facebook.react.bridge.WritableMap;
1718
import com.facebook.react.modules.core.DeviceEventManagerModule;
1819

20+
import java.util.ArrayList;
1921
import java.util.List;
2022
import java.util.EnumMap;
2123
import java.util.EnumSet;
@@ -32,6 +34,8 @@ class RCTCameraViewFinder extends TextureView implements TextureView.SurfaceText
3234
private int _cameraType;
3335
private int _captureMode;
3436
private SurfaceTexture _surfaceTexture;
37+
private int _surfaceTextureWidth;
38+
private int _surfaceTextureHeight;
3539
private boolean _isStarting;
3640
private boolean _isStopping;
3741
private Camera _camera;
@@ -53,16 +57,22 @@ public RCTCameraViewFinder(Context context, int type) {
5357
@Override
5458
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
5559
_surfaceTexture = surface;
60+
_surfaceTextureWidth = width;
61+
_surfaceTextureHeight = height;
5662
startCamera();
5763
}
5864

5965
@Override
6066
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
67+
_surfaceTextureWidth = width;
68+
_surfaceTextureHeight = height;
6169
}
6270

6371
@Override
6472
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
6573
_surfaceTexture = null;
74+
_surfaceTextureWidth = 0;
75+
_surfaceTextureHeight = 0;
6676
stopCamera();
6777
return true;
6878
}
@@ -126,17 +136,30 @@ synchronized private void startCamera() {
126136
try {
127137
_camera = RCTCamera.getInstance().acquireCameraInstance(_cameraType);
128138
Camera.Parameters parameters = _camera.getParameters();
129-
// set autofocus
139+
140+
final boolean isCaptureModeStill = (_captureMode == RCTCameraModule.RCT_CAMERA_CAPTURE_MODE_STILL);
141+
final boolean isCaptureModeVideo = (_captureMode == RCTCameraModule.RCT_CAMERA_CAPTURE_MODE_VIDEO);
142+
if (!isCaptureModeStill && !isCaptureModeVideo) {
143+
throw new RuntimeException("Unsupported capture mode:" + _captureMode);
144+
}
145+
146+
// Set auto-focus. Try to set to continuous picture/video, and fall back to general
147+
// auto if available.
130148
List<String> focusModes = parameters.getSupportedFocusModes();
131-
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
149+
if (isCaptureModeStill && focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
132150
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
151+
} else if (isCaptureModeVideo && focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
152+
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
153+
} else if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
154+
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
133155
}
156+
134157
// set picture size
135158
// defaults to max available size
136159
List<Camera.Size> supportedSizes;
137-
if (_captureMode == RCTCameraModule.RCT_CAMERA_CAPTURE_MODE_STILL) {
160+
if (isCaptureModeStill) {
138161
supportedSizes = parameters.getSupportedPictureSizes();
139-
} else if (_captureMode == RCTCameraModule.RCT_CAMERA_CAPTURE_MODE_VIDEO) {
162+
} else if (isCaptureModeVideo) {
140163
supportedSizes = RCTCamera.getInstance().getSupportedVideoSizes(_camera);
141164
} else {
142165
throw new RuntimeException("Unsupported capture mode:" + _captureMode);
@@ -288,15 +311,15 @@ protected Void doInBackground(Void... ignored) {
288311

289312
// rotate for zxing if orientation is portrait
290313
if (RCTCamera.getInstance().getActualDeviceOrientation() == 0) {
291-
byte[] rotated = new byte[imageData.length];
292-
for (int y = 0; y < height; y++) {
293-
for (int x = 0; x < width; x++) {
294-
rotated[x * height + height - y - 1] = imageData[x + y * width];
314+
byte[] rotated = new byte[imageData.length];
315+
for (int y = 0; y < height; y++) {
316+
for (int x = 0; x < width; x++) {
317+
rotated[x * height + height - y - 1] = imageData[x + y * width];
318+
}
295319
}
296-
}
297-
width = size.height;
298-
height = size.width;
299-
imageData = rotated;
320+
width = size.height;
321+
height = size.width;
322+
imageData = rotated;
300323
}
301324

302325
try {
@@ -362,24 +385,63 @@ private void handleZoom(MotionEvent event, Camera.Parameters params) {
362385
_camera.setParameters(params);
363386
}
364387

388+
/**
389+
* Handles setting focus to the location of the event.
390+
*
391+
* Note that this will override the focus mode on the camera to FOCUS_MODE_AUTO if available,
392+
* even if this was previously something else (such as FOCUS_MODE_CONTINUOUS_*; see also
393+
* {@link #startCamera()}. However, this makes sense - after the user has initiated any
394+
* specific focus intent, we shouldn't be refocusing and overriding their request!
395+
*/
365396
public void handleFocus(MotionEvent event, Camera.Parameters params) {
366-
int pointerId = event.getPointerId(0);
367-
int pointerIndex = event.findPointerIndex(pointerId);
368-
// Get the pointer's current position
369-
float x = event.getX(pointerIndex);
370-
float y = event.getY(pointerIndex);
371-
372397
List<String> supportedFocusModes = params.getSupportedFocusModes();
373398
if (supportedFocusModes != null && supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
399+
// Ensure focus areas are enabled. If max num focus areas is 0, then focus area is not
400+
// supported, so we cannot do anything here.
401+
if (params.getMaxNumFocusAreas() == 0) {
402+
return;
403+
}
404+
405+
// Cancel any previous focus actions.
406+
_camera.cancelAutoFocus();
407+
408+
// Compute focus area rect.
409+
Camera.Area focusAreaFromMotionEvent;
410+
try {
411+
focusAreaFromMotionEvent = RCTCameraUtils.computeFocusAreaFromMotionEvent(event, _surfaceTextureWidth, _surfaceTextureHeight);
412+
} catch (final RuntimeException e) {
413+
e.printStackTrace();
414+
return;
415+
}
416+
417+
// Set focus mode to auto.
418+
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
419+
// Set focus area.
420+
final ArrayList<Camera.Area> focusAreas = new ArrayList<Camera.Area>();
421+
focusAreas.add(focusAreaFromMotionEvent);
422+
params.setFocusAreas(focusAreas);
423+
424+
// Also set metering area if enabled. If max num metering areas is 0, then metering area
425+
// is not supported. We can usually safely omit this anyway, though.
426+
if (params.getMaxNumMeteringAreas() > 0) {
427+
params.setMeteringAreas(focusAreas);
428+
}
429+
430+
// Set parameters before starting auto-focus.
431+
_camera.setParameters(params);
432+
433+
// Start auto-focus now that focus area has been set. If successful, then can cancel
434+
// it afterwards. Wrap in try-catch to avoid crashing on merely autoFocus fails.
374435
try {
375436
_camera.autoFocus(new Camera.AutoFocusCallback() {
376437
@Override
377-
public void onAutoFocus(boolean b, Camera camera) {
378-
// currently set to auto-focus on single touch
438+
public void onAutoFocus(boolean success, Camera camera) {
439+
if (success) {
440+
camera.cancelAutoFocus();
441+
}
379442
}
380443
});
381444
} catch (Exception e) {
382-
// just print stack trace, we don't want to crash by autoFocus fails
383445
e.printStackTrace();
384446
}
385447
}

0 commit comments

Comments
 (0)