Skip to content
Closed
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 @@ -184,6 +184,7 @@ Additionally, the Camera can be used for barcode scanning
| `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` |
| `orientation` | `'portrait'`/`'landscape'`/`'auto'` | Lock camera orientation to portrait, landscape, or auto-detect device orientation. Default: `auto` |
| `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)}`. |
Expand Down
43 changes: 33 additions & 10 deletions android/src/main/java/com/rncamerakit/CKCamera.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
private var maxZoom: Double? = null
private var zoomStartedAt = 1.0f
private var pinchGestureStartedAt = 0.0f
private var orientationMode: String? = null

// Barcode Props
private var scanBarcode: Boolean = false
Expand Down Expand Up @@ -192,15 +193,33 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
override fun onOrientationChanged(orientation: Int) {
val imageCapture = imageCapture ?: return
var newOrientation: Int = imageCapture.targetRotation
if (orientation >= 315 || orientation < 45) {
newOrientation = Surface.ROTATION_0
} else if (orientation in 225..314) {
newOrientation = Surface.ROTATION_90
} else if (orientation in 135..224) {
newOrientation = Surface.ROTATION_180
} else if (orientation in 45..134) {
newOrientation = Surface.ROTATION_270
}

if (orientationMode != null && orientationMode != "auto") {
if (orientationMode == "portrait") {
if (orientation >= 225 || orientation < 45) {
newOrientation = Surface.ROTATION_0
} else if (orientation in 45..224) {
newOrientation = Surface.ROTATION_180
}
} else if (orientationMode == "landscape") {
if (orientation >= 225 || orientation < 45) {
newOrientation = Surface.ROTATION_90
} else if (orientation in 45..224) {
newOrientation = Surface.ROTATION_270
}
}
} else {
if (orientation >= 315 || orientation < 45) {
newOrientation = Surface.ROTATION_0
} else if (orientation in 225..314) {
newOrientation = Surface.ROTATION_90
} else if (orientation in 135..224) {
newOrientation = Surface.ROTATION_180
} else if (orientation in 45..134) {
newOrientation = Surface.ROTATION_270
}
}

if (newOrientation != imageCapture.targetRotation) {
imageCapture.targetRotation = newOrientation
onOrientationChange(newOrientation)
Expand Down Expand Up @@ -424,7 +443,11 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
}

fun setShutterPhotoSound(enabled: Boolean) {
shutterPhotoSound = enabled;
shutterPhotoSound = enabled
}

fun setOrientation(orientation: String?) {
orientationMode = orientation
}

fun capture(options: Map<String, Any>, promise: Promise) {
Expand Down
5 changes: 5 additions & 0 deletions android/src/main/java/com/rncamerakit/CKCameraManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ class CKCameraManager : SimpleViewManager<CKCamera>(), CKCameraManagerInterface<
view.setShutterPhotoSound(enabled);
}

@ReactProp(name = "orientation")
override fun setOrientation(view: CKCamera, orientation: String?) {
view.setOrientation(orientation)
}

// Methods only available on iOS
override fun setRatioOverlay(view: CKCamera?, value: String?) = Unit

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case "frameColor":
mViewManager.setFrameColor(view, ColorPropConverter.getColor(value, view.getContext()));
break;
case "orientation":
mViewManager.setOrientation(view, value == null ? null : (String) value);
break;
case "ratioOverlay":
mViewManager.setRatioOverlay(view, value == null ? null : (String) value);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public interface CKCameraManagerInterface<T extends View> {
void setMaxZoom(T view, double value);
void setTorchMode(T view, @Nullable String value);
void setCameraType(T view, @Nullable String value);
void setOrientation(T view, @Nullable String value);
void setScanBarcode(T view, boolean value);
void setShowFrame(T view, boolean value);
void setLaserColor(T view, @Nullable Integer value);
Expand Down
1 change: 1 addition & 0 deletions ios/ReactNativeCameraKit/CKCameraManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ @interface RCT_EXTERN_MODULE(CKCameraManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(barcodeFrameSize, NSDictionary)

RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(orientation, NSString)
RCT_EXPORT_VIEW_PROPERTY(onCaptureButtonPressIn, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCaptureButtonPressOut, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onZoom, RCTDirectEventBlock)
Expand Down
5 changes: 5 additions & 0 deletions ios/ReactNativeCameraKit/CKCameraViewComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
_view.maxZoom = maxZoom;
[changedProps addObject:@"maxZoom"];
}
id orientation = CKConvertFollyDynamicToId(newProps.orientation);
if (orientation != nil) {
_view.orientation = orientation;
[changedProps addObject:@"orientation"];
}
float barcodeWidth = newProps.barcodeFrameSize.width;
float barcodeHeight = newProps.barcodeFrameSize.height;
if (barcodeWidth != [_view.barcodeFrameSize[@"width"] floatValue] || barcodeHeight != [_view.barcodeFrameSize[@"height"] floatValue]) {
Expand Down
1 change: 1 addition & 0 deletions ios/ReactNativeCameraKit/CameraProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ protocol CameraProtocol: AnyObject, FocusInterfaceViewDelegate {
func update(torchMode: TorchMode)
func update(flashMode: FlashMode)
func update(cameraType: CameraType)
func update(orientation: OrientationMode)
func update(onOrientationChange: RCTDirectEventBlock?)
func update(onZoom: RCTDirectEventBlock?)
func update(zoom: Double?)
Expand Down
23 changes: 23 additions & 0 deletions ios/ReactNativeCameraKit/CameraView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public class CameraView: UIView {

// other
@objc public var onOrientationChange: RCTDirectEventBlock?
@objc public var orientation: NSString?
@objc public var onZoom: RCTDirectEventBlock?
@objc public var resetFocusTimeout = 0
@objc public var resetFocusWhenMotionDetected = false
Expand Down Expand Up @@ -88,6 +89,10 @@ public class CameraView: UIView {
#else
camera.setup(cameraType: cameraType, supportedBarcodeType: scanBarcode && onReadCode != nil ? supportedBarcodeType : [])
#endif

// Apply initial orientation lock after camera setup
let orientationMode = convertOrientationStringToMode(orientation as String?)
camera.update(orientation: orientationMode)
}
}

Expand Down Expand Up @@ -288,6 +293,12 @@ public class CameraView: UIView {
if changedProps.contains("maxZoom") {
camera.update(maxZoom: maxZoom?.doubleValue)
}

if changedProps.contains("orientation") {
let orientationMode = convertOrientationStringToMode(orientation as String?)
camera.update(orientation: orientationMode)
}

}

// MARK: Public
Expand Down Expand Up @@ -317,6 +328,18 @@ public class CameraView: UIView {

// MARK: - Private Helper

private func convertOrientationStringToMode(_ orientationString: String?) -> OrientationMode {
guard let orientationString = orientationString else { return .auto }
switch orientationString {
case "portrait":
return .portrait
case "landscape":
return .landscape
default:
return .auto
}
}

private func update(zoomMode: ZoomMode) {
if zoomMode == .on {
if zoomGestureRecognizer == nil {
Expand Down
87 changes: 83 additions & 4 deletions ios/ReactNativeCameraKit/RealCamera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
private var lastOnZoom: Double?
private var zoom: Double?
private var maxZoom: Double?

private var orientation: OrientationMode = .auto

private var deviceOrientation = UIDeviceOrientation.unknown
private var motionManager: CMMotionManager?

Expand Down Expand Up @@ -266,6 +267,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
}
}

func update(orientation: OrientationMode) {
self.orientation = orientation

DispatchQueue.main.async {
self.setVideoOrientationToInterfaceOrientation()
}
}

func update(flashMode: FlashMode) {
self.flashMode = flashMode
}
Expand Down Expand Up @@ -337,11 +346,44 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
the main thread and session configuration is done on the session queue.
*/
DispatchQueue.main.async {
let videoPreviewLayerOrientation =
self.videoOrientation(from: self.deviceOrientation) ?? self.cameraPreview.previewLayer.connection?.videoOrientation
let videoPreviewLayerOrientation: AVCaptureVideoOrientation

// Check if orientation is locked to a specific mode
if self.orientation != .auto {
// Get current device/interface orientation for intelligent selection
var currentInterfaceOrientation: UIInterfaceOrientation
if #available(iOS 13.0, *) {
currentInterfaceOrientation = self.previewView.window?.windowScene?.interfaceOrientation ?? .portrait
} else {
currentInterfaceOrientation = UIApplication.shared.statusBarOrientation
}

switch self.orientation {
case .portrait:
if currentInterfaceOrientation == .portraitUpsideDown {
videoPreviewLayerOrientation = .portraitUpsideDown
} else {
videoPreviewLayerOrientation = .portrait
}
case .landscape:
if currentInterfaceOrientation == .landscapeRight {
videoPreviewLayerOrientation = .landscapeRight
} else {
videoPreviewLayerOrientation = .landscapeLeft
}
default:
// Fallback to auto behavior
videoPreviewLayerOrientation =
self.videoOrientation(from: self.deviceOrientation) ?? self.cameraPreview.previewLayer.connection?.videoOrientation ?? .portrait
}
} else {
// Use device orientation or fallback to preview layer orientation
videoPreviewLayerOrientation =
self.videoOrientation(from: self.deviceOrientation) ?? self.cameraPreview.previewLayer.connection?.videoOrientation ?? .portrait
}

self.sessionQueue.async {
if let photoOutputConnection = self.photoOutput.connection(with: .video), let videoPreviewLayerOrientation {
if let photoOutputConnection = self.photoOutput.connection(with: .video) {
photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
}

Expand Down Expand Up @@ -694,6 +736,43 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega

private func setVideoOrientationToInterfaceOrientation() {
#if !targetEnvironment(macCatalyst)
if self.orientation != .auto {
let targetOrientation: AVCaptureVideoOrientation

// Get current device/interface orientation for intelligent selection
var currentInterfaceOrientation: UIInterfaceOrientation
if #available(iOS 13.0, *) {
currentInterfaceOrientation = self.previewView.window?.windowScene?.interfaceOrientation ?? .portrait
} else {
currentInterfaceOrientation = UIApplication.shared.statusBarOrientation
}

switch self.orientation {
case .portrait:
// Lock to portrait, choose best portrait orientation based on current state
if currentInterfaceOrientation == .portraitUpsideDown {
targetOrientation = .portraitUpsideDown
} else {
targetOrientation = .portrait
}
case .landscape:
// Lock to landscape, choose best landscape orientation based on current state
if currentInterfaceOrientation == .landscapeRight {
targetOrientation = .landscapeRight
} else {
targetOrientation = .landscapeLeft
}
default:
// Fallback to auto behavior
targetOrientation = self.videoOrientation(from: currentInterfaceOrientation)
}


self.cameraPreview.previewLayer.connection?.videoOrientation = targetOrientation
return
}

// Use device/interface orientation if not locked (auto mode)
var interfaceOrientation: UIInterfaceOrientation
if #available(iOS 13.0, *) {
interfaceOrientation = self.previewView.window?.windowScene?.interfaceOrientation ?? .portrait
Expand Down
14 changes: 14 additions & 0 deletions ios/ReactNativeCameraKit/SimulatorCamera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class SimulatorCamera: CameraProtocol {
func setup(cameraType: CameraType, supportedBarcodeType: [CodeFormat]) {
DispatchQueue.main.async {
self.mockPreview.cameraTypeLabel.text = "Camera type: \(cameraType)"
self.mockPreview.orientationLabel.text = "Orientation: auto"
}

// Listen to orientation changes
Expand Down Expand Up @@ -136,6 +137,19 @@ class SimulatorCamera: CameraProtocol {
}
}

func update(orientation: OrientationMode) {
DispatchQueue.main.async {
switch orientation {
case .auto:
self.mockPreview.orientationLabel.text = "Orientation: auto"
case .portrait:
self.mockPreview.orientationLabel.text = "Orientation: locked to portrait"
case .landscape:
self.mockPreview.orientationLabel.text = "Orientation: locked to landscape"
}
}
}

func update(maxZoom: Double?) {
self.maxZoom = maxZoom
}
Expand Down
3 changes: 2 additions & 1 deletion ios/ReactNativeCameraKit/SimulatorPreviewView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class SimulatorPreviewView: UIView {
let torchModeLabel = UILabel()
let flashModeLabel = UILabel()
let cameraTypeLabel = UILabel()
let orientationLabel = UILabel()
let resizeModeLabel = UILabel()

var balloonLayer = CALayer()
Expand All @@ -31,7 +32,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, resizeModeLabel].forEach {
[zoomLabel, focusAtLabel, torchModeLabel, flashModeLabel, cameraTypeLabel, resizeModeLabel, orientationLabel].forEach {
$0.numberOfLines = 0
stackView.addArrangedSubview($0)
}
Expand Down
15 changes: 15 additions & 0 deletions ios/ReactNativeCameraKit/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,21 @@ enum SetupResult: Int {
case sessionConfigurationFailed
}

@objc(CKOrientationMode)
public enum OrientationMode: Int, CustomStringConvertible {
case auto
case portrait
case landscape

public var description: String {
switch self {
case .auto: return "auto"
case .portrait: return "portrait"
case .landscape: return "landscape"
}
}
}

enum Orientation: Int {
case portrait = 0 // ⬆️
case landscapeLeft = 1 // ⬅️
Expand Down
2 changes: 2 additions & 0 deletions src/CameraProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type TorchMode,
type ResizeMode,
type CodeFormat,
type OrientationMode,
} from './types';
import { Orientation } from './index';

Expand Down Expand Up @@ -77,6 +78,7 @@ export interface CameraProps extends ViewProps {
maxZoom?: number;
torchMode?: TorchMode;
cameraType?: CameraType;
orientation?: OrientationMode;
onOrientationChange?: (event: OnOrientationChangeData) => void;
/**
* Callback triggered when user pinches to zoom and on startup.
Expand Down
1 change: 1 addition & 0 deletions src/specs/CameraNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface NativeProps extends ViewProps {
maxZoom?: Double;
torchMode?: string;
cameraType?: string;
orientation?: string;
onOrientationChange?: DirectEventHandler<OnOrientationChangeData>;
onZoom?: DirectEventHandler<OnZoom>;
onError?: DirectEventHandler<{errorMessage: string }>;
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export type ZoomMode = 'on' | 'off';

export type ResizeMode = 'cover' | 'contain';

export type OrientationMode = 'auto' | 'portrait' | 'landscape';

export type CaptureData = {
uri: string;
name: string;
Expand Down