Skip to content

Commit 47284db

Browse files
committed
feat: orientation lock
1 parent 3abe9c9 commit 47284db

File tree

16 files changed

+192
-15
lines changed

16 files changed

+192
-15
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ Additionally, the Camera can be used for barcode scanning
184184
| `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)}`. |
185185
| `torchMode` | `'on'`/`'off'` | Toggle flash light when camera is active. Default: `off` |
186186
| `cameraType` | CameraType.Back/CameraType.Front | Choose what camera to use. Default: `CameraType.Back` |
187+
| `orientation` | `'portrait'`/`'landscape'`/`'auto'` | Lock camera orientation to portrait, landscape, or auto-detect device orientation. Default: `auto` |
187188
| `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 |
188189
| **Android only** |
189190
| `onError` | Function | Android only. Callback when camera fails to initialize. Ex: `onError={(e) => console.log(e.nativeEvent.errorMessage)}`. |

android/src/main/java/com/rncamerakit/CKCamera.kt

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
102102
private var maxZoom: Double? = null
103103
private var zoomStartedAt = 1.0f
104104
private var pinchGestureStartedAt = 0.0f
105+
private var orientationMode: String? = null
105106

106107
// Barcode Props
107108
private var scanBarcode: Boolean = false
@@ -192,15 +193,33 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
192193
override fun onOrientationChanged(orientation: Int) {
193194
val imageCapture = imageCapture ?: return
194195
var newOrientation: Int = imageCapture.targetRotation
195-
if (orientation >= 315 || orientation < 45) {
196-
newOrientation = Surface.ROTATION_0
197-
} else if (orientation in 225..314) {
198-
newOrientation = Surface.ROTATION_90
199-
} else if (orientation in 135..224) {
200-
newOrientation = Surface.ROTATION_180
201-
} else if (orientation in 45..134) {
202-
newOrientation = Surface.ROTATION_270
203-
}
196+
197+
if (orientationMode != null && orientationMode != "auto") {
198+
if (orientationMode == "portrait") {
199+
if (orientation >= 225 || orientation < 45) {
200+
newOrientation = Surface.ROTATION_0
201+
} else if (orientation in 45..224) {
202+
newOrientation = Surface.ROTATION_180
203+
}
204+
} else if (orientationMode == "landscape") {
205+
if (orientation >= 225 || orientation < 45) {
206+
newOrientation = Surface.ROTATION_90
207+
} else if (orientation in 45..224) {
208+
newOrientation = Surface.ROTATION_270
209+
}
210+
}
211+
} else {
212+
if (orientation >= 315 || orientation < 45) {
213+
newOrientation = Surface.ROTATION_0
214+
} else if (orientation in 225..314) {
215+
newOrientation = Surface.ROTATION_90
216+
} else if (orientation in 135..224) {
217+
newOrientation = Surface.ROTATION_180
218+
} else if (orientation in 45..134) {
219+
newOrientation = Surface.ROTATION_270
220+
}
221+
}
222+
204223
if (newOrientation != imageCapture.targetRotation) {
205224
imageCapture.targetRotation = newOrientation
206225
onOrientationChange(newOrientation)
@@ -424,7 +443,11 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
424443
}
425444

426445
fun setShutterPhotoSound(enabled: Boolean) {
427-
shutterPhotoSound = enabled;
446+
shutterPhotoSound = enabled
447+
}
448+
449+
fun setOrientation(orientation: String?) {
450+
orientationMode = orientation
428451
}
429452

430453
fun capture(options: Map<String, Any>, promise: Promise) {

android/src/main/java/com/rncamerakit/CKCameraManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ class CKCameraManager : SimpleViewManager<CKCamera>(), CKCameraManagerInterface<
143143
view.setShutterPhotoSound(enabled);
144144
}
145145

146+
@ReactProp(name = "orientation")
147+
override fun setOrientation(view: CKCamera, orientation: String?) {
148+
view.setOrientation(orientation)
149+
}
150+
146151
// Methods only available on iOS
147152
override fun setRatioOverlay(view: CKCamera?, value: String?) = Unit
148153

android/src/paper/java/com/facebook/react/viewmanagers/CKCameraManagerDelegate.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
6060
case "frameColor":
6161
mViewManager.setFrameColor(view, ColorPropConverter.getColor(value, view.getContext()));
6262
break;
63+
case "orientation":
64+
mViewManager.setOrientation(view, value == null ? null : (String) value);
65+
break;
6366
case "ratioOverlay":
6467
mViewManager.setRatioOverlay(view, value == null ? null : (String) value);
6568
break;

android/src/paper/java/com/facebook/react/viewmanagers/CKCameraManagerInterface.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public interface CKCameraManagerInterface<T extends View> {
2222
void setMaxZoom(T view, double value);
2323
void setTorchMode(T view, @Nullable String value);
2424
void setCameraType(T view, @Nullable String value);
25+
void setOrientation(T view, @Nullable String value);
2526
void setScanBarcode(T view, boolean value);
2627
void setShowFrame(T view, boolean value);
2728
void setLaserColor(T view, @Nullable Integer value);

ios/ReactNativeCameraKit/CKCameraManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ @interface RCT_EXTERN_MODULE(CKCameraManager, RCTViewManager)
3232
RCT_EXPORT_VIEW_PROPERTY(barcodeFrameSize, NSDictionary)
3333

3434
RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock)
35+
RCT_EXPORT_VIEW_PROPERTY(orientation, NSString)
3536
RCT_EXPORT_VIEW_PROPERTY(onCaptureButtonPressIn, RCTDirectEventBlock)
3637
RCT_EXPORT_VIEW_PROPERTY(onCaptureButtonPressOut, RCTDirectEventBlock)
3738
RCT_EXPORT_VIEW_PROPERTY(onZoom, RCTDirectEventBlock)

ios/ReactNativeCameraKit/CKCameraViewComponentView.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
237237
_view.maxZoom = maxZoom;
238238
[changedProps addObject:@"maxZoom"];
239239
}
240+
id orientation = CKConvertFollyDynamicToId(newProps.orientation);
241+
if (orientation != nil) {
242+
_view.orientation = orientation;
243+
[changedProps addObject:@"orientation"];
244+
}
240245
float barcodeWidth = newProps.barcodeFrameSize.width;
241246
float barcodeHeight = newProps.barcodeFrameSize.height;
242247
if (barcodeWidth != [_view.barcodeFrameSize[@"width"] floatValue] || barcodeHeight != [_view.barcodeFrameSize[@"height"] floatValue]) {

ios/ReactNativeCameraKit/CameraProtocol.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ protocol CameraProtocol: AnyObject, FocusInterfaceViewDelegate {
1414
func update(torchMode: TorchMode)
1515
func update(flashMode: FlashMode)
1616
func update(cameraType: CameraType)
17+
func update(orientation: OrientationMode)
1718
func update(onOrientationChange: RCTDirectEventBlock?)
1819
func update(onZoom: RCTDirectEventBlock?)
1920
func update(zoom: Double?)

ios/ReactNativeCameraKit/CameraView.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class CameraView: UIView {
5252

5353
// other
5454
@objc public var onOrientationChange: RCTDirectEventBlock?
55+
@objc public var orientation: NSString?
5556
@objc public var onZoom: RCTDirectEventBlock?
5657
@objc public var resetFocusTimeout = 0
5758
@objc public var resetFocusWhenMotionDetected = false
@@ -88,6 +89,10 @@ public class CameraView: UIView {
8889
#else
8990
camera.setup(cameraType: cameraType, supportedBarcodeType: scanBarcode && onReadCode != nil ? supportedBarcodeType : [])
9091
#endif
92+
93+
// Apply initial orientation lock after camera setup
94+
let orientationMode = convertOrientationStringToMode(orientation as String?)
95+
camera.update(orientation: orientationMode)
9196
}
9297
}
9398

@@ -288,6 +293,12 @@ public class CameraView: UIView {
288293
if changedProps.contains("maxZoom") {
289294
camera.update(maxZoom: maxZoom?.doubleValue)
290295
}
296+
297+
if changedProps.contains("orientation") {
298+
let orientationMode = convertOrientationStringToMode(orientation as String?)
299+
camera.update(orientation: orientationMode)
300+
}
301+
291302
}
292303

293304
// MARK: Public
@@ -317,6 +328,18 @@ public class CameraView: UIView {
317328

318329
// MARK: - Private Helper
319330

331+
private func convertOrientationStringToMode(_ orientationString: String?) -> OrientationMode {
332+
guard let orientationString = orientationString else { return .auto }
333+
switch orientationString {
334+
case "portrait":
335+
return .portrait
336+
case "landscape":
337+
return .landscape
338+
default:
339+
return .auto
340+
}
341+
}
342+
320343
private func update(zoomMode: ZoomMode) {
321344
if zoomMode == .on {
322345
if zoomGestureRecognizer == nil {

ios/ReactNativeCameraKit/RealCamera.swift

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
4444
private var lastOnZoom: Double?
4545
private var zoom: Double?
4646
private var maxZoom: Double?
47-
47+
private var orientation: OrientationMode = .auto
48+
4849
private var deviceOrientation = UIDeviceOrientation.unknown
4950
private var motionManager: CMMotionManager?
5051

@@ -266,6 +267,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
266267
}
267268
}
268269

270+
func update(orientation: OrientationMode) {
271+
self.orientation = orientation
272+
273+
DispatchQueue.main.async {
274+
self.setVideoOrientationToInterfaceOrientation()
275+
}
276+
}
277+
269278
func update(flashMode: FlashMode) {
270279
self.flashMode = flashMode
271280
}
@@ -337,11 +346,44 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
337346
the main thread and session configuration is done on the session queue.
338347
*/
339348
DispatchQueue.main.async {
340-
let videoPreviewLayerOrientation =
341-
self.videoOrientation(from: self.deviceOrientation) ?? self.cameraPreview.previewLayer.connection?.videoOrientation
349+
let videoPreviewLayerOrientation: AVCaptureVideoOrientation
350+
351+
// Check if orientation is locked to a specific mode
352+
if self.orientation != .auto {
353+
// Get current device/interface orientation for intelligent selection
354+
var currentInterfaceOrientation: UIInterfaceOrientation
355+
if #available(iOS 13.0, *) {
356+
currentInterfaceOrientation = self.previewView.window?.windowScene?.interfaceOrientation ?? .portrait
357+
} else {
358+
currentInterfaceOrientation = UIApplication.shared.statusBarOrientation
359+
}
360+
361+
switch self.orientation {
362+
case .portrait:
363+
if currentInterfaceOrientation == .portraitUpsideDown {
364+
videoPreviewLayerOrientation = .portraitUpsideDown
365+
} else {
366+
videoPreviewLayerOrientation = .portrait
367+
}
368+
case .landscape:
369+
if currentInterfaceOrientation == .landscapeRight {
370+
videoPreviewLayerOrientation = .landscapeRight
371+
} else {
372+
videoPreviewLayerOrientation = .landscapeLeft
373+
}
374+
default:
375+
// Fallback to auto behavior
376+
videoPreviewLayerOrientation =
377+
self.videoOrientation(from: self.deviceOrientation) ?? self.cameraPreview.previewLayer.connection?.videoOrientation ?? .portrait
378+
}
379+
} else {
380+
// Use device orientation or fallback to preview layer orientation
381+
videoPreviewLayerOrientation =
382+
self.videoOrientation(from: self.deviceOrientation) ?? self.cameraPreview.previewLayer.connection?.videoOrientation ?? .portrait
383+
}
342384

343385
self.sessionQueue.async {
344-
if let photoOutputConnection = self.photoOutput.connection(with: .video), let videoPreviewLayerOrientation {
386+
if let photoOutputConnection = self.photoOutput.connection(with: .video) {
345387
photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
346388
}
347389

@@ -694,6 +736,43 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
694736

695737
private func setVideoOrientationToInterfaceOrientation() {
696738
#if !targetEnvironment(macCatalyst)
739+
if self.orientation != .auto {
740+
let targetOrientation: AVCaptureVideoOrientation
741+
742+
// Get current device/interface orientation for intelligent selection
743+
var currentInterfaceOrientation: UIInterfaceOrientation
744+
if #available(iOS 13.0, *) {
745+
currentInterfaceOrientation = self.previewView.window?.windowScene?.interfaceOrientation ?? .portrait
746+
} else {
747+
currentInterfaceOrientation = UIApplication.shared.statusBarOrientation
748+
}
749+
750+
switch self.orientation {
751+
case .portrait:
752+
// Lock to portrait, choose best portrait orientation based on current state
753+
if currentInterfaceOrientation == .portraitUpsideDown {
754+
targetOrientation = .portraitUpsideDown
755+
} else {
756+
targetOrientation = .portrait
757+
}
758+
case .landscape:
759+
// Lock to landscape, choose best landscape orientation based on current state
760+
if currentInterfaceOrientation == .landscapeRight {
761+
targetOrientation = .landscapeRight
762+
} else {
763+
targetOrientation = .landscapeLeft
764+
}
765+
default:
766+
// Fallback to auto behavior
767+
targetOrientation = self.videoOrientation(from: currentInterfaceOrientation)
768+
}
769+
770+
771+
self.cameraPreview.previewLayer.connection?.videoOrientation = targetOrientation
772+
return
773+
}
774+
775+
// Use device/interface orientation if not locked (auto mode)
697776
var interfaceOrientation: UIInterfaceOrientation
698777
if #available(iOS 13.0, *) {
699778
interfaceOrientation = self.previewView.window?.windowScene?.interfaceOrientation ?? .portrait

0 commit comments

Comments
 (0)