Skip to content

Commit 19a6ac7

Browse files
committed
Fix Android display metrics refresh on density changes
1 parent dcbd860 commit 19a6ac7

6 files changed

Lines changed: 120 additions & 14 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,15 @@
7272
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback;
7373
import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager;
7474
import com.facebook.react.devsupport.interfaces.RedBoxHandler;
75+
import com.facebook.react.fabric.FabricUIManager;
7576
import com.facebook.react.interfaces.TaskInterface;
7677
import com.facebook.react.internal.AndroidChoreographerProvider;
7778
import com.facebook.react.internal.ChoreographerProvider;
7879
import com.facebook.react.modules.appearance.AppearanceModule;
7980
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
8081
import com.facebook.react.modules.core.DeviceEventManagerModule;
8182
import com.facebook.react.modules.core.ReactChoreographer;
83+
import com.facebook.react.modules.deviceinfo.DeviceInfoModule;
8284
import com.facebook.react.packagerconnection.RequestHandler;
8385
import com.facebook.react.uimanager.DisplayMetricsHolder;
8486
import com.facebook.react.uimanager.ReactRoot;
@@ -859,6 +861,27 @@ public void onConfigurationChanged(Context updatedContext, @Nullable Configurati
859861

860862
ReactContext currentReactContext = getCurrentReactContext();
861863
if (currentReactContext != null) {
864+
boolean didDisplayMetricsChange =
865+
DisplayMetricsHolder.updateDisplayMetricsIfChanged(updatedContext);
866+
if (didDisplayMetricsChange) {
867+
@Nullable UIManager uiManager = UIManagerHelper.getUIManager(currentReactContext, FABRIC);
868+
if (uiManager instanceof FabricUIManager) {
869+
((FabricUIManager) uiManager).updateDisplayMetricDensity();
870+
}
871+
872+
synchronized (mAttachedReactRoots) {
873+
for (ReactRoot reactRoot : mAttachedReactRoots) {
874+
reactRoot.getRootViewGroup().requestLayout();
875+
}
876+
}
877+
878+
DeviceInfoModule deviceInfoModule =
879+
currentReactContext.getNativeModule(DeviceInfoModule.class);
880+
if (deviceInfoModule != null) {
881+
deviceInfoModule.emitUpdateDimensionsEvent();
882+
}
883+
}
884+
862885
AppearanceModule appearanceModule =
863886
currentReactContext.getNativeModule(AppearanceModule.class);
864887

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ private void init() {
137137
setClipChildren(false);
138138

139139
if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) {
140-
DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext());
140+
DisplayMetricsHolder.initDisplayMetrics(getContext());
141141
}
142142
}
143143

@@ -916,7 +916,7 @@ private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLay
916916
private int mDeviceRotation = 0;
917917

918918
/* package */ CustomGlobalLayoutListener() {
919-
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(getContext().getApplicationContext());
919+
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(getContext());
920920
mVisibleViewArea = new Rect();
921921
}
922922

@@ -991,7 +991,7 @@ private void checkForDeviceOrientationChanges() {
991991
return;
992992
}
993993
mDeviceRotation = rotation;
994-
DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext());
994+
DisplayMetricsHolder.initDisplayMetrics(getContext());
995995
emitOrientationChanged(rotation);
996996
}
997997

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ public <T extends View> int startSurface(
361361
UiThreadUtil.isOnUiThread() ? RootViewUtil.getViewportOffset(rootView) : new Point(0, 0);
362362

363363
Assertions.assertNotNull(mBinding, "Binding in FabricUIManager is null");
364+
mBinding.setPixelDensity(context.getResources().getDisplayMetrics().density);
364365
mBinding.startSurfaceWithConstraints(
365366
rootTag,
366367
moduleName,
@@ -1033,6 +1034,12 @@ void setBinding(FabricUIManagerBinding binding) {
10331034
mBinding = binding;
10341035
}
10351036

1037+
public void updateDisplayMetricDensity() {
1038+
if (mBinding != null) {
1039+
mBinding.setPixelDensity(PixelUtil.getDisplayMetricDensity());
1040+
}
1041+
}
1042+
10361043
/**
10371044
* Updates the layout metrics of the root view based on the Measure specs received by parameters.
10381045
*/

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatur
6262
import com.facebook.react.modules.appearance.AppearanceModule
6363
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
6464
import com.facebook.react.modules.core.DeviceEventManagerModule
65+
import com.facebook.react.modules.deviceinfo.DeviceInfoModule
6566
import com.facebook.react.modules.systeminfo.AndroidInfoHelpers
6667
import com.facebook.react.runtime.internal.bolts.Task
6768
import com.facebook.react.runtime.internal.bolts.TaskCompletionSource
6869
import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder
6970
import com.facebook.react.uimanager.DisplayMetricsHolder
70-
import com.facebook.react.uimanager.PixelUtil
7171
import com.facebook.react.uimanager.events.BlackHoleEventDispatcher
7272
import com.facebook.react.uimanager.events.EventDispatcher
7373
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
@@ -736,16 +736,14 @@ public class ReactHostImpl(
736736
override fun onConfigurationChanged(context: Context) {
737737
val currentReactContext = this.currentReactContext
738738
if (currentReactContext != null) {
739-
if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) {
740-
val previousFontScale = PixelUtil.toPixelFromSP(1.0)
741-
DisplayMetricsHolder.initDisplayMetrics(currentReactContext)
742-
val newFontScale = PixelUtil.toPixelFromSP(1.0)
743-
744-
if (previousFontScale != newFontScale) {
745-
synchronized(attachedSurfaces) {
746-
attachedSurfaces.forEach { surface -> surface.view?.requestLayout() }
747-
}
739+
val didDisplayMetricsChange = DisplayMetricsHolder.updateDisplayMetricsIfChanged(context)
740+
if (didDisplayMetricsChange) {
741+
synchronized(attachedSurfaces) {
742+
attachedSurfaces.forEach { surface -> surface.view?.requestLayout() }
748743
}
744+
745+
val deviceInfoModule = currentReactContext.getNativeModule(DeviceInfoModule::class.java)
746+
deviceInfoModule?.emitUpdateDimensionsEvent()
749747
}
750748

751749
val appearanceModule = currentReactContext.getNativeModule(AppearanceModule::class.java)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ public object DisplayMetricsHolder {
5050
@SuppressLint("DeprecatedMethod") // for Android Lint
5151
@Suppress("DEPRECATION") // for Kotlin compiler
5252
public fun initDisplayMetrics(context: Context) {
53+
screenDisplayMetrics = getDisplayMetrics(context)
54+
}
55+
56+
@JvmStatic
57+
@SuppressLint("DeprecatedMethod") // for Android Lint
58+
@Suppress("DEPRECATION") // for Kotlin compiler
59+
public fun updateDisplayMetricsIfChanged(context: Context): Boolean {
60+
val oldMetrics = screenDisplayMetrics
61+
val newMetrics = getDisplayMetrics(context)
62+
val didChange =
63+
oldMetrics == null ||
64+
oldMetrics.widthPixels != newMetrics.widthPixels ||
65+
oldMetrics.heightPixels != newMetrics.heightPixels ||
66+
oldMetrics.density != newMetrics.density ||
67+
oldMetrics.scaledDensity != newMetrics.scaledDensity ||
68+
oldMetrics.densityDpi != newMetrics.densityDpi
69+
screenDisplayMetrics = newMetrics
70+
return didChange
71+
}
72+
73+
@SuppressLint("DeprecatedMethod") // for Android Lint
74+
@Suppress("DEPRECATION") // for Kotlin compiler
75+
private fun getDisplayMetrics(context: Context): DisplayMetrics {
5376
val displayMetrics = context.resources.displayMetrics
5477
val screenDisplayMetrics = DisplayMetrics()
5578
screenDisplayMetrics.setTo(displayMetrics)
@@ -65,7 +88,7 @@ public object DisplayMetricsHolder {
6588
// physical display metrics without the system font scale setting.
6689
// This is needed for proper text scaling when fontScale < 1.0
6790
screenDisplayMetrics.scaledDensity = displayMetrics.scaledDensity
68-
DisplayMetricsHolder.screenDisplayMetrics = screenDisplayMetrics
91+
return screenDisplayMetrics
6992
}
7093

7194
internal fun getStatusBarHeightPx(activity: Activity?): Int {

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/DisplayMetricsHolderTest.kt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,61 @@ class DisplayMetricsHolderTest {
8080
assertThat(DisplayMetricsHolder.getScreenDisplayMetrics()).isNotNull()
8181
}
8282

83+
@Test
84+
fun updateDisplayMetricsIfChanged_returnsTrueAndUpdatesMetricsWhenMetricsChange() {
85+
val originalMetrics = DisplayMetrics().apply {
86+
density = 2.0f
87+
scaledDensity = 2.0f
88+
widthPixels = 1000
89+
heightPixels = 2000
90+
densityDpi = DisplayMetrics.DENSITY_XHIGH
91+
}
92+
val updatedMetrics = DisplayMetrics().apply {
93+
density = 1.5f
94+
scaledDensity = 1.5f
95+
widthPixels = 1200
96+
heightPixels = 1800
97+
densityDpi = DisplayMetrics.DENSITY_HIGH
98+
}
99+
val mockContext: Context = mock()
100+
val mockResources: android.content.res.Resources = mock()
101+
102+
DisplayMetricsHolder.setScreenDisplayMetrics(originalMetrics)
103+
whenever(mockContext.resources).thenReturn(mockResources)
104+
whenever(mockResources.displayMetrics).thenReturn(updatedMetrics)
105+
whenever(mockContext.getSystemService(Context.WINDOW_SERVICE))
106+
.thenThrow(IllegalStateException("non-visual context"))
107+
108+
val didChange = DisplayMetricsHolder.updateDisplayMetricsIfChanged(mockContext)
109+
110+
assertThat(didChange).isTrue()
111+
assertThat(DisplayMetricsHolder.getScreenDisplayMetrics().density).isEqualTo(1.5f)
112+
assertThat(DisplayMetricsHolder.getScreenDisplayMetrics().scaledDensity).isEqualTo(1.5f)
113+
}
114+
115+
@Test
116+
fun updateDisplayMetricsIfChanged_returnsFalseWhenMetricsMatch() {
117+
val metrics = DisplayMetrics().apply {
118+
density = 2.0f
119+
scaledDensity = 2.0f
120+
widthPixels = 1000
121+
heightPixels = 2000
122+
densityDpi = DisplayMetrics.DENSITY_XHIGH
123+
}
124+
val mockContext: Context = mock()
125+
val mockResources: android.content.res.Resources = mock()
126+
127+
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics().apply { setTo(metrics) })
128+
whenever(mockContext.resources).thenReturn(mockResources)
129+
whenever(mockResources.displayMetrics).thenReturn(metrics)
130+
whenever(mockContext.getSystemService(Context.WINDOW_SERVICE))
131+
.thenThrow(IllegalStateException("non-visual context"))
132+
133+
val didChange = DisplayMetricsHolder.updateDisplayMetricsIfChanged(mockContext)
134+
135+
assertThat(didChange).isFalse()
136+
}
137+
83138
@Test
84139
fun initDisplayMetricsIfNotInitialized_onlyInitializesOnce() {
85140
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(context)

0 commit comments

Comments
 (0)