Skip to content

fix(android): use the context's display for display metrics, not the device default#56895

Open
DouweBos wants to merge 1 commit into
facebook:mainfrom
DouweBos:fix/android-secondary-display-pixelutil
Open

fix(android): use the context's display for display metrics, not the device default#56895
DouweBos wants to merge 1 commit into
facebook:mainfrom
DouweBos:fix/android-secondary-display-pixelutil

Conversation

@DouweBos
Copy link
Copy Markdown

@DouweBos DouweBos commented May 19, 2026

Summary:

DisplayMetricsHolder.initDisplayMetrics populates screenDisplayMetrics by calling getRealMetrics on WindowManager.defaultDisplay. defaultDisplay is always the device's primary display, regardless of which display the activity is actually running on.

When an activity runs on a secondary display (Samsung DeX, desktop mode an external monitor, a freeform window, or an emulator virtual display via am start --display N) its density and dimensions differ from the primary's.

PixelUtil (toPixelFromDIP / toDIPFromPixel / toPixelFromSP / getDisplayMetricDensity) reads screenDisplayMetrics for every dp <-> px conversion the layout engine performs, so Fabric lays out content at the primary display's scale into a window that doesn't have that scale. The visible content is clipped to a fraction of the activity window with the rest left black, and text renders at sub-pixel positions (blurry).

This is observable from JS — Dimensions.get('window') and useWindowDimensions() are correct, but Dimensions.get('screen') reports the primary display's scale:

window: { width: 1600, height: 720, scale: 1.5 }   <- the activity's display
screen: { width: 800,  height: 360, scale: 3   }   <- the device's primary display

This PR changes initDisplayMetrics to read from the display the Context is associated with, Context.getDisplay() on API 30+, falling back to WindowManager.defaultDisplay on older API levels, instead of always the device default. It also passes the view's own context (not the application context) from ReactRootView, so that context is associated with the activity's display.

Single-display behavior is unchanged: when the activity is on the primary display, the context's display is the default display, so the metrics are identical.

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.

Test Plan:

Reproducer (standalone minimal app + commands): https://github.com/DouweBos/rn-secondary-display-repro

Unit tests

./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests "com.facebook.react.uimanager.DisplayMetricsHolderTest" --tests "com.facebook.react.uimanager.PixelUtilTest"

A/B on rn-tester, built from source

Built rn-tester from this branch and from its parent commit (this commit reverted), installed both on the same Pixel 9 Pro AVD (API 36) with a virtual secondary display at a different density than the primary:

adb emu multidisplay add 1 2400 1080 240 0   # 2400x1080 @240dpi (1.5x); primary is 3.0x
adb shell am start -n com.facebook.react.uiapp/.RNTesterActivity --display 2
SF_ID=$(adb shell dumpsys SurfaceFlinger --display-id | awk '/Virtual display/ {print $2; exit}')
adb shell screencap -d "$SF_ID" -p /sdcard/d.png && adb pull /sdcard/d.png .

Before (parent commit) — screenDisplayMetrics taken from the primary display; the dp ↔ px conversion uses the primary's 3.0x density on a 1.5x display, so text is clipped/garbled ("Comp" instead of "Components", "Bu", "DrawerLay", …) and the bottom tab indicator is sized for the wrong width:

rn-tester before

After (this branch) — metrics come from the activity's display; layout fills the window and all labels render fully and crisply:

rn-tester after

@meta-cla
Copy link
Copy Markdown

meta-cla Bot commented May 19, 2026

Hi @DouweBos!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@meta-cla
Copy link
Copy Markdown

meta-cla Bot commented May 19, 2026

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 19, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label May 19, 2026
@javache javache requested a review from alanleedev May 20, 2026 09:28
…device default

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 facebook#56894 (also tracked in facebook#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.
@DouweBos DouweBos force-pushed the fix/android-secondary-display-pixelutil branch from eeaa7cd to aa34674 Compare May 20, 2026 15:41
@DouweBos DouweBos changed the title fix(android): use window display metrics for dp <-> px conversion in PixelUtil fix(android): use the context's display for display metrics, not the device default May 20, 2026
@DouweBos
Copy link
Copy Markdown
Author

Sorry, incorrectly assumed the original PixUtil functions were still present. Updated the PR to a more stable fix by relying on the context and the associated display instead of the defaultDisplay it relies on right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] Dimensions.get('screen').scale reports primary display density on secondary displays, breaking Fabric layout

1 participant