Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,16 @@
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;
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
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.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;
Expand Down Expand Up @@ -859,6 +862,29 @@ public void onConfigurationChanged(Context updatedContext, @Nullable Configurati

ReactContext currentReactContext = getCurrentReactContext();
if (currentReactContext != null) {
if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) {
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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ private void init() {
setClipChildren(false);

if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) {
DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext());
DisplayMetricsHolder.initDisplayMetrics(getContext());
}
}

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -991,7 +991,7 @@ private void checkForDeviceOrientationChanges() {
return;
}
mDeviceRotation = rotation;
DisplayMetricsHolder.initDisplayMetrics(getContext().getApplicationContext());
DisplayMetricsHolder.initDisplayMetrics(getContext());
emitOrientationChanged(rotation);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ public <T extends View> 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,
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -737,14 +737,14 @@ public class ReactHostImpl(
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) {
val didDisplayMetricsChange = DisplayMetricsHolder.updateDisplayMetricsIfChanged(context)

@fabriziocucci fabriziocucci Jun 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for gating both ReactHostImpl and ReactInstanceManager on enableFontScaleChangesUpdatingLayout(). That covers the consistency point.

One thing I noticed on the new arch path: setPixelDensity seems to be the only thing that sets the Fabric pixel density and it looks global (no surfaceId). The relayout here goes through setConstraints, which seems to carry width/height/offset/RTL but no scale factor. A relayout alone might not refresh the density. Old arch ReactInstanceManager pushes the new density via updateDisplayMetricDensity() but I don't see ReactHostImpl (or anything else in runtime/) doing the same. So on new arch a density change seems like it might relayout with a stale C++ pixel density, which would keep sp -> px wrong.

I might be missing something here. The test plan also doesn't seem to cover a device run. Did you get a chance to check the density actually updates on new arch (bridgeless)? If not, it seems like ReactHostImpl may also need to call updateDisplayMetricDensity().

if (didDisplayMetricsChange) {
synchronized(attachedSurfaces) {
attachedSurfaces.forEach { surface -> surface.view?.requestLayout() }
}

val deviceInfoModule = currentReactContext.getNativeModule(DeviceInfoModule::class.java)
deviceInfoModule?.emitUpdateDimensionsEvent()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading