react-native icon indicating copy to clipboard operation
react-native copied to clipboard

Dimensions.get('window').height not consistent in Android 14 and Android 15

Open arnoldc opened this issue 1 year ago • 10 comments

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)
Screenshot 2024-10-17 at 3 45 37 PM Screenshot_20241017-154526

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

  1. create new project from via npx react-native init projectname and begin running it
  2. 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
  3. 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

arnoldc avatar Oct 17 '24 07:10 arnoldc

this below is the Android 15 (pixel 6a) - Emulator version Screenshot 2024-10-17 at 4 12 03 PM but still its same with physical device , but idk why android 14 and android 15 has different height but it same model??

arnoldc avatar Oct 17 '24 08:10 arnoldc

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.....

kontea-dev avatar Oct 17 '24 10:10 kontea-dev

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?

arnoldc avatar Oct 17 '24 10:10 arnoldc

@alanleedev could you take a look at this one when you have a minute?

cortinico avatar Oct 17 '24 16:10 cortinico

@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.~~ Screenshot 2024-10-18 at 12 22 37 AM

alanleedev avatar Oct 18 '24 08:10 alanleedev

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.

alanleedev avatar Oct 18 '24 09:10 alanleedev

What method could replace in place of Display.getSize() method ? If that is the case @alanleedev

mahishdino avatar Oct 19 '24 18:10 mahishdino

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

arnoldc avatar Oct 21 '24 04:10 arnoldc

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.

alanleedev avatar Oct 21 '24 04:10 alanleedev

@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.

alanleedev avatar Oct 21 '24 04:10 alanleedev

We are discussion what we should do about this. Will update once we have direction.

alanleedev avatar Oct 28 '24 18:10 alanleedev

Hi, thanks for the update. In the meantime, are there any good workarounds? This is impacting the placement of visual elements on our screens.

asherLZR avatar Oct 30 '24 15:10 asherLZR

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

alanleedev avatar Oct 30 '24 18:10 alanleedev

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.

asherLZR avatar Oct 30 '24 21:10 asherLZR

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.

asherLZR avatar Oct 31 '24 08:10 asherLZR

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

soutua avatar Oct 31 '24 10:10 soutua

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?

vytautasvargonas avatar Oct 31 '24 13:10 vytautasvargonas

Having the same issue. Any updates on that?

dgreasi avatar Nov 15 '24 08:11 dgreasi

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.

curtis-jotson avatar Nov 22 '24 22:11 curtis-jotson

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: #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

ucheNkadiCode avatar Nov 30 '24 11:11 ucheNkadiCode

I am facing same issue my app is working as expected in almost every device except mine pixel 7 pro with android 15 :(

saadkhalil01 avatar Dec 13 '24 13:12 saadkhalil01

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())
        }
    }

AlexLacoste avatar Jan 03 '25 14:01 AlexLacoste

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

luca-tomasetti avatar Jan 27 '25 15:01 luca-tomasetti

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); returns 2205 for the y property.
  • On a Pixel 6a simulator on API 35, getWindowManager().getDefaultDisplay().getSize(point); still returns 2205 for the y property.

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)
}

JorenVos avatar Feb 06 '25 10:02 JorenVos

@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?

Jakub-Plan-d-k avatar Jul 06 '25 08:07 Jakub-Plan-d-k

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

cortinico avatar Jul 07 '25 10:07 cortinico

Note that previous PR was reverted and we still need the fix.

alanleedev avatar Sep 05 '25 15:09 alanleedev

I had the same issue and I solved it by using useSafeAreaFrame as explained here: #41918 (comment)

WOW! The most easiest Fix. Thanks a lot!

LucaL1fe avatar Oct 01 '25 04:10 LucaL1fe

@alanleedev is there a planned fix for this? Is the current dimensions api still inconsistent?

androideveloper avatar Oct 29 '25 13:10 androideveloper

I see https://github.com/facebook/react-native/commit/3b185e4bcef24e0689cccd4cf250d469b114d4da has been included in a release, does this mean this issue can be closed?

SimpleCreations avatar Nov 07 '25 13:11 SimpleCreations