diff --git a/README.md b/README.md index 28fa2c27f..99b880e99 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,10 @@ https://github.com/zoontek/react-native-permissions#ios-flow ### Why no permissions API? Conceptually, permissions are simple: Granted / Denied. -However, in reality it's not that simple due to privacy enhancements on iOS and Android. +However, in reality it's not that simple due to privacy enhancements on iOS and Android. [Here's an example diagram from react-native-permissions's README](https://github.com/zoontek/react-native-permissions#ios-flow), which illustrates the complexity of the user-experience, which we don't want to duplicate in a camera library: + ``` ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ check(PERMISSIONS.IOS.CAMERA) ┃ @@ -149,7 +150,7 @@ import { Camera, CameraType } from 'react-native-camera-kit'; (this.camera = ref)} cameraType={CameraType.Back} // front/back(default) - flashMode='auto' + flashMode="auto" /> ``` @@ -171,34 +172,35 @@ Additionally, the Camera can be used for barcode scanning ### Camera Props (Optional) -| Props | Type | Description | -| ------------------------------ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ref` | Ref | Reference on the camera view | -| `style` | StyleProp\ | Style to apply on the camera view | -| `flashMode` | `'on'`/`'off'`/`'auto'` | Camera flash mode. Default: `auto` | -| `focusMode` | `'on'`/`'off'` | Camera focus mode. Default: `on` | -| `zoomMode` | `'on'`/`'off'` | Enable the pinch to zoom gesture. Default: `on` | -| `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)}`. | -| `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 | -| **Android only** | -| `onError` | Function | Android only. Callback when camera fails to initialize. Ex: `onError={(e) => console.log(e.nativeEvent.errorMessage)}`. | -| `shutterPhotoSound` | `boolean` | Android only. Enable or disable the shutter sound when capturing a photo. Default: `true` | +| Props | Type | Description | +| ------------------------------ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ref` | Ref | Reference on the camera view | +| `style` | StyleProp\ | Style to apply on the camera view | +| `flashMode` | `'on'`/`'off'`/`'auto'` | Camera flash mode. Default: `auto` | +| `focusMode` | `'on'`/`'off'` | Camera focus mode. Default: `on` | +| `zoomMode` | `'on'`/`'off'` | Enable the pinch to zoom gesture. Default: `on` | +| `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)}`. | +| `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 | +| **Android only** | +| `onError` | Function | Android only. Callback when camera fails to initialize. Ex: `onError={(e) => console.log(e.nativeEvent.errorMessage)}`. | +| `shutterPhotoSound` | `boolean` | Android only. Enable or disable the shutter sound when capturing a photo. Default: `true` | | **iOS only** | -| `ratioOverlay` | `'int:int'` | Show a guiding overlay in the camera preview for the selected ratio. Does not crop image as of v9.0. Example: `'16:9'` | -| `ratioOverlayColor` | Color | Any color with alpha. Default: `'#ffffff77'` | -| `resetFocusTimeout` | `number` | Dismiss tap to focus after this many milliseconds. Default `0` (disabled). Example: `5000` is 5 seconds. | -| `resetFocusWhenMotionDetected` | Boolean | Dismiss tap to focus when focus area content changes. Native iOS feature, see documentation: https://developer.apple.com/documentation/avfoundation/avcapturedevice/1624644-subjectareachangemonitoringenabl?language=objc). Default `true`. | -| `scanThrottleDelay` | `number` | Duration between scan detection in milliseconds. Default 2000 (2s) | +| `ratioOverlay` | `'int:int'` | Show a guiding overlay in the camera preview for the selected ratio. Does not crop image as of v9.0. Example: `'16:9'` | +| `ratioOverlayColor` | Color | Any color with alpha. Default: `'#ffffff77'` | +| `resetFocusTimeout` | `number` | Dismiss tap to focus after this many milliseconds. Default `0` (disabled). Example: `5000` is 5 seconds. | +| `resetFocusWhenMotionDetected` | Boolean | Dismiss tap to focus when focus area content changes. Native iOS feature, see documentation: https://developer.apple.com/documentation/avfoundation/avcapturedevice/1624644-subjectareachangemonitoringenabl?language=objc). Default `true`. | +| `resizeMode` | `'cover' / 'contain'` | Determines the scaling and cropping behavior of content within the view. `cover` (resizeAspectFill on iOS) scales the content to fill the view completely, potentially cropping content if its aspect ratio differs from the view. `contain` (resizeAspect on iOS) scales the content to fit within the view's bounds without cropping, ensuring all content is visible but may introduce letterboxing. Default behavior depends on the specific use case. | +| `scanThrottleDelay` | `number` | Duration between scan detection in milliseconds. Default 2000 (2s) | | **Barcode only** | -| `scanBarcode` | `boolean` | Enable barcode scanner. Default: `false` | -| `showFrame` | `boolean` | Show frame in barcode scanner. Default: `false` | -| `laserColor` | Color | Color of barcode scanner laser visualization. Default: `red` | -| `frameColor` | Color | Color of barcode scanner frame visualization. Default: `yellow` | -| `onReadCode` | Function | Callback when scanner successfully reads barcode. Returned event contains `codeStringValue`. Default: `null`. Ex: `onReadCode={(event) => console.log(event.nativeEvent.codeStringValue)}` | +| `scanBarcode` | `boolean` | Enable barcode scanner. Default: `false` | +| `showFrame` | `boolean` | Show frame in barcode scanner. Default: `false` | +| `laserColor` | Color | Color of barcode scanner laser visualization. Default: `red` | +| `frameColor` | Color | Color of barcode scanner frame visualization. Default: `yellow` | +| `onReadCode` | Function | Callback when scanner successfully reads barcode. Returned event contains `codeStringValue`. Default: `null`. Ex: `onReadCode={(event) => console.log(event.nativeEvent.codeStringValue)}` | ### Imperative API @@ -269,7 +271,6 @@ If you are using Expo Managed Workflow, you can use this library with a third-pa [See more here](https://github.com/avantstay/expo-react-native-camera-kit) - ## Contributing - Pull Requests are welcome, if you open a pull request we will do our best to get to it in a timely manner diff --git a/example/images/resize.png b/example/images/resize.png new file mode 100644 index 000000000..3bcbb1bb1 Binary files /dev/null and b/example/images/resize.png differ diff --git a/example/src/CameraExample.tsx b/example/src/CameraExample.tsx index e11442ddf..9dcf4564d 100644 --- a/example/src/CameraExample.tsx +++ b/example/src/CameraExample.tsx @@ -36,6 +36,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => { const [showImageUri, setShowImageUri] = useState(''); const [zoom, setZoom] = useState(); const [orientationAnim] = useState(new Animated.Value(3)); + const [resize, setResize] = useState<'contain' | 'cover'>('contain'); // iOS will error out if capturing too fast, // so block capturing until the current capture is done @@ -65,6 +66,14 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => { setFlashData(flashArray[newPosition]); }; + const onSetResize = () => { + if (resize === 'contain') { + setResize('cover'); + } else { + setResize('contain'); + } + }; + const onSetTorch = () => { setTorchMode(!torchMode); }; @@ -91,13 +100,37 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => { console.log('image', image); }; - function CaptureButton({ onPress, children }: { onPress: () => void, children?: React.ReactNode }) { - const w = 80, brdW = 4, spc = 6; - const cInner = 'white', cOuter = 'white'; + function CaptureButton({ onPress, children }: { onPress: () => void; children?: React.ReactNode }) { + const w = 80, + brdW = 4, + spc = 6; + const cInner = 'white', + cOuter = 'white'; return ( - - + + {children} ); @@ -111,7 +144,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => { inputRange: [1, 4], outputRange: ['180deg', '-90deg'], }); - const uiRotationStyle = rotateUi ? {transform: [{ rotate: uiRotation }]} : undefined; + const uiRotationStyle = rotateUi ? { transform: [{ rotate: uiRotation }] } : undefined; function rotateUiTo(rotationValue: number) { Animated.timing(orientationAnim, { @@ -128,12 +161,20 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => { {flashData.image && ( - + )} - + setZoom(1)}> @@ -149,6 +190,14 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => { style={[styles.topButtonImg, uiRotationStyle]} /> + + + + @@ -160,6 +209,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => { style={styles.cameraPreview} cameraType={cameraType} flashMode={flashData?.mode} + resizeMode={resize} resetFocusWhenMotionDetected zoom={zoom} maxZoom={10} @@ -265,7 +315,7 @@ const styles = StyleSheet.create({ flex: 1, }, cameraPreview: { - aspectRatio: 3 / 4, + flex: 1, width: '100%', }, bottomButtons: { diff --git a/ios/ReactNativeCameraKit/CKCameraManager.m b/ios/ReactNativeCameraKit/CKCameraManager.m index b79fd65ce..b1403ec65 100644 --- a/ios/ReactNativeCameraKit/CKCameraManager.m +++ b/ios/ReactNativeCameraKit/CKCameraManager.m @@ -20,6 +20,7 @@ @interface RCT_EXTERN_MODULE(CKCameraManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(torchMode, CKTorchMode) RCT_EXPORT_VIEW_PROPERTY(ratioOverlay, NSString) RCT_EXPORT_VIEW_PROPERTY(ratioOverlayColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(resizeMode, CKResizeMode) RCT_EXPORT_VIEW_PROPERTY(scanBarcode, BOOL) RCT_EXPORT_VIEW_PROPERTY(onReadCode, RCTDirectEventBlock) diff --git a/ios/ReactNativeCameraKit/CKTypes+RCTConvert.m b/ios/ReactNativeCameraKit/CKTypes+RCTConvert.m index 8156fa821..bcd89bc16 100644 --- a/ios/ReactNativeCameraKit/CKTypes+RCTConvert.m +++ b/ios/ReactNativeCameraKit/CKTypes+RCTConvert.m @@ -41,4 +41,9 @@ @implementation RCTConvert (CKTypes) @"off": @(CKZoomModeOff) }), CKZoomModeOn, integerValue) +RCT_ENUM_CONVERTER(CKResizeMode, (@{ + @"cover": @(CKResizeModeCover), + @"contain": @(CKResizeModeContain) +}), CKResizeModeCover, integerValue) + @end diff --git a/ios/ReactNativeCameraKit/CameraProtocol.swift b/ios/ReactNativeCameraKit/CameraProtocol.swift index 34a14e93b..bad0c4ea6 100644 --- a/ios/ReactNativeCameraKit/CameraProtocol.swift +++ b/ios/ReactNativeCameraKit/CameraProtocol.swift @@ -18,6 +18,7 @@ protocol CameraProtocol: AnyObject, FocusInterfaceViewDelegate { func update(onZoom: RCTDirectEventBlock?) func update(zoom: Double?) func update(maxZoom: Double?) + func update(resizeMode: ResizeMode) func zoomPinchStart() func zoomPinchChange(pinchScale: CGFloat) diff --git a/ios/ReactNativeCameraKit/CameraView.swift b/ios/ReactNativeCameraKit/CameraView.swift index 6e4a2abb5..eb01b0f4a 100644 --- a/ios/ReactNativeCameraKit/CameraView.swift +++ b/ios/ReactNativeCameraKit/CameraView.swift @@ -33,6 +33,7 @@ class CameraView: UIView { // props // camera settings @objc var cameraType: CameraType = .back + @objc var resizeMode: ResizeMode = .contain @objc var flashMode: FlashMode = .auto @objc var torchMode: TorchMode = .off // ratio overlay @@ -163,6 +164,10 @@ class CameraView: UIView { if changedProps.contains("onZoom") { camera.update(onZoom: onZoom) } + + if changedProps.contains("resizeMode") { + camera.update(resizeMode: resizeMode) + } // Ratio overlay if changedProps.contains("ratioOverlay") { diff --git a/ios/ReactNativeCameraKit/RealCamera.swift b/ios/ReactNativeCameraKit/RealCamera.swift index 2cfab7b08..a412083cd 100644 --- a/ios/ReactNativeCameraKit/RealCamera.swift +++ b/ios/ReactNativeCameraKit/RealCamera.swift @@ -30,6 +30,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega private let photoOutput = AVCapturePhotoOutput() private let metadataOutput = AVCaptureMetadataOutput() + private var resizeMode: ResizeMode = .contain private var flashMode: FlashMode = .auto private var torchMode: TorchMode = .off private var resetFocus: (() -> Void)? @@ -295,6 +296,17 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega } } + func update(resizeMode: ResizeMode) { + DispatchQueue.main.async { + switch resizeMode { + case .cover: + self.cameraPreview.previewLayer.videoGravity = .resizeAspectFill + case .contain: + self.cameraPreview.previewLayer.videoGravity = .resizeAspect + } + } + } + func capturePicture(onWillCapture: @escaping () -> Void, onSuccess: @escaping (_ imageData: Data, _ thumbnailData: Data?, _ dimensions: CMVideoDimensions) -> Void, onError: @escaping (_ message: String) -> Void) { diff --git a/ios/ReactNativeCameraKit/SimulatorCamera.swift b/ios/ReactNativeCameraKit/SimulatorCamera.swift index f6a4b9be0..bb57c6033 100644 --- a/ios/ReactNativeCameraKit/SimulatorCamera.swift +++ b/ios/ReactNativeCameraKit/SimulatorCamera.swift @@ -17,6 +17,7 @@ class SimulatorCamera: CameraProtocol { private var wideAngleZoomFactor: Double = 2.0 private var zoom: Double? private var maxZoom: Double? + private var resizeMode: ResizeMode = .contain var previewView: UIView { mockPreview } @@ -127,7 +128,6 @@ class SimulatorCamera: CameraProtocol { func update(cameraType: CameraType) { DispatchQueue.main.async { self.mockPreview.cameraTypeLabel.text = "Camera type: \(cameraType)" - self.mockPreview.randomize() } } @@ -161,6 +161,14 @@ class SimulatorCamera: CameraProtocol { } } + func update(resizeMode: ResizeMode) { + DispatchQueue.main.async { + self.mockPreview.resizeModeLabel.text = "Resize mode: \(resizeMode)" + self.mockPreview.randomize() + } + } + + func isBarcodeScannerEnabled(_ isEnabled: Bool, supportedBarcodeTypes: [CodeFormat], onBarcodeRead: ((_ barcode: String,_ codeFormat:CodeFormat) -> Void)?) {} diff --git a/ios/ReactNativeCameraKit/SimulatorPreviewView.swift b/ios/ReactNativeCameraKit/SimulatorPreviewView.swift index f6965f239..53f8aa457 100644 --- a/ios/ReactNativeCameraKit/SimulatorPreviewView.swift +++ b/ios/ReactNativeCameraKit/SimulatorPreviewView.swift @@ -11,6 +11,7 @@ class SimulatorPreviewView: UIView { let torchModeLabel = UILabel() let flashModeLabel = UILabel() let cameraTypeLabel = UILabel() + let resizeModeLabel = UILabel() var balloonLayer = CALayer() @@ -30,7 +31,7 @@ class SimulatorPreviewView: UIView { stackView.translatesAutoresizingMaskIntoConstraints = false stackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor).isActive = true stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10).isActive = true - [zoomLabel, focusAtLabel, torchModeLabel, flashModeLabel, cameraTypeLabel].forEach { + [zoomLabel, focusAtLabel, torchModeLabel, flashModeLabel, cameraTypeLabel, resizeModeLabel].forEach { $0.numberOfLines = 0 stackView.addArrangedSubview($0) } diff --git a/ios/ReactNativeCameraKit/Types.swift b/ios/ReactNativeCameraKit/Types.swift index 331ebbcb8..6aaf995d0 100644 --- a/ios/ReactNativeCameraKit/Types.swift +++ b/ios/ReactNativeCameraKit/Types.swift @@ -105,6 +105,19 @@ public enum ZoomMode: Int, CustomStringConvertible { } } +@objc(CKResizeMode) +public enum ResizeMode: Int, CustomStringConvertible { + case cover + case contain + + public var description: String { + switch self { + case .cover: return "cover" + case .contain: return "contain" + } + } +} + @objc(CKSetupResult) enum SetupResult: Int { case notStarted diff --git a/src/Camera.d.ts b/src/Camera.d.ts index 4b7c38c12..e9d5b7a9b 100644 --- a/src/Camera.d.ts +++ b/src/Camera.d.ts @@ -1,4 +1,4 @@ -import { CameraApi, FlashMode, FocusMode, ZoomMode, TorchMode, CameraType, CodeFormat } from './types'; +import { CameraApi, FlashMode, FocusMode, ZoomMode, TorchMode, CameraType, CodeFormat, ResizeMode } from './types'; import { Orientation } from './index'; export type OnReadCodeData = { @@ -96,6 +96,7 @@ export interface CameraProps { ratioOverlayColor?: number | string; resetFocusTimeout?: number; resetFocusWhenMotionDetected?: boolean; + resizeMode?: ResizeMode; /** **iOS Only**. Throttle how often the barcode scanner triggers a new scan */ scanThrottleDelay?: number; /** **Android only**. Play a shutter capture sound when capturing a photo */ diff --git a/src/index.ts b/src/index.ts index a6da37b6b..69c657b5b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { NativeModules } from 'react-native'; import Camera from './Camera'; -import { CameraApi, CameraType, CaptureData, FlashMode, FocusMode, TorchMode, ZoomMode } from './types'; +import { CameraApi, CameraType, CaptureData, FlashMode, FocusMode, TorchMode, ZoomMode, ResizeMode } from './types'; const { CameraKit } = NativeModules; @@ -15,4 +15,4 @@ export const Orientation = { export default CameraKit; -export { Camera, CameraType, TorchMode, FlashMode, FocusMode, ZoomMode, CameraApi, CaptureData }; +export { Camera, CameraType, TorchMode, FlashMode, FocusMode, ZoomMode, CameraApi, CaptureData, ResizeMode }; diff --git a/src/types.ts b/src/types.ts index 5bc8946a5..38cee33a1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,8 @@ export type FocusMode = 'on' | 'off'; export type ZoomMode = 'on' | 'off'; +export type ResizeMode = 'cover' | 'contain'; + export type CaptureData = { uri: string; name: string;