Skip to content
Merged
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
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,44 @@ Then, you need to add the plugin to your app.json file:

This way, Expo will handle the native setup for you during `prebuild`.

> Note: only SDK 50 and above are supported, the plugin is configured to handle only the kotlin template.

## Setup

### Android

This library uses a custom broadcast receiver to handle the manual orientation changes: when the user disables the
autorotation feature and the system prompts the user to rotate the device, the library will listen to the broadcast
sent by the MainActivity and update the interface orientation accordingly.

To allow the library to listen to the broadcast, you need to override the `onConfigurationChanged` method in your
MainActivity file, as shown below:

```kotlin
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)

val orientationDirectorCustomAction =
"${packageName}.${ConfigurationChangedBroadcastReceiver.CUSTOM_INTENT_ACTION}"

val intent =
Intent(orientationDirectorCustomAction).apply {
putExtra("newConfig", newConfig)
setPackage(packageName)
}

this.sendBroadcast(intent)
}
```

Nothing else is required for Android.

### iOS

To properly handle interface orientation changes in iOS, you need to update your AppDelegate file. Since React Native
0.77, the AppDelegate has been migrated to Swift, so see the instructions below for both Swift and Objective-C.

### Objective-C
#### Objective-C

In your AppDelegate.h file, import "OrientationDirector.h" and implement supportedInterfaceOrientationsForWindow method as follows:

Expand All @@ -82,7 +114,7 @@ In your AppDelegate.h file, import "OrientationDirector.h" and implement support
}
```

### Swift
#### Swift

You need to create a [bridging header](https://developer.apple.com/documentation/swift/importing-objective-c-into-swift#Import-Code-Within-an-App-Target)
to import the library, as shown below:
Expand All @@ -101,8 +133,6 @@ override func application(_ application: UIApplication, supportedInterfaceOrient

If you need help, you can check the example project.

There is no need to do anything in Android, it works out of the box.

## Usage

This library exports a class called: [RNOrientationDirector](https://github.com/gladiuscode/react-native-orientation-director/blob/main/src/RNOrientationDirector.ts) that exposes the following methods:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.orientationdirector.implementation

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import com.facebook.react.bridge.ReactApplicationContext

/**
* This custom broadcast receiver is needed to properly update the interface orientation when
* the user has disabled the automatic rotation.
*
* It listens for an explicit intent that the MainActivity can send in the onConfigurationChanged
* method and calls a custom callback that is set in the main implementation init
*/
class ConfigurationChangedBroadcastReceiver internal constructor(private val context: ReactApplicationContext) :
BroadcastReceiver() {

private var onReceiveCallback: ((intent: Intent?) -> Unit)? = null

override fun onReceive(context: Context?, intent: Intent?) {
this.onReceiveCallback?.invoke(intent)
}

fun setOnReceiveCallback(callback: (intent: Intent?) -> Unit) {
onReceiveCallback = callback
}

/**
* This method registers the receiver by checking the api we are currently running with.
* With the latest changes in Android 14, we need to explicitly set the `Context.RECEIVER_NOT_EXPORTED`
* flag.
*/
fun register() {
val filter = IntentFilter("${context.packageName}.$CUSTOM_INTENT_ACTION")

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
context.registerReceiver(this, filter)
}
}

fun unregister() {
context.unregisterReceiver(this)
}

companion object {
const val CUSTOM_INTENT_ACTION = "CONFIGURATION_CHANGED"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class OrientationDirectorModuleImpl internal constructor(private val context: Re
)
)
private var mLifecycleListener = LifecycleListener()
private var mBroadcastReceiver = ConfigurationChangedBroadcastReceiver(context)

private var initialSupportedInterfaceOrientations = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private var lastInterfaceOrientation = Orientation.UNKNOWN
Expand All @@ -31,24 +32,31 @@ class OrientationDirectorModuleImpl internal constructor(private val context: Re

mAutoRotationObserver.enable()

mBroadcastReceiver.setOnReceiveCallback {
adaptInterfaceTo(lastDeviceOrientation, false)
}

context.addLifecycleEventListener(mLifecycleListener)
mLifecycleListener.setOnHostResumeCallback {
if (!didComputeInitialDeviceOrientation || areOrientationSensorsEnabled) {
mOrientationSensorsEventListener.enable()
}
mAutoRotationObserver.enable()
mBroadcastReceiver.register()
}
mLifecycleListener.setOnHostPauseCallback {
if (initialized && areOrientationSensorsEnabled) {
mOrientationSensorsEventListener.disable()
mAutoRotationObserver.disable()
}
mBroadcastReceiver.unregister()
}
mLifecycleListener.setOnHostDestroyCallback {
if (areOrientationSensorsEnabled) {
mOrientationSensorsEventListener.disable()
mAutoRotationObserver.disable()
}
mBroadcastReceiver.unregister()
}

initialSupportedInterfaceOrientations =
Expand Down Expand Up @@ -89,15 +97,16 @@ class OrientationDirectorModuleImpl internal constructor(private val context: Re
return
}

val lastInterfaceOrientationIsAlreadyInLandscape = lastInterfaceOrientation == Orientation.LANDSCAPE_RIGHT
|| lastInterfaceOrientation == Orientation.LANDSCAPE_LEFT
if (lastInterfaceOrientationIsAlreadyInLandscape) {
updateLastInterfaceOrientationTo(lastInterfaceOrientation)
return;
}
val lastInterfaceOrientationIsAlreadyInLandscape =
lastInterfaceOrientation == Orientation.LANDSCAPE_RIGHT
|| lastInterfaceOrientation == Orientation.LANDSCAPE_LEFT
if (lastInterfaceOrientationIsAlreadyInLandscape) {
updateLastInterfaceOrientationTo(lastInterfaceOrientation)
return;
}

val systemDefaultLandscapeOrientation = Orientation.LANDSCAPE_RIGHT
updateLastInterfaceOrientationTo(systemDefaultLandscapeOrientation)
updateLastInterfaceOrientationTo(systemDefaultLandscapeOrientation)
}

fun unlock() {
Expand Down Expand Up @@ -160,12 +169,13 @@ class OrientationDirectorModuleImpl internal constructor(private val context: Re
}
}

private fun adaptInterfaceTo(deviceOrientation: Orientation) {
if (!mAutoRotationObserver.getLastAutoRotationStatus()) {
private fun adaptInterfaceTo(deviceOrientation: Orientation, checkLastAutoRotationStatus: Boolean = true) {
if (checkLastAutoRotationStatus && !mAutoRotationObserver.getLastAutoRotationStatus()) {
return
}

val supportsLandscape = mUtils.getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
val supportsLandscape =
mUtils.getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
if (isLocked && !supportsLandscape) {
return
}
Expand All @@ -191,8 +201,9 @@ class OrientationDirectorModuleImpl internal constructor(private val context: Re
* Instead, we check that its value is either LANDSCAPE_RIGHT or LANDSCAPE_LEFT, otherwise we
* exit
*/
val newInterfaceOrientationIsNotLandscape = newInterfaceOrientation != Orientation.LANDSCAPE_RIGHT
&& newInterfaceOrientation != Orientation.LANDSCAPE_LEFT;
val newInterfaceOrientationIsNotLandscape =
newInterfaceOrientation != Orientation.LANDSCAPE_RIGHT
&& newInterfaceOrientation != Orientation.LANDSCAPE_LEFT;
if (supportsLandscape && newInterfaceOrientationIsNotLandscape) {
return
}
Expand Down
2 changes: 1 addition & 1 deletion example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
android:supportsRtl="true">>
android:supportsRtl="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.orientationdirectorexample

import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import com.orientationdirector.implementation.ConfigurationChangedBroadcastReceiver

class MainActivity : ReactActivity() {

Expand All @@ -19,9 +22,24 @@ class MainActivity : ReactActivity() {
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
}

override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)

val orientationDirectorCustomAction =
"${packageName}.${ConfigurationChangedBroadcastReceiver.CUSTOM_INTENT_ACTION}"

val intent =
Intent(orientationDirectorCustomAction).apply {
putExtra("newConfig", newConfig)
setPackage(packageName)
}

this.sendBroadcast(intent)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`withRNOrientationMainActivity updates the MainActivity.kt with both import and method implementation 1`] = `
"package com.orientationdirectorexample

import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate

// React Native Orientation Director @generated begin @react-native-orientation-director/library-import - expo prebuild (DO NOT MODIFY) sync-dd77fee7fe624fed474053ea60c3105920a01a6a
import com.orientationdirector.implementation.ConfigurationChangedBroadcastReceiver

// React Native Orientation Director @generated end @react-native-orientation-director/library-import
class MainActivity : ReactActivity() {

/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "OrientationDirectorExample"

/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
}
// React Native Orientation Director @generated begin @react-native-orientation-director/supportedInterfaceOrientationsFor-implementation - expo prebuild (DO NOT MODIFY) sync-7a5cdf10057b2ddf1bcf4593bf408862cbed5473

override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)

val orientationDirectorCustomAction =
packageName + "." + ConfigurationChangedBroadcastReceiver.CUSTOM_INTENT_ACTION

val intent =
Intent(orientationDirectorCustomAction).apply {
putExtra("newConfig", newConfig)
setPackage(packageName)
}

this.sendBroadcast(intent)
}

// React Native Orientation Director @generated end @react-native-orientation-director/supportedInterfaceOrientationsFor-implementation

}
"
`;
30 changes: 30 additions & 0 deletions plugin/__tests__/fixtures/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.orientationdirectorexample

import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate

class MainActivity : ReactActivity() {

/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "OrientationDirectorExample"

/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
}

}
18 changes: 18 additions & 0 deletions plugin/__tests__/withRNOrientationMainActivity.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as fs from 'node:fs';
import * as path from 'node:path';

import { ktFileUpdater } from '../src/withRNOrientationMainActivity';

describe('withRNOrientationMainActivity', function () {
beforeEach(function () {
jest.resetAllMocks();
});

it('updates the MainActivity.kt with both import and method implementation', async function () {
const mainActivityPath = path.join(__dirname, './fixtures/MainActivity.kt');
const mainActivity = await fs.promises.readFile(mainActivityPath, 'utf-8');

const result = ktFileUpdater(mainActivity);
expect(result).toMatchSnapshot();
});
});
2 changes: 2 additions & 0 deletions plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { withAppBridgingHeaderMod } from './custom-mod/withBridgingHeader';
import { withRNOrientationAppDelegate } from './withRNOrientationAppDelegate';
import { withRNOrientationBridgingHeader } from './withRNOrientationBridgingHeader';
import { withRNOrientationMainActivity } from './withRNOrientationMainActivity';

/**
* So, expo config plugin are awesome and the documentation is well written, but I still needed to look around to see
Expand All @@ -22,6 +23,7 @@ const withRNOrientationDirector: ConfigPlugin = (config) => {
return withPlugins(config, [
withRNOrientationAppDelegate,
withRNOrientationBridgingHeader,
withRNOrientationMainActivity,
withAppBridgingHeaderMod,
]);
};
Expand Down
Loading