Added zoom control

Added max zoom control
Added onZoom handler
This commit is contained in:
Seph Soliman 2023-07-11 17:54:49 -07:00
parent 6e054145bf
commit f58bc77ef1
15 changed files with 268 additions and 29 deletions

View File

@ -9,8 +9,6 @@ jobs:
uses: actions/checkout@v2
- name: Install modules
run: yarn
- name: Bootstrap
run: yarn bootstrap
- name: Build
run: cd example/ios && xcodebuild -workspace CameraKitExample.xcworkspace -configuration Debug -scheme CameraKitExample -sdk iphoneos build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
build-example-android:
@ -23,7 +21,5 @@ jobs:
uses: gradle/wrapper-validation-action@v1
- name: Install modules
run: yarn
- name: Bootstrap
run: yarn bootstrap
- name: Build
run: cd example/android && ./gradlew assembleDebug

View File

@ -8,6 +8,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Install modules
run: yarn
run: yarn --ignore-scripts
- name: Lint
run: yarn lint

View File

@ -575,7 +575,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 57d2868c099736d80fcd648bf211b4431e51a558
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
DoubleConversion: cde416483dac037923206447da6e1454df403714
FBLazyVector: f637f31eacba90d4fdeff3fa41608b8f361c173b
FBReactNativeSpec: 0d9a4f4de7ab614c49e98c00aedfd3bfbda33d59
Flipper: 26fc4b7382499f1281eb8cb921e5c3ad6de91fe0
@ -588,7 +588,7 @@ SPEC CHECKSUMS:
Flipper-RSocket: d9d9ade67cbecf6ac10730304bf5607266dd2541
FlipperKit: cbdee19bdd4e7f05472a66ce290f1b729ba3cb86
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
glog: 40a13f7840415b9a77023fbcae0f1e6f43192af3
hermes-engine: 47986d26692ae75ee7a17ab049caee8864f855de
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c

View File

@ -128,6 +128,7 @@ const BarcodeExample = ({ onBack }: { onBack: () => void }) => {
flashMode={flashData?.mode}
zoomMode="on"
focusMode="on"
maxZoom={5}
torchMode={torchMode ? 'on' : 'off'}
onOrientationChange={(e) => {
// We recommend locking the camera UI to portrait (using a different library)

View File

@ -34,6 +34,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
const [captured, setCaptured] = useState(false);
const [cameraType, setCameraType] = useState(CameraType.Back);
const [showImageUri, setShowImageUri] = useState<string>('');
const [zoom, setZoom] = useState(0);
// iOS will error out if capturing too fast,
// so block capturing until the current capture is done
@ -54,6 +55,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
const onSwitchCameraPressed = () => {
const direction = cameraType === CameraType.Back ? CameraType.Front : CameraType.Back;
setCameraType(direction);
setZoom(0); // When changing camera type, reset to default zoom for that camera
};
const onSetFlash = () => {
@ -101,6 +103,10 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
<Image source={require('../images/cameraFlipIcon.png')} resizeMode="contain" />
</TouchableOpacity>
<TouchableOpacity style={styles.zoom} onPress={() => setZoom(0)}>
<Text style={styles.zoomFactor}>{Number(zoom).toFixed(1)}x</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.topButton} onPress={onSetTorch}>
<Image
source={torchMode ? require('../images/torchOn.png') : require('../images/torchOff.png')}
@ -120,12 +126,18 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
flashMode={flashData?.mode}
zoomMode="on"
focusMode="on"
zoom={zoom}
maxZoom={15}
torchMode={torchMode ? 'on' : 'off'}
onZoom={(e) => {
console.log('zoom', e.nativeEvent.zoom);
setZoom(e.nativeEvent.zoom);
}}
onOrientationChange={(e) => {
// We recommend locking the camera UI to portrait (using a different library)
// and rotating the UI elements counter to the orientation
// However, we include onOrientationChange so you can match your UI to what the camera does
switch(e.nativeEvent.orientation) {
switch (e.nativeEvent.orientation) {
case Orientation.LANDSCAPE_LEFT:
console.log('orientationChange', 'LANDSCAPE_LEFT');
break;
@ -141,7 +153,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
default:
console.log('orientationChange', e.nativeEvent);
break;
}
}
}}
/>
)}
@ -235,6 +247,13 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
zoom: {
alignItems: 'center',
justifyContent: 'center',
},
zoomFactor: {
color: '#ffffff',
},
thumbnailContainer: {
flex: 1,
alignItems: 'flex-end',

View File

@ -29,10 +29,13 @@ RCT_EXPORT_VIEW_PROPERTY(laserColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(frameColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onZoom, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(resetFocusTimeout, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(resetFocusWhenMotionDetected, BOOL)
RCT_EXPORT_VIEW_PROPERTY(focusMode, CKFocusMode)
RCT_EXPORT_VIEW_PROPERTY(zoomMode, CKZoomMode)
RCT_EXPORT_VIEW_PROPERTY(zoom, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(maxZoom, NSNumber)
RCT_EXTERN_METHOD(capture:(NSDictionary*)options
resolve:(RCTPromiseResolveBlock)resolve

View File

@ -11,11 +11,16 @@ protocol CameraProtocol: AnyObject, FocusInterfaceViewDelegate {
func setup(cameraType: CameraType, supportedBarcodeType: [AVMetadataObject.ObjectType])
func cameraRemovedFromSuperview()
func update(pinchScale: CGFloat)
func update(torchMode: TorchMode)
func update(flashMode: FlashMode)
func update(cameraType: CameraType)
func update(onOrientationChange: RCTDirectEventBlock?)
func update(onZoom: RCTDirectEventBlock?)
func update(zoom: Double?)
func update(maxZoom: Double?)
func zoomPinchStart()
func zoomPinchChange(pinchScale: CGFloat)
func isBarcodeScannerEnabled(_ isEnabled: Bool,
supportedBarcodeType: [AVMetadataObject.ObjectType],

View File

@ -47,10 +47,13 @@ class CameraView: UIView {
@objc var laserColor: UIColor?
// other
@objc var onOrientationChange: RCTDirectEventBlock?
@objc var onZoom: RCTDirectEventBlock?
@objc var resetFocusTimeout = 0
@objc var resetFocusWhenMotionDetected = false
@objc var focusMode: FocusMode = .on
@objc var zoomMode: ZoomMode = .on
@objc var zoom: NSNumber?
@objc var maxZoom: NSNumber?
// MARK: - Setup
@ -149,6 +152,10 @@ class CameraView: UIView {
if changedProps.contains("onOrientationChange") {
camera.update(onOrientationChange: onOrientationChange)
}
if changedProps.contains("onZoom") {
camera.update(onZoom: onZoom)
}
// Ratio overlay
if changedProps.contains("ratioOverlay") {
@ -217,6 +224,14 @@ class CameraView: UIView {
}
}
}
if changedProps.contains("zoom") {
camera.update(zoom: zoom?.doubleValue)
}
if changedProps.contains("maxZoom") {
camera.update(maxZoom: maxZoom?.doubleValue)
}
}
// MARK: Public
@ -240,7 +255,7 @@ class CameraView: UIView {
}
}, onError: onError)
}
// MARK: - Private Helper
private func handleCameraPermission() {
@ -310,10 +325,11 @@ class CameraView: UIView {
// MARK: - Gesture selectors
@objc func handlePinchToZoomRecognizer(_ pinchRecognizer: UIPinchGestureRecognizer) {
if pinchRecognizer.state == .began {
camera.zoomPinchStart()
}
if pinchRecognizer.state == .changed {
camera.update(pinchScale: pinchRecognizer.scale)
// Reset scale after every reading to get a one timeframe scale value. Otherwise pinchRecognizer.scale is relative to the start of the gesture
pinchRecognizer.scale = 1.0
camera.zoomPinchChange(pinchScale: pinchRecognizer.scale)
}
}
}

View File

@ -34,6 +34,9 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
private var onBarcodeRead: ((_ barcode: String) -> Void)?
private var scannerFrameSize: CGRect? = nil
private var onOrientationChange: RCTDirectEventBlock?
private var onZoom: RCTDirectEventBlock?
private var zoom: Double?
private var maxZoom: Double?
private var deviceOrientation = UIDeviceOrientation.unknown
private var motionManager: CMMotionManager?
@ -118,8 +121,16 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
}
}
}
private var zoomStartedAt: Double = 1.0
func zoomPinchStart() {
sessionQueue.async {
guard let videoDevice = self.videoDeviceInput?.device else { return }
self.zoomStartedAt = videoDevice.videoZoomFactor
}
}
func update(pinchScale: CGFloat) {
func zoomPinchChange(pinchScale: CGFloat) {
guard !pinchScale.isNaN else { return }
sessionQueue.async {
@ -127,17 +138,70 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
do {
try videoDevice.lockForConfiguration()
defer { videoDevice.unlockForConfiguration() }
let desiredZoomFactor = videoDevice.videoZoomFactor * pinchScale
let maxZoomFactor = min(20, videoDevice.maxAvailableVideoZoomFactor)
videoDevice.videoZoomFactor = max(1.0, min(desiredZoomFactor, maxZoomFactor))
videoDevice.unlockForConfiguration()
let desiredZoomFactor = self.zoomStartedAt * pinchScale
var maxZoomFactor = videoDevice.maxAvailableVideoZoomFactor
if let maxZoom = self.maxZoom {
maxZoomFactor = min(maxZoom, maxZoomFactor)
}
let zoomForDevice = max(1.0, min(desiredZoomFactor, maxZoomFactor))
if zoomForDevice != videoDevice.videoZoomFactor {
// Only trigger zoom changes if it's an uncontrolled component (zoom isn't manually set)
// otherwise it's likely to cause issues inf. loops
if self.zoom == nil {
videoDevice.videoZoomFactor = zoomForDevice
}
self.onZoom?(["zoom": zoomForDevice])
}
} catch {
print("Error setting zoom factor: \(error)")
}
}
}
func update(maxZoom: Double?) {
self.maxZoom = maxZoom
}
func update(zoom: Double?) {
self.zoom = zoom
sessionQueue.async {
guard let videoDevice = self.videoDeviceInput?.device else { return }
var zoomOrDefault = zoom ?? 0
// -1 will reset to zoom default (which is not 1 on modern cameras)
if zoomOrDefault == 0 {
zoomOrDefault = self.wideAngleZoomFactor(for: videoDevice)
}
do {
try videoDevice.lockForConfiguration()
defer { videoDevice.unlockForConfiguration() }
var maxZoomFactor = videoDevice.maxAvailableVideoZoomFactor
if let maxZoom = self.maxZoom {
maxZoomFactor = min(maxZoom, maxZoomFactor)
}
let zoomForDevice = max(1.0, min(zoomOrDefault, maxZoomFactor))
videoDevice.videoZoomFactor = zoomForDevice
// If they wanted to reset, tell them what the default zoom turned out to be
// regardless if it's controlled
if self.zoom == nil || zoom == 0 {
self.onZoom?(["zoom": zoomForDevice])
}
} catch {
print("Error setting zoom factor: \(error)")
}
}
}
func update(onZoom: RCTDirectEventBlock?) {
self.onZoom = onZoom
}
func focus(at touchPoint: CGPoint, focusBehavior: FocusBehavior) {
DispatchQueue.main.async {
@ -227,7 +291,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
if self.session.canAddInput(videoDeviceInput) {
self.session.addInput(videoDeviceInput)
videoDevice.videoZoomFactor = self.wideAngleZoomFactor(for: videoDevice)
let zoomForDevice = self.wideAngleZoomFactor(for: videoDevice)
if self.zoom == nil {
videoDevice.videoZoomFactor = zoomForDevice
}
self.onZoom?(["zoom": zoomForDevice])
self.videoDeviceInput = videoDeviceInput
} else {
// If it fails, put back current camera
@ -408,7 +476,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
if session.canAddInput(videoDeviceInput) {
session.addInput(videoDeviceInput)
videoDevice.videoZoomFactor = wideAngleZoomFactor(for: videoDevice)
let zoomForDevice = wideAngleZoomFactor(for: videoDevice)
if self.zoom == nil {
videoDevice.videoZoomFactor = zoomForDevice
}
self.onZoom?(["zoom": zoomForDevice])
self.videoDeviceInput = videoDeviceInput
} else {
return .sessionConfigurationFailed

View File

@ -11,6 +11,12 @@ import UIKit
*/
class SimulatorCamera: CameraProtocol {
private var onOrientationChange: RCTDirectEventBlock?
private var onZoom: RCTDirectEventBlock?
private var videoDeviceZoomFactor: Double = 1.0
private var videoDeviceMaxAvailableVideoZoomFactor: Double = 150.0
private var wideAngleZoomFactor: Double = 2.0
private var zoom: Double?
private var maxZoom: Double?
var previewView: UIView { mockPreview }
@ -54,9 +60,42 @@ class SimulatorCamera: CameraProtocol {
self.onOrientationChange = onOrientationChange
}
func update(pinchScale: CGFloat) {
func update(onZoom: RCTDirectEventBlock?) {
self.onZoom = onZoom
}
func setVideoDevice(zoomFactor: Double) {
self.videoDeviceZoomFactor = zoomFactor
self.mockPreview.zoomLabel.text = "Zoom: \(zoomFactor)"
}
private var zoomStartedAt: Double = 1.0
func zoomPinchStart() {
DispatchQueue.main.async {
self.mockPreview.zoomVelocityLabel.text = "Zoom Scale: \(pinchScale)"
self.zoomStartedAt = self.videoDeviceZoomFactor
self.mockPreview.zoomLabel.text = "Zoom start"
}
}
func zoomPinchChange(pinchScale: CGFloat) {
guard !pinchScale.isNaN else { return }
DispatchQueue.main.async {
let desiredZoomFactor = self.zoomStartedAt * pinchScale
var maxZoomFactor = self.videoDeviceMaxAvailableVideoZoomFactor
if let maxZoom = self.maxZoom {
maxZoomFactor = min(maxZoom, maxZoomFactor)
}
let zoomForDevice = max(1.0, min(desiredZoomFactor, maxZoomFactor))
if zoomForDevice != self.videoDeviceZoomFactor {
// Only trigger zoom changes if it's an uncontrolled component (zoom isn't manually set)
// otherwise it's likely to cause issues inf. loops
if self.zoom == nil {
self.setVideoDevice(zoomFactor: zoomForDevice)
}
self.onZoom?(["zoom": zoomForDevice])
}
}
}
@ -93,6 +132,36 @@ class SimulatorCamera: CameraProtocol {
self.mockPreview.randomize()
}
}
func update(maxZoom: Double?) {
self.maxZoom = maxZoom
}
func update(zoom: Double?) {
self.zoom = zoom
DispatchQueue.main.async {
var zoomOrDefault = zoom ?? 0
// -1 will reset to zoom default (which is not 1 on modern cameras)
if zoomOrDefault == 0 {
zoomOrDefault = self.wideAngleZoomFactor
}
var maxZoomFactor = self.videoDeviceMaxAvailableVideoZoomFactor
if let maxZoom = self.maxZoom {
maxZoomFactor = min(maxZoom, maxZoomFactor)
}
let zoomForDevice = max(1.0, min(zoomOrDefault, maxZoomFactor))
self.setVideoDevice(zoomFactor: zoomForDevice)
// If they wanted to reset, tell them what the default zoom turned out to be
// regardless if it's controlled
if self.zoom == nil || zoom == 0 {
self.onZoom?(["zoom": zoomForDevice])
}
}
}
func isBarcodeScannerEnabled(_ isEnabled: Bool,
supportedBarcodeType: [AVMetadataObject.ObjectType],

View File

@ -6,7 +6,7 @@
import UIKit
class SimulatorPreviewView: UIView {
let zoomVelocityLabel = UILabel()
let zoomLabel = UILabel()
let focusAtLabel = UILabel()
let torchModeLabel = UILabel()
let flashModeLabel = UILabel()
@ -30,7 +30,7 @@ class SimulatorPreviewView: UIView {
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10).isActive = true
[zoomVelocityLabel, focusAtLabel, torchModeLabel, flashModeLabel, cameraTypeLabel].forEach {
[zoomLabel, focusAtLabel, torchModeLabel, flashModeLabel, cameraTypeLabel].forEach {
$0.numberOfLines = 0
stackView.addArrangedSubview($0)
}

View File

@ -32,8 +32,8 @@ const Camera = React.forwardRef((props: CameraProps, ref) => {
return (
<NativeCamera
style={{ minWidth: 100, minHeight: 100 }}
flashMode={props.flashMode}
ref={nativeRef}
maxZoom={props.maxZoom ?? 20}
{...transformedProps}
/>
);

56
src/Camera.d.ts vendored
View File

@ -13,16 +13,72 @@ export type OnOrientationChangeData = {
};
};
export type OnZoom = {
nativeEvent: {
zoom: number;
};
}
export interface CameraProps {
ref?: LegacyRef<Component<CameraApi, {}, any>>;
style?: StyleProp<ViewStyle>;
// Behavior
flashMode?: FlashMode;
focusMode?: FocusMode;
/**
* Enable or disable the pinch gesture handler
* Example:
* ```
* <Camera zoom="on" />
* ```
*/
zoomMode?: ZoomMode;
/**
* **iOS only.**
* Control zoom. `0` is resets zoom. `1` is the widest zoom possible. Higher values zooms in.
* Example:
* ```
* const [zoom, setZoom] = useState(0);
* <Button onPress={() => setZoom(0)} title="Reset" />
* <Camera
* zoom={zoom}
* onZoom={(e) => {
* setZoom(e.nativeEvent.zoom);
* console.log('zoom', e.nativeEvent.zoom);
* }}
* />
* ```
*/
zoom?: number;
/** Maximum zoom factor. Default 20.
* Should be at least 1.0 (no zoom, widest angle).
* Modern iPhones will otherwise have a maximum zoom factor of >150
* Example:
* ```
* <Camera
* maxZoom={15}
* />
* ```
*/
maxZoom?: number;
torchMode?: TorchMode;
cameraType?: CameraType;
onOrientationChange?: (event: OnOrientationChangeData) => void;
/**
* **iOS only.**
* Triggered when:
* - User pinches to zoom, or
* - 'zoom={0}' prop is set to 0, which resets zoom for the camera
* Example:
* ```
* <Camera
* onZoom={(e) => {
* console.log('zoom', e.nativeEvent.zoom);
* }}
* />
* ```
*/
onZoom?: (event: OnZoom) => void;
// Barcode only
scanBarcode?: boolean;
showFrame?: boolean;

View File

@ -21,7 +21,9 @@ const Camera = React.forwardRef((props: CameraProps, ref: any) => {
},
}));
return <NativeCamera style={{ minWidth: 100, minHeight: 100 }} ref={nativeRef} {...props} />;
return (
<NativeCamera maxZoom={props.maxZoom ?? 20} style={{ minWidth: 100, minHeight: 100 }} ref={nativeRef} {...props} />
);
});
Camera.defaultProps = {

View File

@ -1,7 +1,7 @@
import { NativeModules } from 'react-native';
import Camera from './Camera';
import type { CameraApi, CameraType, CaptureData, FlashMode, FocusMode, TorchMode, ZoomMode } from './types';
import { CameraApi, CameraType, CaptureData, FlashMode, FocusMode, TorchMode, ZoomMode } from './types';
const { CameraKit } = NativeModules;