From 19a6ac72a8ab89e338d151de53cdf65baa9111ba Mon Sep 17 00:00:00 2001 From: Nikhil Pakhloo Date: Sat, 20 Jun 2026 13:44:19 +0530 Subject: [PATCH] Fix Android display metrics refresh on density changes --- .../facebook/react/ReactInstanceManager.java | 23 ++++++++ .../com/facebook/react/ReactRootView.java | 6 +- .../react/fabric/FabricUIManager.java | 7 +++ .../facebook/react/runtime/ReactHostImpl.kt | 18 +++--- .../react/uimanager/DisplayMetricsHolder.kt | 25 ++++++++- .../uimanager/DisplayMetricsHolderTest.kt | 55 +++++++++++++++++++ 6 files changed, 120 insertions(+), 14 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index 8f97e9e39dac..1ea081f0ba44 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -72,6 +72,7 @@ import com.facebook.react.devsupport.interfaces.PackagerStatusCallback; import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager; import com.facebook.react.devsupport.interfaces.RedBoxHandler; +import com.facebook.react.fabric.FabricUIManager; import com.facebook.react.interfaces.TaskInterface; import com.facebook.react.internal.AndroidChoreographerProvider; import com.facebook.react.internal.ChoreographerProvider; @@ -79,6 +80,7 @@ import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.ReactChoreographer; +import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.packagerconnection.RequestHandler; import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.ReactRoot; @@ -859,6 +861,27 @@ public void onConfigurationChanged(Context updatedContext, @Nullable Configurati ReactContext currentReactContext = getCurrentReactContext(); if (currentReactContext != null) { + boolean didDisplayMetricsChange = + DisplayMetricsHolder.updateDisplayMetricsIfChanged(updatedContext); + if (didDisplayMetricsChange) { + @Nullable UIManager uiManager = UIManagerHelper.getUIManager(currentReactContext, FABRIC); + if (uiManager instanceof FabricUIManager) { + ((FabricUIManager) uiManager).updateDisplayMetricDensity(); + } + + synchronized (mAttachedReactRoots) { + for (ReactRoot reactRoot : mAttachedReactRoots) { + reactRoot.getRootViewGroup().requestLayout(); + } + } + + DeviceInfoModule deviceInfoModule = + currentReactContext.getNativeModule(DeviceInfoModule.class); + if (deviceInfoModule != null) { + deviceInfoModule.emitUpdateDimensionsEvent(); + } + } + AppearanceModule appearanceModule = currentReactContext.getNativeModule(AppearanceModule.class); 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 f55eca6e4a7a..a6246e1d203d 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()); } } @@ -916,7 +916,7 @@ private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLay private int mDeviceRotation = 0; /* package */ CustomGlobalLayoutListener() { - DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(getContext().getApplicationContext()); + DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(getContext()); mVisibleViewArea = new Rect(); } @@ -991,7 +991,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/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index ee64131ab187..a9bff054d1c4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -361,6 +361,7 @@ public int startSurface( UiThreadUtil.isOnUiThread() ? RootViewUtil.getViewportOffset(rootView) : new Point(0, 0); Assertions.assertNotNull(mBinding, "Binding in FabricUIManager is null"); + mBinding.setPixelDensity(context.getResources().getDisplayMetrics().density); mBinding.startSurfaceWithConstraints( rootTag, moduleName, @@ -1033,6 +1034,12 @@ void setBinding(FabricUIManagerBinding binding) { mBinding = binding; } + public void updateDisplayMetricDensity() { + if (mBinding != null) { + mBinding.setPixelDensity(PixelUtil.getDisplayMetricDensity()); + } + } + /** * Updates the layout metrics of the root view based on the Measure specs received by parameters. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt index 66fa21ae9df3..c49852209765 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -62,12 +62,12 @@ import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatur import com.facebook.react.modules.appearance.AppearanceModule import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler import com.facebook.react.modules.core.DeviceEventManagerModule +import com.facebook.react.modules.deviceinfo.DeviceInfoModule import com.facebook.react.modules.systeminfo.AndroidInfoHelpers import com.facebook.react.runtime.internal.bolts.Task import com.facebook.react.runtime.internal.bolts.TaskCompletionSource import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder import com.facebook.react.uimanager.DisplayMetricsHolder -import com.facebook.react.uimanager.PixelUtil import com.facebook.react.uimanager.events.BlackHoleEventDispatcher import com.facebook.react.uimanager.events.EventDispatcher import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper @@ -736,16 +736,14 @@ public class ReactHostImpl( override fun onConfigurationChanged(context: Context) { val currentReactContext = this.currentReactContext if (currentReactContext != null) { - if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) { - val previousFontScale = PixelUtil.toPixelFromSP(1.0) - DisplayMetricsHolder.initDisplayMetrics(currentReactContext) - val newFontScale = PixelUtil.toPixelFromSP(1.0) - - if (previousFontScale != newFontScale) { - synchronized(attachedSurfaces) { - attachedSurfaces.forEach { surface -> surface.view?.requestLayout() } - } + val didDisplayMetricsChange = DisplayMetricsHolder.updateDisplayMetricsIfChanged(context) + if (didDisplayMetricsChange) { + synchronized(attachedSurfaces) { + attachedSurfaces.forEach { surface -> surface.view?.requestLayout() } } + + val deviceInfoModule = currentReactContext.getNativeModule(DeviceInfoModule::class.java) + deviceInfoModule?.emitUpdateDimensionsEvent() } val appearanceModule = currentReactContext.getNativeModule(AppearanceModule::class.java) 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..861afa2495fc 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 @@ -50,6 +50,29 @@ public object DisplayMetricsHolder { @SuppressLint("DeprecatedMethod") // for Android Lint @Suppress("DEPRECATION") // for Kotlin compiler public fun initDisplayMetrics(context: Context) { + screenDisplayMetrics = getDisplayMetrics(context) + } + + @JvmStatic + @SuppressLint("DeprecatedMethod") // for Android Lint + @Suppress("DEPRECATION") // for Kotlin compiler + public fun updateDisplayMetricsIfChanged(context: Context): Boolean { + val oldMetrics = screenDisplayMetrics + val newMetrics = getDisplayMetrics(context) + val didChange = + oldMetrics == null || + oldMetrics.widthPixels != newMetrics.widthPixels || + oldMetrics.heightPixels != newMetrics.heightPixels || + oldMetrics.density != newMetrics.density || + oldMetrics.scaledDensity != newMetrics.scaledDensity || + oldMetrics.densityDpi != newMetrics.densityDpi + screenDisplayMetrics = newMetrics + return didChange + } + + @SuppressLint("DeprecatedMethod") // for Android Lint + @Suppress("DEPRECATION") // for Kotlin compiler + private fun getDisplayMetrics(context: Context): DisplayMetrics { val displayMetrics = context.resources.displayMetrics val screenDisplayMetrics = DisplayMetrics() screenDisplayMetrics.setTo(displayMetrics) @@ -65,7 +88,7 @@ public object DisplayMetricsHolder { // physical display metrics without the system font scale setting. // This is needed for proper text scaling when fontScale < 1.0 screenDisplayMetrics.scaledDensity = displayMetrics.scaledDensity - DisplayMetricsHolder.screenDisplayMetrics = screenDisplayMetrics + return screenDisplayMetrics } internal fun getStatusBarHeightPx(activity: Activity?): Int { diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/DisplayMetricsHolderTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/DisplayMetricsHolderTest.kt index e6e0858c45f7..8818c6e0b14c 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/DisplayMetricsHolderTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/DisplayMetricsHolderTest.kt @@ -80,6 +80,61 @@ class DisplayMetricsHolderTest { assertThat(DisplayMetricsHolder.getScreenDisplayMetrics()).isNotNull() } + @Test + fun updateDisplayMetricsIfChanged_returnsTrueAndUpdatesMetricsWhenMetricsChange() { + val originalMetrics = DisplayMetrics().apply { + density = 2.0f + scaledDensity = 2.0f + widthPixels = 1000 + heightPixels = 2000 + densityDpi = DisplayMetrics.DENSITY_XHIGH + } + val updatedMetrics = DisplayMetrics().apply { + density = 1.5f + scaledDensity = 1.5f + widthPixels = 1200 + heightPixels = 1800 + densityDpi = DisplayMetrics.DENSITY_HIGH + } + val mockContext: Context = mock() + val mockResources: android.content.res.Resources = mock() + + DisplayMetricsHolder.setScreenDisplayMetrics(originalMetrics) + whenever(mockContext.resources).thenReturn(mockResources) + whenever(mockResources.displayMetrics).thenReturn(updatedMetrics) + whenever(mockContext.getSystemService(Context.WINDOW_SERVICE)) + .thenThrow(IllegalStateException("non-visual context")) + + val didChange = DisplayMetricsHolder.updateDisplayMetricsIfChanged(mockContext) + + assertThat(didChange).isTrue() + assertThat(DisplayMetricsHolder.getScreenDisplayMetrics().density).isEqualTo(1.5f) + assertThat(DisplayMetricsHolder.getScreenDisplayMetrics().scaledDensity).isEqualTo(1.5f) + } + + @Test + fun updateDisplayMetricsIfChanged_returnsFalseWhenMetricsMatch() { + val metrics = DisplayMetrics().apply { + density = 2.0f + scaledDensity = 2.0f + widthPixels = 1000 + heightPixels = 2000 + densityDpi = DisplayMetrics.DENSITY_XHIGH + } + val mockContext: Context = mock() + val mockResources: android.content.res.Resources = mock() + + DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics().apply { setTo(metrics) }) + whenever(mockContext.resources).thenReturn(mockResources) + whenever(mockResources.displayMetrics).thenReturn(metrics) + whenever(mockContext.getSystemService(Context.WINDOW_SERVICE)) + .thenThrow(IllegalStateException("non-visual context")) + + val didChange = DisplayMetricsHolder.updateDisplayMetricsIfChanged(mockContext) + + assertThat(didChange).isFalse() + } + @Test fun initDisplayMetricsIfNotInitialized_onlyInitializesOnce() { DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(context)