From f744d6a79839ffcd44efbb440db1f918faed1408 Mon Sep 17 00:00:00 2001 From: gladiuscode Date: Mon, 28 Sep 2026 10:26:19 +0200 Subject: [PATCH 1/6] feat: add onDisplayChanged event (listenForDisplayChanges) Notify when the app moves to a different physical display, e.g. when a foldable device is folded or unfolded, with the new display size in points (iOS) / dp (Android). Rotations and window resizes don't trigger it. - iOS: emitted when the window scene moves to another screen (iOS 16+) - Android: emitted when the physical size of the display changes, through DisplayManager.DisplayListener - Example: show the last display change in the Explore screen Co-Authored-By: Claude Opus 5.5 --- .../OrientationDirectorModule.kt | 8 ++ .../implementation/DisplayChangesListener.kt | 106 ++++++++++++++++++ .../implementation/EventManager.kt | 8 ++ .../implementation/EventManagerDelegate.kt | 1 + .../OrientationDirectorModuleImpl.kt | 8 ++ example/src/screens/Explore.tsx | 14 +++ ios/OrientationDirector.mm | 8 ++ ios/implementation/EventManager.swift | 10 ++ .../OrientationDirectorImpl.swift | 25 +++++ src/EventEmitter.ts | 7 ++ src/NativeOrientationDirector.ts | 4 + src/RNOrientationDirector.ts | 14 +++ src/types/DisplayChangedEvent.interface.ts | 12 ++ 13 files changed, 225 insertions(+) create mode 100644 android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt create mode 100644 src/types/DisplayChangedEvent.interface.ts diff --git a/android/src/main/java/com/orientationdirector/OrientationDirectorModule.kt b/android/src/main/java/com/orientationdirector/OrientationDirectorModule.kt index 25f201b..9cbca02 100644 --- a/android/src/main/java/com/orientationdirector/OrientationDirectorModule.kt +++ b/android/src/main/java/com/orientationdirector/OrientationDirectorModule.kt @@ -73,6 +73,14 @@ class OrientationDirectorModule(reactContext: ReactApplicationContext) : } } + override fun sendOnDisplayChanged(params: WritableMap) { + try { + emitOnDisplayChanged(params) + } catch(_: Exception) { + // No listener instance yet + } + } + companion object { const val NAME = OrientationDirectorModuleImpl.NAME } diff --git a/android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt b/android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt new file mode 100644 index 0000000..89cb943 --- /dev/null +++ b/android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt @@ -0,0 +1,106 @@ +package com.orientationdirector.implementation + +import android.content.Context +import android.graphics.Point +import android.hardware.display.DisplayManager +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.view.WindowManager +import com.facebook.react.bridge.ReactApplicationContext + +/** + * Notifies when the app moves to a display with a different physical size, + * e.g. when a foldable device is folded or unfolded. + * + * DisplayManager notifies any display change, rotations included, so the + * physical size is compared regardless of its orientation. + */ +class DisplayChangesListener internal constructor(private val context: ReactApplicationContext) : + DisplayManager.DisplayListener { + + private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + private val handler = Handler(Looper.getMainLooper()) + private var isRegistered = false + private var lastPhysicalSize: Pair? = null + + private var onDisplayChangedCallback: ((widthDp: Double, heightDp: Double) -> Unit)? = null + + fun setOnDisplayChangedCallback(callback: (widthDp: Double, heightDp: Double) -> Unit) { + onDisplayChangedCallback = callback + } + + fun register() { + if (isRegistered) { + return + } + + displayManager.registerDisplayListener(this, handler) + isRegistered = true + + // The display might have changed while the listener was unregistered + checkDisplay() + } + + fun unregister() { + if (!isRegistered) { + return + } + + displayManager.unregisterDisplayListener(this) + isRegistered = false + } + + override fun onDisplayAdded(displayId: Int) = Unit + + override fun onDisplayRemoved(displayId: Int) = Unit + + override fun onDisplayChanged(displayId: Int) { + checkDisplay() + } + + private fun checkDisplay() { + val size = computeDisplaySize() ?: return + val physicalSize = Pair(minOf(size.x, size.y), maxOf(size.x, size.y)) + + val previousPhysicalSize = lastPhysicalSize + lastPhysicalSize = physicalSize + + // First sync, nothing changed yet + if (previousPhysicalSize == null || previousPhysicalSize == physicalSize) { + return + } + + val density = computeDensity() + onDisplayChangedCallback?.invoke(size.x / density, size.y / density) + } + + /** + * Returns the size of the display the activity is on, in pixels, + * in the orientation it is currently displayed. + */ + private fun computeDisplaySize(): Point? { + val activity = context.currentActivity ?: return null + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val bounds = activity.windowManager.maximumWindowMetrics.bounds + return Point(bounds.width(), bounds.height()) + } + + val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val size = Point() + @Suppress("DEPRECATION") + windowManager.defaultDisplay.getRealSize(size) + return size + } + + private fun computeDensity(): Double { + val activity = context.currentActivity + + if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return activity.windowManager.maximumWindowMetrics.density.toDouble() + } + + return (activity?.resources ?: context.resources).displayMetrics.density.toDouble() + } +} diff --git a/android/src/main/java/com/orientationdirector/implementation/EventManager.kt b/android/src/main/java/com/orientationdirector/implementation/EventManager.kt index d126d14..3918c76 100644 --- a/android/src/main/java/com/orientationdirector/implementation/EventManager.kt +++ b/android/src/main/java/com/orientationdirector/implementation/EventManager.kt @@ -27,6 +27,14 @@ class EventManager(private val delegate: EventManagerDelegate) { delegate.sendOnLockChanged(params) } + fun sendDisplayDidChange(width: Double, height: Double) { + val params = Arguments.createMap().apply { + putDouble("width", width) + putDouble("height", height) + } + delegate.sendOnDisplayChanged(params) + } + companion object { const val NAME = "OrientationEventManager" } diff --git a/android/src/main/java/com/orientationdirector/implementation/EventManagerDelegate.kt b/android/src/main/java/com/orientationdirector/implementation/EventManagerDelegate.kt index 1f11c17..484bbbd 100644 --- a/android/src/main/java/com/orientationdirector/implementation/EventManagerDelegate.kt +++ b/android/src/main/java/com/orientationdirector/implementation/EventManagerDelegate.kt @@ -6,4 +6,5 @@ fun sendOnDeviceOrientationChanged(params: WritableMap) fun sendOnInterfaceOrientationChanged(params: WritableMap) fun sendOnLockChanged(params: WritableMap) + fun sendOnDisplayChanged(params: WritableMap) } diff --git a/android/src/main/java/com/orientationdirector/implementation/OrientationDirectorModuleImpl.kt b/android/src/main/java/com/orientationdirector/implementation/OrientationDirectorModuleImpl.kt index 27cd29b..c39a88c 100644 --- a/android/src/main/java/com/orientationdirector/implementation/OrientationDirectorModuleImpl.kt +++ b/android/src/main/java/com/orientationdirector/implementation/OrientationDirectorModuleImpl.kt @@ -16,6 +16,7 @@ class OrientationDirectorModuleImpl internal constructor(private val context: Re ) private var mLifecycleListener = LifecycleListener() private var mBroadcastReceiver = ConfigurationChangedBroadcastReceiver(context) + private var mDisplayChangesListener = DisplayChangesListener(context) private var initialSupportedInterfaceOrientations = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED private var lastInterfaceOrientation = Orientation.UNKNOWN @@ -36,6 +37,10 @@ class OrientationDirectorModuleImpl internal constructor(private val context: Re checkInterfaceOrientation(false) } + mDisplayChangesListener.setOnDisplayChangedCallback { width, height -> + mEventManager.sendDisplayDidChange(width, height) + } + // NOTE(1.init): This is needed to compute the initial device orientation mOrientationSensorsEventListener.enable() @@ -46,6 +51,7 @@ class OrientationDirectorModuleImpl internal constructor(private val context: Re } mAutoRotationObserver.enable() mBroadcastReceiver.register() + mDisplayChangesListener.register() } mLifecycleListener.setOnHostPauseCallback { if (!didComputeInitialDeviceOrientation || areOrientationSensorsEnabled) { @@ -53,6 +59,7 @@ class OrientationDirectorModuleImpl internal constructor(private val context: Re } mAutoRotationObserver.disable() mBroadcastReceiver.unregister() + mDisplayChangesListener.unregister() } mLifecycleListener.setOnHostDestroyCallback { if (!didComputeInitialDeviceOrientation || areOrientationSensorsEnabled) { @@ -60,6 +67,7 @@ class OrientationDirectorModuleImpl internal constructor(private val context: Re } mAutoRotationObserver.disable() mBroadcastReceiver.unregister() + mDisplayChangesListener.unregister() } initialSupportedInterfaceOrientations = diff --git a/example/src/screens/Explore.tsx b/example/src/screens/Explore.tsx index ba90ab3..d1cfc4a 100644 --- a/example/src/screens/Explore.tsx +++ b/example/src/screens/Explore.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react'; import { Button, Text, View } from 'react-native'; import { exploreStyle } from './styles'; import RNOrientationDirector, { @@ -13,6 +14,16 @@ function Explore() { const interfaceOrientation = useInterfaceOrientation(); const deviceOrientation = useDeviceOrientation(); const isInterfaceOrientationLocked = useIsInterfaceOrientationLocked(); + const [lastDisplayChange, setLastDisplayChange] = useState('None'); + + useEffect(() => { + const subscription = RNOrientationDirector.listenForDisplayChanges( + ({ width, height }) => { + setLastDisplayChange(`${Math.round(width)}x${Math.round(height)}`); + } + ); + return () => subscription.remove(); + }, []); const handleGoToInnerExploreOnPress = () => { navigation.navigate('InnerExplore' as never); @@ -44,6 +55,9 @@ function Explore() { Is Interface Orientation Locked: {isInterfaceOrientationLocked ? 'Yes' : 'No'} + + Last Display Change:{lastDisplayChange} + ); diff --git a/ios/OrientationDirector.mm b/ios/OrientationDirector.mm index 5d02375..116752b 100644 --- a/ios/OrientationDirector.mm +++ b/ios/OrientationDirector.mm @@ -69,6 +69,14 @@ -(void)emitOnLockChangedWithParams:(NSDictionary*)params { } } +-(void)emitDisplayChangedWithParams:(NSDictionary*)params { + try { + [self emitOnDisplayChanged:params]; + } catch (std::exception &e) { + // Ignore if no listeners + } +} + /// /////////////////////////////////////////////////////////////////////////////////////// diff --git a/ios/implementation/EventManager.swift b/ios/implementation/EventManager.swift index 486712a..6e75b97 100644 --- a/ios/implementation/EventManager.swift +++ b/ios/implementation/EventManager.swift @@ -36,10 +36,20 @@ public class EventManager: NSObject { let params = Dictionary(dictionaryLiteral: ("locked", value)) delegate.emitOnLockChanged(params: params as NSDictionary) } + + func sendDisplayDidChange(size: CGSize) { + guard let delegate = delegate else { + return + } + + let params: [String: Any] = ["width": size.width, "height": size.height] + delegate.emitDisplayChanged(params: params as NSDictionary) + } } @objc public protocol OrientationEventEmitterDelegate { func emitOnLockChanged(params: NSDictionary) func emitDeviceOrientationChanged(params: NSDictionary) func emitInterfaceOrientationChanged(params: NSDictionary) + func emitDisplayChanged(params: NSDictionary) } diff --git a/ios/implementation/OrientationDirectorImpl.swift b/ios/implementation/OrientationDirectorImpl.swift index 238bcee..5c07064 100644 --- a/ios/implementation/OrientationDirectorImpl.swift +++ b/ios/implementation/OrientationDirectorImpl.swift @@ -21,6 +21,11 @@ import UIKit private var lastDeviceOrientation = Orientation.UNKNOWN private var isLocked = false + /// # Only on iOS >= 16 + /// The screen the scene was on at the last geometry change, + /// used to detect when the app moves to another display (e.g. fold / unfold). + private weak var lastScreen: UIScreen? + /// # Only on iOS < 16 /// This variable is needed to prevent a loop where /// we lock the interface to a specific orientation @@ -218,6 +223,8 @@ import UIKit /// interface orientations are ignored (e.g. iPhone Duo inner display) and /// when the scene moves to another screen (fold / unfold). private func onSceneGeometryChanged(scene: UIWindowScene) { + checkDisplay(of: scene) + let interfaceOrientation = utils.getInterfaceOrientation(scene: scene) if interfaceOrientation == .unknown { return @@ -227,6 +234,24 @@ import UIKit updateLastInterfaceOrientationTo(value: newInterfaceOrientation) } + private func checkDisplay(of scene: UIWindowScene) { + let screen = scene.screen + + let previousScreen = lastScreen + lastScreen = screen + + // First sync, nothing changed yet + guard let previousScreen = previousScreen else { + return + } + + if previousScreen === screen { + return + } + + eventManager.sendDisplayDidChange(size: screen.bounds.size) + } + /// # Only on iOS < 16 private func adaptInterfaceTo(deviceOrientation: Orientation) { let supportsLandscape = self.supportedInterfaceOrientations.contains(.landscape) diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index a827218..a19824d 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -2,6 +2,7 @@ import { type EventSubscription, Platform } from 'react-native'; import NativeOrientationDirector from './NativeOrientationDirector'; import type { OrientationEvent } from './types/OrientationEvent.interface'; import type { LockedEvent } from './types/LockedEvent.interface'; +import type { DisplayChangedEvent } from './types/DisplayChangedEvent.interface'; class EventEmitter { private static androidListenerCount = 0; @@ -34,6 +35,12 @@ class EventEmitter { return NativeOrientationDirector.onLockChanged(callback); } + static addDisplayDidChangeListener( + callback: (event: DisplayChangedEvent) => void + ) { + return NativeOrientationDirector.onDisplayChanged(callback); + } + private static createDeviceOrientationListenerProxy( listener: EventSubscription ) { diff --git a/src/NativeOrientationDirector.ts b/src/NativeOrientationDirector.ts index eb87b8a..14e72b7 100644 --- a/src/NativeOrientationDirector.ts +++ b/src/NativeOrientationDirector.ts @@ -28,6 +28,10 @@ export interface Spec extends TurboModule { orientation: number; }>; readonly onLockChanged: CodegenTypes.EventEmitter<{ locked: boolean }>; + readonly onDisplayChanged: CodegenTypes.EventEmitter<{ + width: number; + height: number; + }>; } export default TurboModuleRegistry.getEnforcing('OrientationDirector'); diff --git a/src/RNOrientationDirector.ts b/src/RNOrientationDirector.ts index 26a8148..ae9eee0 100644 --- a/src/RNOrientationDirector.ts +++ b/src/RNOrientationDirector.ts @@ -7,6 +7,7 @@ import { OrientationType } from './types/OrientationType.enum'; import type { OrientationEvent } from './types/OrientationEvent.interface'; import type { LockableOrientation } from './types/LockableOrientation.type'; import type { LockedEvent } from './types/LockedEvent.interface'; +import type { DisplayChangedEvent } from './types/DisplayChangedEvent.interface'; import type { HumanReadableAutoRotationsResource } from './types/HumanReadableAutoRotationsResource.type'; import EventEmitter from './EventEmitter'; @@ -123,6 +124,19 @@ class RNOrientationDirector { return EventEmitter.addLockDidChangeListener(callback); } + /** + * Triggers the provided callback each time the app moves to a different + * physical display, e.g. when a foldable device is folded or unfolded. + * It is not triggered by rotations or by window resizes (split view, multi-window). + * + * @param callback invoked with the new display size + */ + static listenForDisplayChanges( + callback: (event: DisplayChangedEvent) => void + ) { + return EventEmitter.addDisplayDidChangeListener(callback); + } + static convertOrientationToHumanReadableString(orientation: Orientation) { return RNOrientationDirector._humanReadableOrientationsResource[ orientation diff --git a/src/types/DisplayChangedEvent.interface.ts b/src/types/DisplayChangedEvent.interface.ts new file mode 100644 index 0000000..c99b06d --- /dev/null +++ b/src/types/DisplayChangedEvent.interface.ts @@ -0,0 +1,12 @@ +export interface DisplayChangedEvent { + /** + * Width of the new display, in points (iOS) / dp (Android), + * in the orientation it is currently displayed. + */ + width: number; + /** + * Height of the new display, in points (iOS) / dp (Android), + * in the orientation it is currently displayed. + */ + height: number; +} From a318f6479f371e19eba146ac77e9242044d01845 Mon Sep 17 00:00:00 2001 From: gladiuscode Date: Tue, 29 Sep 2026 23:13:14 +0200 Subject: [PATCH 2/6] docs: document listenForDisplayChanges Co-Authored-By: Claude Sonnet 5.5 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 358d33c..f25f1d1 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ This library exports a class called: [RNOrientationDirector](https://github.com/ | listenForDeviceOrientationChanges | Triggers a provided callback each time the device orientation changes | | listenForInterfaceOrientationChanges | Triggers a provided callback each time the interface orientation changes | | listenForLockChanges | Triggers a provided callback each time the interface orientation status changes | +| listenForDisplayChanges | Triggers a provided callback each time the app moves to a different display | | convertOrientationToHumanReadableString | Returns a human readable string based on the given orientation | | convertAutoRotationToHumanReadableString | Returns a human readable string based on the given auto rotation | | setHumanReadableOrientations | Sets the mapping needed to convert orientation values to human readable strings | @@ -220,6 +221,9 @@ On foldable devices the app moves between the outer and the inner display when t outer one. `isLocked` still reports whether a lock has been requested, and the interface orientation always reports the orientation actually displayed. +To be notified when the app moves to another display (e.g. fold / unfold), use `listenForDisplayChanges`: the +orientation values alone might not change in that case. + ### Android Since on Android we need to deal with sensors and their usage, it is worth noting that the device orientation computation works From 464624efbcc9b372b12f292277d5122b47011818 Mon Sep 17 00:00:00 2001 From: gladiuscode Date: Mon, 5 Oct 2026 23:06:07 +0200 Subject: [PATCH 3/6] fix: address display change review comments - android: resync the display on configuration changes to detect moves between existing displays - ios: keep observing the first window scene and ignore activations of unrelated scenes Co-Authored-By: Claude Sonnet 5.5 --- .../implementation/DisplayChangesListener.kt | 11 ++++++++--- .../implementation/OrientationDirectorModuleImpl.kt | 1 + ios/implementation/SceneGeometryListener.swift | 7 +++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt b/android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt index 89cb943..8516068 100644 --- a/android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt +++ b/android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt @@ -39,7 +39,7 @@ class DisplayChangesListener internal constructor(private val context: ReactAppl isRegistered = true // The display might have changed while the listener was unregistered - checkDisplay() + sync() } fun unregister() { @@ -56,10 +56,15 @@ class DisplayChangesListener internal constructor(private val context: ReactAppl override fun onDisplayRemoved(displayId: Int) = Unit override fun onDisplayChanged(displayId: Int) { - checkDisplay() + sync() } - private fun checkDisplay() { + /** + * Compares the current display with the last known one. + * It is also needed when the activity moves between existing displays, + * as DisplayManager doesn't notify it since no display has changed. + */ + fun sync() { val size = computeDisplaySize() ?: return val physicalSize = Pair(minOf(size.x, size.y), maxOf(size.x, size.y)) diff --git a/android/src/main/java/com/orientationdirector/implementation/OrientationDirectorModuleImpl.kt b/android/src/main/java/com/orientationdirector/implementation/OrientationDirectorModuleImpl.kt index c39a88c..eefb0ea 100644 --- a/android/src/main/java/com/orientationdirector/implementation/OrientationDirectorModuleImpl.kt +++ b/android/src/main/java/com/orientationdirector/implementation/OrientationDirectorModuleImpl.kt @@ -35,6 +35,7 @@ class OrientationDirectorModuleImpl internal constructor(private val context: Re mBroadcastReceiver.setOnReceiveCallback { checkInterfaceOrientation(false) + mDisplayChangesListener.sync() } mDisplayChangesListener.setOnDisplayChangedCallback { width, height -> diff --git a/ios/implementation/SceneGeometryListener.swift b/ios/implementation/SceneGeometryListener.swift index df28bba..a26cf30 100644 --- a/ios/implementation/SceneGeometryListener.swift +++ b/ios/implementation/SceneGeometryListener.swift @@ -50,6 +50,13 @@ public class SceneGeometryListener { } @objc private func sceneDidActivate(_ notification: Notification) { + /// Apps can have multiple scenes: once the React Native scene is observed, + /// activations of unrelated scenes must not replace it. A new scene is picked + /// only when the observed one is gone, as observedScene is weak. + if observedScene != nil { + return + } + attach(to: notification.object as? UIWindowScene) } From d3260ad0f3fcd822652ac78e4bfbe746ffa2c9b2 Mon Sep 17 00:00:00 2001 From: gladiuscode Date: Mon, 5 Oct 2026 23:19:41 +0200 Subject: [PATCH 4/6] feat: add useDisplay hook Co-Authored-By: Claude Sonnet 5.5 --- README.md | 1 + src/hooks/useDisplay.hook.ts | 35 +++++++++++++++++++++++++++++++++++ src/index.tsx | 3 +++ 3 files changed, 39 insertions(+) create mode 100644 src/hooks/useDisplay.hook.ts diff --git a/README.md b/README.md index f25f1d1..c574093 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,7 @@ In addition, the library exposes the following hooks: | [useInterfaceOrientation](https://github.com/gladiuscode/react-native-orientation-director/blob/main/src/hooks/useInterfaceOrientation.hook.ts) | Returns the current interface orientation and listens to changes | | [useDeviceOrientation](https://github.com/gladiuscode/react-native-orientation-director/blob/main/src/hooks/useDeviceOrientation.hook.ts) | Returns the current device orientation and listens to changes | | [useIsInterfaceOrientationLocked](https://github.com/gladiuscode/react-native-orientation-director/blob/main/src/hooks/useIsInterfaceOrientationLocked.hook.ts) | Returns the current interface orientation status and listens to changes | +| [useDisplay](https://github.com/gladiuscode/react-native-orientation-director/blob/main/src/hooks/useDisplay.hook.ts) | Returns the size of the current display and listens to display changes | Head over to the [example project](example) to see how to use the library. diff --git a/src/hooks/useDisplay.hook.ts b/src/hooks/useDisplay.hook.ts new file mode 100644 index 0000000..f56d890 --- /dev/null +++ b/src/hooks/useDisplay.hook.ts @@ -0,0 +1,35 @@ +import React from 'react'; +import { Dimensions } from 'react-native'; +import RNOrientationDirector from '../RNOrientationDirector'; +import type { DisplayChangedEvent } from '../types/DisplayChangedEvent.interface'; + +/** + * Hook that returns the size of the display the app is on. + * It starts from the current screen size and listens for display changes + * (e.g. fold / unfold on foldable devices), updating the state accordingly. + * + * The size is the one reported when the display last changed: rotations + * and window resizes do not update it. + */ +const useDisplay = () => { + const [display, setDisplay] = React.useState(() => { + const { width, height } = Dimensions.get('screen'); + return { width, height }; + }); + + React.useEffect(() => { + const onChange = (event: DisplayChangedEvent) => { + setDisplay({ width: event.width, height: event.height }); + }; + + const subscription = + RNOrientationDirector.listenForDisplayChanges(onChange); + return () => { + subscription.remove(); + }; + }, []); + + return display; +}; + +export default useDisplay; diff --git a/src/index.tsx b/src/index.tsx index 8330b3f..53b6142 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,5 +11,8 @@ export { useInterfaceOrientation }; import useIsInterfaceOrientationLocked from './hooks/useIsInterfaceOrientationLocked.hook'; export { useIsInterfaceOrientationLocked }; +import useDisplay from './hooks/useDisplay.hook'; +export { useDisplay }; + import RNOrientationDirector from './RNOrientationDirector'; export default RNOrientationDirector; From 4141d3fa9fc115b5e8ba24b08ddbf3710d07c519 Mon Sep 17 00:00:00 2001 From: gladiuscode Date: Mon, 5 Oct 2026 23:27:43 +0200 Subject: [PATCH 5/6] chore(example): use useDisplay in Explore screen Co-Authored-By: Claude Sonnet 5.5 --- example/src/screens/Explore.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/example/src/screens/Explore.tsx b/example/src/screens/Explore.tsx index d1cfc4a..645545c 100644 --- a/example/src/screens/Explore.tsx +++ b/example/src/screens/Explore.tsx @@ -1,8 +1,8 @@ -import { useEffect, useState } from 'react'; import { Button, Text, View } from 'react-native'; import { exploreStyle } from './styles'; import RNOrientationDirector, { useDeviceOrientation, + useDisplay, useInterfaceOrientation, useIsInterfaceOrientationLocked, } from 'react-native-orientation-director'; @@ -14,16 +14,7 @@ function Explore() { const interfaceOrientation = useInterfaceOrientation(); const deviceOrientation = useDeviceOrientation(); const isInterfaceOrientationLocked = useIsInterfaceOrientationLocked(); - const [lastDisplayChange, setLastDisplayChange] = useState('None'); - - useEffect(() => { - const subscription = RNOrientationDirector.listenForDisplayChanges( - ({ width, height }) => { - setLastDisplayChange(`${Math.round(width)}x${Math.round(height)}`); - } - ); - return () => subscription.remove(); - }, []); + const display = useDisplay(); const handleGoToInnerExploreOnPress = () => { navigation.navigate('InnerExplore' as never); @@ -56,7 +47,8 @@ function Explore() { {isInterfaceOrientationLocked ? 'Yes' : 'No'} - Last Display Change:{lastDisplayChange} + Current Display:{Math.round(display.width)}x + {Math.round(display.height)} From 26466fb8dc46c707abfa5b7604d7a776ed7ade09 Mon Sep 17 00:00:00 2001 From: gladiuscode Date: Tue, 6 Oct 2026 23:52:11 +0200 Subject: [PATCH 6/6] fix(android): compare display size in dp to detect density changes Co-Authored-By: Claude Opus 5.5 --- .../implementation/DisplayChangesListener.kt | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt b/android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt index 8516068..e1b6441 100644 --- a/android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt +++ b/android/src/main/java/com/orientationdirector/implementation/DisplayChangesListener.kt @@ -10,11 +10,12 @@ import android.view.WindowManager import com.facebook.react.bridge.ReactApplicationContext /** - * Notifies when the app moves to a display with a different physical size, + * Notifies when the app moves to a display with a different size in dp, * e.g. when a foldable device is folded or unfolded. * * DisplayManager notifies any display change, rotations included, so the - * physical size is compared regardless of its orientation. + * size is compared regardless of its orientation. It is compared in dp so that + * density changes with the same pixel size are notified as well. */ class DisplayChangesListener internal constructor(private val context: ReactApplicationContext) : DisplayManager.DisplayListener { @@ -22,7 +23,7 @@ class DisplayChangesListener internal constructor(private val context: ReactAppl private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager private val handler = Handler(Looper.getMainLooper()) private var isRegistered = false - private var lastPhysicalSize: Pair? = null + private var lastSizeDp: Pair? = null private var onDisplayChangedCallback: ((widthDp: Double, heightDp: Double) -> Unit)? = null @@ -66,18 +67,20 @@ class DisplayChangesListener internal constructor(private val context: ReactAppl */ fun sync() { val size = computeDisplaySize() ?: return - val physicalSize = Pair(minOf(size.x, size.y), maxOf(size.x, size.y)) + val density = computeDensity() + val widthDp = size.x / density + val heightDp = size.y / density + val sizeDp = Pair(minOf(widthDp, heightDp), maxOf(widthDp, heightDp)) - val previousPhysicalSize = lastPhysicalSize - lastPhysicalSize = physicalSize + val previousSizeDp = lastSizeDp + lastSizeDp = sizeDp // First sync, nothing changed yet - if (previousPhysicalSize == null || previousPhysicalSize == physicalSize) { + if (previousSizeDp == null || previousSizeDp == sizeDp) { return } - val density = computeDensity() - onDisplayChangedCallback?.invoke(size.x / density, size.y / density) + onDisplayChangedCallback?.invoke(widthDp, heightDp) } /**