From aa346742c7231927564e4a3504942f275294438d Mon Sep 17 00:00:00 2001 From: Douwe Bos Date: Wed, 20 May 2026 17:41:37 +0200 Subject: [PATCH] fix(android): use the context's display for display metrics, not the device default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DisplayMetricsHolder.initDisplayMetrics populated screenDisplayMetrics by calling getRealMetrics on WindowManager.defaultDisplay, which is always the device's primary display regardless of which display the activity is running on. On a secondary display (Samsung DeX, desktop mode, external monitor, freeform window) that reports the primary display's density and dimensions, so PixelUtil's dp <-> px conversion — and therefore Fabric's layout — scales content for the wrong display. The visible region ends up clipped to a fraction of the activity window and text renders at sub-pixel positions. Use Context.getDisplay() (API 30+) so the metrics come from the display the context is actually associated with, falling back to defaultDisplay on older API levels. Also pass the view's own context (not the application context) from ReactRootView, so the context is associated with the activity's display. Fixes #56894 (also tracked in #55659). Changelog: [ANDROID] [FIXED] - Display metrics now reflect the activity's display instead of the device's default display, fixing layout scaling on secondary displays / desktop mode / freeform windows. --- .../java/com/facebook/react/ReactRootView.java | 6 +++--- .../react/uimanager/DisplayMetricsHolder.kt | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index a6188100c52e..fedc53553a7a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -137,7 +137,7 @@ private void init() { setClipChildren(false); if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) { - DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext()); + DisplayMetricsHolder.initDisplayMetrics(getContext()); } } @@ -932,7 +932,7 @@ private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLay private int mDeviceRotation = 0; /* package */ CustomGlobalLayoutListener() { - DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(getContext().getApplicationContext()); + DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(getContext()); mVisibleViewArea = new Rect(); } @@ -1007,7 +1007,7 @@ private void checkForDeviceOrientationChanges() { return; } mDeviceRotation = rotation; - DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext()); + DisplayMetricsHolder.initDisplayMetrics(getContext()); emitOrientationChanged(rotation); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.kt index 7382c3650aeb..c62494e7e36a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.kt @@ -10,7 +10,9 @@ package com.facebook.react.uimanager import android.annotation.SuppressLint import android.app.Activity import android.content.Context +import android.os.Build import android.util.DisplayMetrics +import android.view.Display import android.view.WindowManager import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat @@ -54,9 +56,19 @@ public object DisplayMetricsHolder { val screenDisplayMetrics = DisplayMetrics() screenDisplayMetrics.setTo(displayMetrics) try { - val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + // Use the display the context is associated with, not the device's + // default display. They differ when the activity is running on a + // secondary display (Samsung DeX, desktop mode, external monitor, + // freeform window) — using the default display there reports the + // primary display's density/dimensions and breaks layout scaling. + val display: Display? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + context.display + } else { + (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + } // getRealMetrics includes system decor (e.g. nav bar) excluded from resource metrics. - wm.defaultDisplay.getRealMetrics(screenDisplayMetrics) + display?.getRealMetrics(screenDisplayMetrics) } catch (_: Exception) { // Non-visual contexts (e.g. Application) may throw on API 30+. // Falls back to resource display metrics copied via setTo() above.