Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ To have Play Services [pre-download the model in the background after the app is
| `zoom` | `number` | Control the zoom. Default: `1.0` |
| `maxZoom` | `number` | Maximum zoom allowed (but not beyond what camera allows). Default: `undefined` (camera default max) |
| `onZoom` | Function | Callback when user makes a pinch gesture, regardless of what the `zoom` prop was set to. Returned event contains `zoom`. Ex: `onZoom={(e) => console.log(e.nativeEvent.zoom)}`. |
| `onTapToFocus` | Function | Callback when the user taps the preview to focus, fired alongside the built-in tap-to-focus so an overlay can be shown without suppressing focus and zoom. Returned event contains `x` and `y`, normalized 0-1 in preview space. Requires `focusMode` `'on'`. Ex: `onTapToFocus={(e) => console.log(e.nativeEvent.x, e.nativeEvent.y)}`. |
| `torchMode` | `'on'`/`'off'` | Toggle flash light when camera is active. Default: `off` |
| `cameraType` | CameraType.Back/CameraType.Front | Choose what camera to use. Default: `CameraType.Back` |
| `onOrientationChange` | Function | Callback when physical device orientation changes. Returned event contains `orientation`. Ex: `onOrientationChange={(event) => console.log(event.nativeEvent.orientation)}`. Use `import { Orientation } from 'react-native-camera-kit'; if (event.nativeEvent.orientation === Orientation.PORTRAIT) { ... }` to understand the new value |
Expand Down
22 changes: 22 additions & 0 deletions android/src/main/java/com/rncamerakit/CKCamera.kt
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
return@setOnTouchListener scaleDetector.onTouchEvent(event)
}
focusOnPoint(event.x, event.y)
onTapToFocus(event.x, event.y)
return@setOnTouchListener true
}

Expand Down Expand Up @@ -722,6 +723,27 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
?.dispatchEvent(ZoomEvent(surfaceId, id, desiredOrCameraZoom))
}

// Reported alongside the tap-to-focus above so JS can overlay its own UI
// without having to intercept the touch and suppress focus and zoom.
// Normalized to 0..1 of the preview, matching the bounds onFaceDetected reports.
private fun onTapToFocus(x: Float, y: Float) {
val width = viewFinder.width
val height = viewFinder.height
if (width <= 0 || height <= 0) return

val surfaceId = UIManagerHelper.getSurfaceId(currentContext)
UIManagerHelper
.getEventDispatcherForReactTag(currentContext, id)
?.dispatchEvent(
TapToFocusEvent(
surfaceId,
id,
(x / width).toDouble().coerceIn(0.0, 1.0),
(y / height).toDouble().coerceIn(0.0, 1.0),
)
)
}

fun setMaxZoom(factor: Double?) {
maxZoom = factor

Expand Down
24 changes: 24 additions & 0 deletions android/src/main/java/com/rncamerakit/events/TapToFocusEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.rncamerakit.events

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

class TapToFocusEvent(
surfaceId: Int,
viewId: Int,
private val x: Double,
private val y: Double,
) : Event<TapToFocusEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME

override fun getEventData(): WritableMap =
Arguments.createMap().apply {
putDouble("x", x)
putDouble("y", y)
}

companion object {
const val EVENT_NAME = "topTapToFocus"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class CKCameraManager(context: ReactApplicationContext) : SimpleViewManager<CKCa
ReadCodeEvent.EVENT_NAME to mapOf("registrationName" to "onReadCode"),
PictureTakenEvent.EVENT_NAME to mapOf("registrationName" to "onPictureTaken"),
ZoomEvent.EVENT_NAME to mapOf("registrationName" to "onZoom"),
TapToFocusEvent.EVENT_NAME to mapOf("registrationName" to "onTapToFocus"),
ErrorEvent.EVENT_NAME to mapOf("registrationName" to "onError"),
CaptureButtonPressInEvent.EVENT_NAME to mapOf("registrationName" to "onCaptureButtonPressIn"),
CaptureButtonPressOutEvent.EVENT_NAME to mapOf("registrationName" to "onCaptureButtonPressOut"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class CKCameraManager(var context: ReactApplicationContext) : SimpleViewManager<
ReadCodeEvent.EVENT_NAME to mapOf("registrationName" to "onReadCode"),
PictureTakenEvent.EVENT_NAME to mapOf("registrationName" to "onPictureTaken"),
ZoomEvent.EVENT_NAME to mapOf("registrationName" to "onZoom"),
TapToFocusEvent.EVENT_NAME to mapOf("registrationName" to "onTapToFocus"),
ErrorEvent.EVENT_NAME to mapOf("registrationName" to "onError"),
CaptureButtonPressInEvent.EVENT_NAME to mapOf("registrationName" to "onCaptureButtonPressIn"),
CaptureButtonPressOutEvent.EVENT_NAME to mapOf("registrationName" to "onCaptureButtonPressOut"),
Expand Down
5 changes: 5 additions & 0 deletions example/src/CameraExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,11 @@ const CameraExample = ({ onBack, stress }: { onBack: () => void; stress?: boolea
console.log('zoom', e.nativeEvent.zoom);
setZoom(e.nativeEvent.zoom);
}}
onTapToFocus={(e) => {
// Fires alongside the camera's own tap-to-focus, so an overlay can
// react to the tap without suppressing focus and zoom
console.log('tapToFocus', e.nativeEvent.x, e.nativeEvent.y);
}}
torchMode={torchMode ? 'on' : 'off'}
shutterPhotoSound
maxPhotoQualityPrioritization="speed"
Expand Down
1 change: 1 addition & 0 deletions ios/ReactNativeCameraKit/CKCameraManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ @interface RCT_EXTERN_MODULE (CKCameraManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(onCaptureButtonPressIn, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCaptureButtonPressOut, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onZoom, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onTapToFocus, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(resetFocusTimeout, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(resetFocusWhenMotionDetected, BOOL)
RCT_EXPORT_VIEW_PROPERTY(focusMode, CKFocusMode)
Expand Down
11 changes: 11 additions & 0 deletions ios/ReactNativeCameraKit/CKCameraViewComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ - (void)prepareView {
->onZoom({.zoom = [zoom doubleValue]});
}
}];
[_view setOnTapToFocus:^(NSDictionary *event) {
__typeof__(self) strongSelf = weakSelf;

if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
id x = [event valueForKey:@"x"] == nil ? 0 : [event valueForKey:@"x"];
id y = [event valueForKey:@"y"] == nil ? 0 : [event valueForKey:@"y"];
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(
strongSelf->_eventEmitter)
->onTapToFocus({.x = [x doubleValue], .y = [y doubleValue]});
}
}];
[_view setOnCaptureButtonPressIn:^(NSDictionary *event) {
__typeof__(self) strongSelf = weakSelf;

Expand Down
4 changes: 4 additions & 0 deletions ios/ReactNativeCameraKit/CameraView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public class CameraView: UIView {
// other
@objc public var onOrientationChange: RCTDirectEventBlock?
@objc public var onZoom: RCTDirectEventBlock?
@objc public var onTapToFocus: RCTDirectEventBlock?
@objc public var resetFocusTimeout = 0
@objc public var resetFocusWhenMotionDetected = false
@objc public var focusMode: FocusMode = .on
Expand Down Expand Up @@ -157,6 +158,9 @@ public class CameraView: UIView {
addFullSizeSubview(focusInterfaceView)

focusInterfaceView.delegate = camera
focusInterfaceView.onTap = { [weak self] x, y in
self?.onTapToFocus?(["x": x, "y": y])
}

handleCameraPermission()

Expand Down
9 changes: 9 additions & 0 deletions ios/ReactNativeCameraKit/FocusInterfaceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ protocol FocusInterfaceViewDelegate: AnyObject {
class FocusInterfaceView: UIView {
weak var delegate: FocusInterfaceViewDelegate?

// Reported alongside the focus itself so JS can overlay its own UI without
// intercepting the touch, which would suppress focus and zoom.
// Coordinates are normalized to 0...1 of this view, which is full-size.
var onTap: ((_ x: CGFloat, _ y: CGFloat) -> Void)?

private var resetFocusTimeout = 0
private var resetFocusWhenMotionDetected = false

Expand Down Expand Up @@ -175,6 +180,10 @@ class FocusInterfaceView: UIView {

@objc func focusAndExposeTap(_ gestureRecognizer: UIGestureRecognizer) {
let touchPoint = gestureRecognizer.location(in: self)
if let onTap, bounds.width > 0, bounds.height > 0 {
onTap(min(max(touchPoint.x / bounds.width, 0), 1),
min(max(touchPoint.y / bounds.height, 0), 1))
}
delegate?.focus(at: touchPoint,
focusBehavior: .customFocus(resetFocusWhenMotionDetected: resetFocusWhenMotionDetected,
resetFocus: resetFocus,
Expand Down
30 changes: 30 additions & 0 deletions src/CameraProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export type OnZoom = {
};
};

export type OnTapToFocusData = {
nativeEvent: {
/** Horizontal position of the tap, normalized 0-1 across the preview's width */
x: number;
/** Vertical position of the tap, normalized 0-1 across the preview's height */
y: number;
};
};

export type FaceData = {
id: number;
yaw: number;
Expand Down Expand Up @@ -125,6 +134,27 @@ export interface CameraProps extends ViewProps {
* ```
*/
onZoom?: (event: OnZoom) => void;
/**
* Callback triggered when the user taps the preview to focus.
* Fires alongside the built-in tap-to-focus, so an overlay can be shown
* without replacing the camera's own gesture handling.
*
* Coordinates are normalized 0-1 in preview space, matching `onFaceDetected`.
* Requires `focusMode="on"` (the default), since that is when the
* tap-to-focus gesture is active.
*
* Note this is deliberately not called `onFocus`: `ViewProps` already
* declares that prop for accessibility focus.
* Example:
* ```
* <Camera
* onTapToFocus={(e) => {
* console.log('tapped at', e.nativeEvent.x, e.nativeEvent.y);
* }}
* />
* ```
*/
onTapToFocus?: (event: OnTapToFocusData) => void;
/** **Android only**. Triggered when camera fails to initialize */
onError?: (event: { nativeEvent: { errorMessage: string } }) => void;
// Barcode only
Expand Down
6 changes: 6 additions & 0 deletions src/specs/CameraNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ type OnZoom = {
zoom: Double;
}

type OnTapToFocus = {
x: Double;
y: Double;
};

type OnFaceDetectedData = {
faces: {
id: Int32;
Expand Down Expand Up @@ -67,6 +72,7 @@ export interface NativeProps extends ViewProps {
shutterPhotoSound?: boolean;
onOrientationChange?: DirectEventHandler<OnOrientationChangeData>;
onZoom?: DirectEventHandler<OnZoom>;
onTapToFocus?: DirectEventHandler<OnTapToFocus>;
onError?: DirectEventHandler<{errorMessage: string }>;
onReadCode?: DirectEventHandler<OnReadCodeData>;
onCaptureButtonPressIn?: DirectEventHandler<{}>;
Expand Down