Dimensions.get('window').height not consistent in Android 14 and Android 15
Description
Hi React Native team
There seems to be issue with the latest Android 15 release,
The Dimensions.get('window').height seems not consistent in the model device Pixel6a , i think its same for other pixel devices which has latest android 15 recently
| Android 14 (pixel 6a - emulator) | Android 15 (pixel 6a - real device) |
|---|---|
I tried using the react native boiler plate which is running on 0.75.4 as you can see the numbers in the screenshot for height are different, so any idea whats causing the issue why Android 15 has different results from android 14, even if their model device is same?
Steps to reproduce
- create new project from via
npx react-native init projectnameand begin running it - Use two android version for this test , particularly use Android 14 Pixel 6A , and another Android 15 Pixel 6A i think any pixel version will do
- Add this text as the content <Text style={{ fontSize: 30 }}>HEIGHT: {Dimensions.get('window').height}</Text> and begin comparing the two
React Native Version
0.75.4
Affected Platforms
Runtime - Android
Output of npx react-native info
info Fetching system and libraries information...
System:
OS: macOS 14.2.1
CPU: (10) arm64 Apple M1 Pro
Memory: 104.83 MB / 16.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 18.17.1
path: ~/.nvm/versions/node/v18.17.1/bin/node
Yarn:
version: 3.6.4
path: /opt/homebrew/bin/yarn
npm:
version: 9.6.7
path: ~/.nvm/versions/node/v18.17.1/bin/npm
Watchman:
version: 2023.07.03.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.14.3
path: /Users/arnoldcamas/.rbenv/shims/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 23.5
- iOS 17.5
- macOS 14.5
- tvOS 17.5
- visionOS 1.2
- watchOS 10.5
Android SDK:
API Levels:
- "28"
- "29"
- "30"
- "31"
- "32"
- "33"
- "34"
Build Tools:
- 29.0.2
- 30.0.2
- 30.0.3
- 31.0.0
- 32.0.0
- 33.0.0
- 33.0.1
- 34.0.0
System Images:
- android-28 | Google APIs ARM 64 v8a
- android-28 | Google ARM64-V8a Play ARM 64 v8a
- android-29 | Intel x86 Atom_64
- android-29 | Google APIs ARM 64 v8a
- android-29 | Google APIs Intel x86 Atom
- android-29 | Google Play ARM 64 v8a
- android-30 | Google APIs ARM 64 v8a
- android-30 | Google APIs Intel x86_64 Atom
- android-30 | Google Play ARM 64 v8a
- android-30 | Google APIs ATD ARM 64 v8a
- android-30 | Google APIs ATD Intel x86 Atom
- android-31 | Google APIs ARM 64 v8a
- android-31 | Google APIs Intel x86_64 Atom
- android-31 | Google Play ARM 64 v8a
- android-32 | Google APIs ARM 64 v8a
- android-33 | Google APIs ARM 64 v8a
- android-34 | Google Play ARM 64 v8a
- android-35 | Google APIs ARM 64 v8a
Android NDK: Not Found
IDEs:
Android Studio: 2024.1 AI-241.18034.62.2412.12266719
Xcode:
version: 15.4/15F31d
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.10
path: /Users/arnoldcamas/Library/Java/JavaVirtualMachines/corretto-17.0.10/Contents/Home/bin/javac
Ruby:
version: 2.7.4
path: /Users/arnoldcamas/.rbenv/shims/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 18.3.1
wanted: 18.3.1
react-native:
installed: 0.75.4
wanted: 0.75.4
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: false
iOS:
hermesEnabled: Not found
newArchEnabled: false
Stacktrace or Logs
none
Reproducer
https://snack.expo.dev/@arnoldc/4dc13a
Screenshots and Videos
No response
this below is the Android 15 (pixel 6a) - Emulator version
but still its same with physical device , but idk why android 14 and android 15 has different height but it same model??
Same for me on Pixel 6 API 34 vs Pixel 6 API 35 Using height from useWindowDimensions() : Pixel 6 API 34 height => 841.5238..... Pixel 6 API 35 height => 914.2857.....
not sure but i think it was the bump of height in the bar found in the bottom of devices for pixel devices , idk 🤷 if im correct
I also use the hook useWindowDimensions height value
-> e.g const {height, width, scale, fontScale} = useWindowDimensions();
, and the differences is much bigger now , i think there is an issue with calculating the dimension for android 15? any thoughts on this?
@alanleedev could you take a look at this one when you have a minute?
@cortinico
This behavior is not well documented but seems like on Android 15 height for window and screen is returning the same.
Systems bars appear to be ignored.
Emulator screenshots. Left is Android 15 and right is Android 14 on same device type.
~~Looking into fixing this to match existing behavior.~~
Although we do not call this directly and is supposed to be deprecated since API 30 but there is some info in Display.getSize() documentation which may be relevant: https://developer.android.com/reference/android/view/Display#getSize(android.graphics.Point)
- API level 35 and above, the window size will be returned.
- API level 34 and below, the window size minus system decoration areas and display cutout is returned.
I guess this could be correct if forced edge-to-edge is enabled. It does seem like same values is returned on Android 15 regardless of it is forced edge-to-edge or not. Seems like it could be bit tricky to handle and not completely sure if this is something we should try to fix.
What method could replace in place of Display.getSize() method ? If that is the case @alanleedev
so it seems bug in the api of react-native?, if thats the findings as well hmm what should be the workaround for the meantime
any suggestions?
thanks
so it seems bug in the api of react-native?, if thats the findings as well hmm what should be the workaround for the meantime
It is not a bug in react-native. It is actually how the Android API works currently. It behaves differently between Android 14 and Android 15. The question is should we do additional work on top of it to make it work like we expect it to. Doing this can get bit tricky is what I meant.
@mahishdino @arnoldc We do not actually call Display.getSize() directly. I just found hints on what could be happening in its doc. The code that does the widow or screen height calculation is DisplayMetricsHolder.kt.
We are discussion what we should do about this. Will update once we have direction.
Hi, thanks for the update. In the meantime, are there any good workarounds? This is impacting the placement of visual elements on our screens.
Hey folks, I plan to work on a fix to return the expected value. However curious to learn what everyone is using this for to see what kind of workaround we can recommend. @asherLZR @arnoldc
One example is, in a bottom sheet, the initial position starts off screen at Dimensions.get('window').height. When the content is rendered, it gets translated in up to a maximum height also determined by some calculation based on Dimensions. There are also other gesture-based and scroll-based calculations which rely on a meaningful window height.
Naively, one approach for a workaround might be to know the height of the gesture bar, then deduct that from Dimensions.get('window').height or Dimensions.get('screen').height for only API 35. However, I'm not sure if this information is available or if the method is robust.
The only app side workaround that comes to mind at the moment is not to use the Dimensions API at all, but instead add a full screen absolute view (= having position: 'absolute', top: 0, left: 0, right: 0, bottom: 0) e.g. in the app root that listens for the view's onLayout callback, and store the event.nativeEvent.layout.height reported by the onLayout-event, and use that value instead of the value reported by Dimensions API.
Side note, that same approach works as a workaround also for this issue: https://github.com/facebook/react-native/issues/41918
interestingly the Android docs imply that the transition should be enabling edge to edge display. However enabling edge-to-edge as per docs still result in different window size depending on OS version. Has anyone found success with it?
Having the same issue. Any updates on that?
We’ve encountered this issue as well, especially as more users upgrade to Android 15 or purchase new Pixel phones.
In our case, we’re using the window/screen height for some absolutely positioned elements, such as the bottom sheet example mentioned above.
Our workaround has been to use screen dimensions instead of the window, and then apply a hard-coded offset to push elements rendering underneath the status bar or navigation bar into the viewable area. This ensures it looks acceptable across different OS versions and navigation bar configurations.
However, this is far from an ideal solution. There are still cases where this approach doesn’t work well, leading to inconsistencies.
We're very interested in a more consistent workaround or fix.
The only app side workaround that comes to mind at the moment is not to use the Dimensions API at all, but instead add a full screen absolute view (= having
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0) e.g. in the app root that listens for the view'sonLayoutcallback, and store theevent.nativeEvent.layout.heightreported by the onLayout-event, and use that value instead of the value reported by Dimensions API.Side note, that same approach works as a workaround also for this issue: #41918
This is awesome! Thank you so much! I actually didn't need to use the onlayout callback at all and setting position absolute on my container with left, right, top, and bottom set to zero worked perfectly. Works on Ios and android which helps a lot
I am facing same issue my app is working as expected in almost every device except mine pixel 7 pro with android 15 :(
I made my own native function to get the same value for android 15 and android 14 and below.
@ReactMethod(isBlockingSynchronousMethod = true)
fun getScreenDimensions(): WritableMap {
val windowManager = reactApplicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val windowMetrics = windowManager.currentWindowMetrics
val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
val bounds = windowMetrics.bounds
val totalWidthPx = bounds.width()
val totalHeightPx = bounds.height()
val leftInsetPx = insets.left
val rightInsetPx = insets.right
val topInsetPx = insets.top
val bottomInsetPx = insets.bottom
val usableWidthPx = totalWidthPx - leftInsetPx - rightInsetPx
val usableHeightPx = totalHeightPx - topInsetPx - bottomInsetPx
val displayMetrics: DisplayMetrics = reactApplicationContext.resources.displayMetrics
val density = displayMetrics.density
val usableWidthDp = usableWidthPx / density
val usableHeightDp = usableHeightPx / density
return Arguments.createMap().apply {
putDouble("width", usableWidthDp.toDouble())
putDouble("height", usableHeightDp.toDouble())
}
}
I had the same issue and I solved it by using useSafeAreaFrame as explained here: https://github.com/facebook/react-native/issues/41918#issuecomment-2564684192
Google mentions a change of the getSize-method (previously referenced in this issue (link). According to the documentation, it should return a different value for API levels 34 and 35. However, my tests show otherwise when calling this method from MainActivity:
- On a Pixel 6a simulator on API 34,
getWindowManager().getDefaultDisplay().getSize(point);returns2205for theyproperty. - On a Pixel 6a simulator on API 35,
getWindowManager().getDefaultDisplay().getSize(point);still returns2205for theyproperty.
However, when calling it from a native module (non-activity), it returns 2205 on API 34 but 2400 on API 35.
As a temporary workaround, I wanted to retrieve the height from MainActivity when Dimensions.get is being called. In the native code, screen dimensions are stored in the DisplayMetricsHolder from DeviceInfoModule. However, implementing this fix would require recompiling React Native, which I wanted to avoid as a quick solution :D
In the end, I created a patch package that modifies Dimensions.js to use a custom DimensionsModule for Android 15 and higher. (I’m not using the new architecture in this project).
It’s not my proudest piece of code (it relies on deprecated methods), but it restores the same height as on API 34.
Hopefully, a simpler native solution will be found to retrieve the "old" height without relying on deprecated methods.
diff --git a/node_modules/react-native/Libraries/Utilities/Dimensions.js b/node_modules/react-native/Libraries/Utilities/Dimensions.js
index e5775a9..56462b5 100644
--- a/node_modules/react-native/Libraries/Utilities/Dimensions.js
+++ b/node_modules/react-native/Libraries/Utilities/Dimensions.js
@@ -18,6 +18,8 @@ import NativeDeviceInfo, {
type DisplayMetricsAndroid,
} from './NativeDeviceInfo';
import invariant from 'invariant';
+import NativeModules from '../BatchedBridge/NativeModules';
+import Platform from './Platform';
const eventEmitter = new EventEmitter<{
change: [DimensionsPayload],
@@ -111,13 +113,16 @@ class Dimensions {
}
}
+// Workaround for https://github.com/facebook/react-native/issues/47080
+let useCustomImpl = Platform.OS === 'android' && Platform.Version >= 35
+
// Subscribe before calling getConstants to make sure we don't miss any updates in between.
RCTDeviceEventEmitter.addListener(
- 'didUpdateDimensions',
+ useCustomImpl ? 'didUpdateDimensionsCustomImpl' : 'didUpdateDimensions',
(update: DimensionsPayload) => {
Dimensions.set(update);
},
);
-Dimensions.set(NativeDeviceInfo.getConstants().Dimensions);
+Dimensions.set(useCustomImpl ? NativeModules.DimensionsModule.Dimensions : NativeDeviceInfo.getConstants().Dimensions);
export default Dimensions;
DimensionsModule.kt (based on DeviceInfoModule.kt) :
package com.awesomeproject.dimensions
import com.facebook.fbreact.specs.NativeDeviceInfoSpec
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactNoCrashSoftException
import com.facebook.react.bridge.ReactSoftExceptionLogger
import com.facebook.react.bridge.ReadableMap
import com.awesomeproject.dimensions.DimensionsHolder.getDisplayMetricsWritableMap
class DimensionsModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener {
private var reactApplicationContext: ReactApplicationContext? = reactContext
private var fontScale: Float
private var previousDisplayMetrics: ReadableMap? = null
init {
DimensionsHolder.initDisplayMetricsIfNotInitialized(reactContext)
fontScale = reactContext.resources.configuration.fontScale
reactContext.addLifecycleEventListener(this)
}
override fun getConstants(): Map<String, Any> {
val displayMetrics = getDisplayMetricsWritableMap(fontScale.toDouble())
// Cache the initial dimensions for later comparison in emitUpdateDimensionsEvent
previousDisplayMetrics = displayMetrics.copy()
val constants: MutableMap<String, Any> = HashMap()
constants["Dimensions"] = displayMetrics
return constants
}
override fun onHostResume() {
val newFontScale = reactApplicationContext?.resources?.configuration?.fontScale
if (newFontScale != null && newFontScale != fontScale) {
fontScale = newFontScale
emitUpdateDimensionsEvent()
}
}
override fun onHostPause(): Unit = Unit
override fun onHostDestroy(): Unit = Unit
fun emitUpdateDimensionsEvent() {
reactApplicationContext?.let { context ->
if (context.hasActiveReactInstance()) {
// Don't emit an event to JS if the dimensions haven't changed
val displayMetrics = getDisplayMetricsWritableMap(fontScale.toDouble())
if (previousDisplayMetrics == null) {
previousDisplayMetrics = displayMetrics.copy()
} else if (displayMetrics != previousDisplayMetrics) {
previousDisplayMetrics = displayMetrics.copy()
context.emitDeviceEvent("didUpdateDimensionsCustomImpl", displayMetrics)
}
} else {
ReactSoftExceptionLogger.logSoftException(
NativeDeviceInfoSpec.NAME,
ReactNoCrashSoftException(
"No active CatalystInstance, cannot emitUpdateDimensionsEvent"))
}
}
}
override fun getName(): String {
return "DimensionsModule"
}
override fun invalidate() {
super.invalidate()
reactApplicationContext?.removeLifecycleEventListener(this)
}
}
DimensionsHolder.kt (a stripped down version of DisplayMetricsHolder.kt)
package com.awesomeproject.dimensions
import android.content.Context
import android.util.DisplayMetrics
import android.view.WindowManager
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeMap
object DimensionsHolder {
private const val INITIALIZATION_MISSING_MESSAGE =
"DimensionsHolder must be initialized with initDisplayMetricsIfNotInitialized"
@JvmStatic private var windowDisplayMetrics: DisplayMetrics? = null
@JvmStatic private var screenDisplayMetrics: DisplayMetrics? = null
@JvmStatic private var heightFromActivity: Int? = null
@JvmStatic
fun initDisplayMetricsIfNotInitialized(context: Context) {
if (screenDisplayMetrics != null) {
return
}
initDisplayMetrics(context)
}
@JvmStatic
fun initDisplayMetrics(context: Context) {
val displayMetrics = context.resources.displayMetrics
if (heightFromActivity != null) {
displayMetrics.heightPixels = heightFromActivity!!
}
windowDisplayMetrics = displayMetrics
val screenDisplayMetrics = DisplayMetrics()
screenDisplayMetrics.setTo(displayMetrics)
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
checkNotNull(wm) { "WindowManager is null!" }
// Get the real display metrics if we are using API level 17 or higher.
// The real metrics include system decor elements (e.g. soft menu bar).
//
// See:
// http://developer.android.com/reference/android/view/Display.html#getRealMetrics(android.util.DisplayMetrics)
@Suppress("DEPRECATION") wm.defaultDisplay.getRealMetrics(screenDisplayMetrics)
if (heightFromActivity != null) {
screenDisplayMetrics.heightPixels = heightFromActivity!!
}
DimensionsHolder.screenDisplayMetrics = screenDisplayMetrics
}
@JvmStatic
fun setHeightFromActivity(height: Int) {
heightFromActivity = height
}
@JvmStatic
fun getDisplayMetricsWritableMap(fontScale: Double): WritableMap {
checkNotNull(windowDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
checkNotNull(screenDisplayMetrics) { INITIALIZATION_MISSING_MESSAGE }
return WritableNativeMap().apply {
putMap(
"windowPhysicalPixels",
getPhysicalPixelsWritableMap(windowDisplayMetrics as DisplayMetrics, fontScale))
putMap(
"screenPhysicalPixels",
getPhysicalPixelsWritableMap(screenDisplayMetrics as DisplayMetrics, fontScale))
}
}
private fun getPhysicalPixelsWritableMap(
displayMetrics: DisplayMetrics,
fontScale: Double
): WritableMap =
WritableNativeMap().apply {
putInt("width", displayMetrics.widthPixels)
putInt("height", displayMetrics.heightPixels)
putDouble("scale", displayMetrics.density.toDouble())
putDouble("fontScale", fontScale)
putDouble("densityDpi", displayMetrics.densityDpi.toDouble())
}
}
Finally, I had to store the value from the MainActivity in the DimensionsHolder
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
val point = Point()
windowManager.defaultDisplay.getSize(point)
DimensionsHolder.setHeightFromActivity(point.y)
}
@cortinico If I am not mistaken, the commit was reverted a few hours later here:
https://github.com/facebook/react-native/commit/b4dcc9831e12d5dc8cb851c8c051fb98fe5b2eb4
Should the issue be opened again then?
Should the issue be opened again then?
Not needed. We'll re-land the fix later this week. If that doesn't happen, we'll reopen the issue
Note that previous PR was reverted and we still need the fix.
I had the same issue and I solved it by using
useSafeAreaFrameas explained here: #41918 (comment)
WOW! The most easiest Fix. Thanks a lot!
@alanleedev is there a planned fix for this? Is the current dimensions api still inconsistent?
I see https://github.com/facebook/react-native/commit/3b185e4bcef24e0689cccd4cf250d469b114d4da has been included in a release, does this mean this issue can be closed?