Skip to content

Commit 568da25

Browse files
committed
feat: orientation lock
1 parent 3abe9c9 commit 568da25

File tree

16 files changed

+214
-17
lines changed

16 files changed

+214
-17
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: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
109109
private var laserColor = Color.RED
110110
private var barcodeFrameSize: Size? = null
111111

112+
// Orientation Props
113+
private var orientationMode: String? = null
114+
112115
private fun getActivity() : Activity {
113116
return currentContext.currentActivity!!
114117
}
@@ -192,17 +195,28 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
192195
override fun onOrientationChanged(orientation: Int) {
193196
val imageCapture = imageCapture ?: return
194197
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
198+
199+
val deviceRotation = when {
200+
orientation >= 315 || orientation < 45 -> Surface.ROTATION_0
201+
orientation in 225..314 -> Surface.ROTATION_90
202+
orientation in 135..224 -> Surface.ROTATION_180
203+
orientation in 45..134 -> Surface.ROTATION_270
204+
else -> Surface.ROTATION_0
205+
}
206+
207+
newOrientation = when (orientationMode) {
208+
"portrait" -> {
209+
Surface.ROTATION_0
210+
}
211+
"landscape" -> {
212+
Surface.ROTATION_90
213+
}
214+
else -> deviceRotation
203215
}
216+
204217
if (newOrientation != imageCapture.targetRotation) {
205218
imageCapture.targetRotation = newOrientation
219+
preview?.targetRotation = newOrientation
206220
onOrientationChange(newOrientation)
207221
}
208222
}
@@ -292,7 +306,13 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
292306
val previewAspectRatio = aspectRatio(previewWidth, previewHeight)
293307
Log.d(TAG, "Preview aspect ratio: $previewAspectRatio")
294308

295-
val rotation = viewFinder.display.rotation
309+
val deviceRotation = viewFinder.display.rotation
310+
311+
val rotation = when (orientationMode) {
312+
"portrait" -> Surface.ROTATION_0
313+
"landscape" -> Surface.ROTATION_90
314+
else -> deviceRotation
315+
}
296316

297317
// CameraProvider
298318
val cameraProvider = cameraProvider
@@ -305,7 +325,7 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
305325
preview = Preview.Builder()
306326
// We request aspect ratio but no resolution
307327
.setTargetAspectRatio(previewAspectRatio)
308-
// Set initial target rotation
328+
// Set initial target rotation (respecting orientation lock)
309329
.setTargetRotation(rotation)
310330
.build()
311331

@@ -424,7 +444,17 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
424444
}
425445

426446
fun setShutterPhotoSound(enabled: Boolean) {
427-
shutterPhotoSound = enabled;
447+
shutterPhotoSound = enabled
448+
}
449+
450+
fun setOrientation(orientation: String?) {
451+
orientationMode = orientation
452+
453+
if (orientation != null && orientation != "auto") {
454+
orientationListener?.disable()
455+
} else {
456+
orientationListener?.enable()
457+
}
428458
}
429459

430460
fun capture(options: Map<String, Any>, promise: Promise) {
@@ -452,6 +482,16 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
452482
}
453483
}
454484

485+
val currentImageCapture = imageCapture
486+
if (currentImageCapture != null && orientationMode != null && orientationMode != "auto") {
487+
val targetOrientation = when (orientationMode) {
488+
"portrait" -> Surface.ROTATION_0
489+
"landscape" -> Surface.ROTATION_90
490+
else -> currentImageCapture.targetRotation
491+
}
492+
currentImageCapture.targetRotation = targetOrientation
493+
}
494+
455495
// Setup image capture listener which is triggered after photo has been taken
456496
imageCapture?.takePicture(
457497
outputOptions, ContextCompat.getMainExecutor(getActivity()), object : ImageCapture.OnImageSavedCallback {

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: 26 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,21 @@ 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+
334+
switch orientationString {
335+
case "auto":
336+
return .auto
337+
case "portrait":
338+
return .portrait
339+
case "landscape":
340+
return .landscape
341+
default:
342+
return .auto // Default to auto
343+
}
344+
}
345+
320346
private func update(zoomMode: ZoomMode) {
321347
if zoomMode == .on {
322348
if zoomGestureRecognizer == nil {

ios/ReactNativeCameraKit/RealCamera.swift

Lines changed: 84 additions & 5 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

@@ -127,7 +128,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
127128
self.session.startRunning()
128129

129130
// We need to reapply the configuration after starting the camera
130-
self.update(torchMode: self.torchMode)
131+
// self.update(torchMode: self.torchMode)
131132
}
132133

133134
DispatchQueue.main.async {
@@ -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+
case .auto:
375+
// Fallback to device orientation or preview layer
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+
// Check if orientation is locked to a specific mode
740+
if self.orientation != .auto {
741+
let targetOrientation: AVCaptureVideoOrientation
742+
743+
// Get current device/interface orientation for intelligent selection
744+
var currentInterfaceOrientation: UIInterfaceOrientation
745+
if #available(iOS 13.0, *) {
746+
currentInterfaceOrientation = self.previewView.window?.windowScene?.interfaceOrientation ?? .portrait
747+
} else {
748+
currentInterfaceOrientation = UIApplication.shared.statusBarOrientation
749+
}
750+
751+
switch self.orientation {
752+
case .portrait:
753+
// Lock to portrait, choose best portrait orientation based on current state
754+
if currentInterfaceOrientation == .portraitUpsideDown {
755+
targetOrientation = .portraitUpsideDown
756+
} else {
757+
targetOrientation = .portrait // Default to normal portrait
758+
}
759+
case .landscape:
760+
// Lock to landscape, choose best landscape orientation based on current state
761+
if currentInterfaceOrientation == .landscapeRight {
762+
targetOrientation = .landscapeRight
763+
} else {
764+
targetOrientation = .landscapeLeft // Default to landscape left
765+
}
766+
case .auto:
767+
// Fallback to auto behavior
768+
targetOrientation = self.videoOrientation(from: currentInterfaceOrientation)
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)