expo-screen-orientation
Version:
Expo universal module for managing device's screen orientation
286 lines (259 loc) • 10.7 kB
text/typescript
import { Platform, type EventSubscription, UnavailabilityError } from 'expo-modules-core';
import { Dimensions } from 'react-native';
import ExpoScreenOrientation from './ExpoScreenOrientation';
import {
Orientation,
type OrientationChangeEvent,
type OrientationChangeListener,
OrientationLock,
type PlatformOrientationInfo,
WebOrientationLock,
} from './ScreenOrientation.types';
export {
Orientation,
OrientationLock,
type PlatformOrientationInfo,
type OrientationChangeListener,
type OrientationChangeEvent,
WebOrientationLock,
WebOrientation,
SizeClassIOS,
type ScreenOrientationInfo,
} from './ScreenOrientation.types';
// TODO(@kitten): Remove re-export from EMC
export type { EventSubscription as Subscription } from 'expo-modules-core';
let _orientationChangeSubscribers: EventSubscription[] = [];
let _lastOrientationLock: OrientationLock = OrientationLock.UNKNOWN;
// @needsAudit
/**
* Lock the screen orientation to a particular `OrientationLock`.
* @param orientationLock The orientation lock to apply. See the [`OrientationLock`](#orientationlock)
* enum for possible values.
* @return Returns a promise with `void` value, which fulfils when the orientation is set.
*
* @example
* ```ts
* async function changeScreenOrientation() {
* await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_LEFT);
* }
* ```
*/
export async function lockAsync(orientationLock: OrientationLock): Promise<void> {
if (!ExpoScreenOrientation.lockAsync) {
throw new UnavailabilityError('ScreenOrientation', 'lockAsync');
}
const orientationLocks = Object.values(OrientationLock);
if (!orientationLocks.includes(orientationLock)) {
throw new TypeError(`Invalid Orientation Lock: ${orientationLock}`);
}
if (orientationLock === OrientationLock.OTHER) {
return;
}
await ExpoScreenOrientation.lockAsync(orientationLock);
_lastOrientationLock = orientationLock;
}
// @needsAudit @docsMissing
/**
* @param options The platform specific lock to apply. See the [`PlatformOrientationInfo`](#platformorientationinfo)
* object type for the different platform formats.
* @return Returns a promise with `void` value, resolving when the orientation is set and rejecting
* if an invalid option or value is passed.
*/
export async function lockPlatformAsync(options: PlatformOrientationInfo): Promise<void> {
if (!ExpoScreenOrientation.lockPlatformAsync) {
throw new UnavailabilityError('ScreenOrientation', 'lockPlatformAsync');
}
const { screenOrientationConstantAndroid, screenOrientationArrayIOS, screenOrientationLockWeb } =
options;
let platformOrientationParam: any;
if (Platform.OS === 'android' && screenOrientationConstantAndroid) {
if (isNaN(screenOrientationConstantAndroid)) {
throw new TypeError(
`lockPlatformAsync Android platform: screenOrientationConstantAndroid cannot be called with ${screenOrientationConstantAndroid}`
);
}
platformOrientationParam = screenOrientationConstantAndroid;
} else if (Platform.OS === 'ios' && screenOrientationArrayIOS) {
if (!Array.isArray(screenOrientationArrayIOS)) {
throw new TypeError(
`lockPlatformAsync iOS platform: screenOrientationArrayIOS cannot be called with ${screenOrientationArrayIOS}`
);
}
const orientations = Object.values(Orientation);
for (const orientation of screenOrientationArrayIOS) {
if (!orientations.includes(orientation)) {
throw new TypeError(
`lockPlatformAsync iOS platform: ${orientation} is not a valid Orientation`
);
}
}
platformOrientationParam = screenOrientationArrayIOS;
} else if (Platform.OS === 'web' && screenOrientationLockWeb) {
const webOrientationLocks = Object.values(WebOrientationLock);
if (!webOrientationLocks.includes(screenOrientationLockWeb)) {
throw new TypeError(`Invalid Web Orientation Lock: ${screenOrientationLockWeb}`);
}
platformOrientationParam = screenOrientationLockWeb;
}
if (!platformOrientationParam) {
throw new TypeError('lockPlatformAsync cannot be called with undefined option properties');
}
await ExpoScreenOrientation.lockPlatformAsync(platformOrientationParam);
_lastOrientationLock = OrientationLock.OTHER;
}
// @needsAudit
/**
* Sets the screen orientation back to the `OrientationLock.DEFAULT` policy.
* @return Returns a promise with `void` value, which fulfils when the orientation is set.
*/
export async function unlockAsync(): Promise<void> {
if (!ExpoScreenOrientation.lockAsync) {
throw new UnavailabilityError('ScreenOrientation', 'lockAsync');
}
await ExpoScreenOrientation.lockAsync(OrientationLock.DEFAULT);
}
// @needsAudit
/**
* Gets the current screen orientation.
* @return Returns a promise that fulfils with an [`Orientation`](#orientation)
* value that reflects the current screen orientation.
*/
export async function getOrientationAsync(): Promise<Orientation> {
if (!ExpoScreenOrientation.getOrientationAsync) {
throw new UnavailabilityError('ScreenOrientation', 'getOrientationAsync');
}
return await ExpoScreenOrientation.getOrientationAsync();
}
// @needsAudit
/**
* Gets the current screen orientation lock type.
* @return Returns a promise which fulfils with an [`OrientationLock`](#orientationlock)
* value.
*/
export async function getOrientationLockAsync(): Promise<OrientationLock> {
if (!ExpoScreenOrientation.getOrientationLockAsync) {
return _lastOrientationLock;
}
return await ExpoScreenOrientation.getOrientationLockAsync();
}
// @needsAudit
/**
* Gets the platform specific screen orientation lock type.
* @return Returns a promise which fulfils with a [`PlatformOrientationInfo`](#platformorientationinfo)
* value.
*/
export async function getPlatformOrientationLockAsync(): Promise<PlatformOrientationInfo> {
const platformOrientationLock = await ExpoScreenOrientation.getPlatformOrientationLockAsync();
if (Platform.OS === 'android') {
return {
screenOrientationConstantAndroid: platformOrientationLock,
};
} else if (Platform.OS === 'ios') {
return {
screenOrientationArrayIOS: platformOrientationLock,
};
} else if (Platform.OS === 'web') {
return {
screenOrientationLockWeb: platformOrientationLock,
};
} else {
return {};
}
}
// @needsAudit @docsMissing
/**
* Returns whether the [`OrientationLock`](#orientationlock) policy is supported on
* the device.
* @param orientationLock
* @return Returns a promise that resolves to a `boolean` value that reflects whether or not the
* orientationLock is supported.
*/
export async function supportsOrientationLockAsync(
orientationLock: OrientationLock
): Promise<boolean> {
if (!ExpoScreenOrientation.supportsOrientationLockAsync) {
throw new UnavailabilityError('ScreenOrientation', 'supportsOrientationLockAsync');
}
const orientationLocks = Object.values(OrientationLock);
if (!orientationLocks.includes(orientationLock)) {
throw new TypeError(`Invalid Orientation Lock: ${orientationLock}`);
}
return await ExpoScreenOrientation.supportsOrientationLockAsync(orientationLock);
}
// We rely on RN to emit `didUpdateDimensions`
// If this method no longer works, it's possible that the underlying RN implementation has changed
// see https://github.com/facebook/react-native/blob/c31f79fe478b882540d7fd31ee37b53ddbd60a17/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java#L90
// @needsAudit
/**
* Invokes the `listener` function when the screen orientation changes from `portrait` to `landscape`
* or from `landscape` to `portrait`. For example, it won't be invoked when screen orientation
* change from `portrait up` to `portrait down`, but it will be called when there was a change from
* `portrait up` to `landscape left`.
* @param listener Each orientation update will pass an object with the new [`OrientationChangeEvent`](#orientationchangeevent)
* to the listener.
*/
export function addOrientationChangeListener(
listener: OrientationChangeListener
): EventSubscription {
if (typeof listener !== 'function') {
throw new TypeError(`addOrientationChangeListener cannot be called with ${listener}`);
}
const subscription = createDidUpdateDimensionsSubscription(listener);
_orientationChangeSubscribers.push(subscription);
return subscription;
}
// We need to keep track of our own subscribers because EventEmitter uses a shared subscriber
// from NativeEventEmitter that is registered to the same eventTypes as us. Directly calling
// removeAllListeners(eventName) will remove other module's subscribers.
// @needsAudit
/**
* Removes all listeners subscribed to orientation change updates.
* @deprecated this function will be removed in future versions. Keep track of your own subscriptions.
*/
export function removeOrientationChangeListeners(): void {
// Remove listener by subscription instead of eventType to avoid clobbering Dimension module's subscription of didUpdateDimensions
let i = _orientationChangeSubscribers.length;
while (i--) {
const subscriber = _orientationChangeSubscribers[i];
subscriber?.remove();
// remove after a successful unsubscribe
_orientationChangeSubscribers.pop();
}
}
// @needsAudit
/**
* Unsubscribes the listener associated with the `Subscription` object from all orientation change
* updates.
* @param subscription A subscription object that manages the updates passed to a listener function
* on an orientation change.
* @deprecated this function will be removed in a future version. Use `subscription.remove()` instead.
*/
export function removeOrientationChangeListener(subscription: EventSubscription): void {
if (!subscription || !subscription.remove) {
throw new TypeError(`Must pass in a valid subscription`);
}
subscription.remove();
_orientationChangeSubscribers = _orientationChangeSubscribers.filter(
(sub) => sub !== subscription
);
}
function createDidUpdateDimensionsSubscription(
listener: OrientationChangeListener
): EventSubscription {
if (Platform.OS === 'web' || Platform.OS === 'ios') {
return ExpoScreenOrientation.addListener(
'expoDidUpdateDimensions',
async (update: OrientationChangeEvent) => {
listener(update);
}
);
}
// We rely on the RN Dimensions to emit the `didUpdateDimensions` event on Android
return Dimensions.addEventListener('change', async () => {
const [orientationLock, orientation] = await Promise.all([
getOrientationLockAsync(),
getOrientationAsync(),
]);
listener({ orientationInfo: { orientation }, orientationLock });
});
}