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 @@ -21,6 +21,7 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Point;
import android.util.DisplayMetrics;
import android.os.SystemClock;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
Expand Down Expand Up @@ -105,6 +106,7 @@
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;

Expand Down Expand Up @@ -185,6 +187,13 @@ public class FabricUIManager

private final TextEffectRegistry mTextEffectRegistry = new TextEffectRegistry();

/**
* Memoizes the {@link DisplayMetrics} synthesized for each distinct (pointScaleFactor, fontScale)
* pair seen during measurement. In practice this holds one entry per display the app has surfaces
* on, but text measurement is hot enough that allocating per call is worth avoiding.
*/
private final Map<Long, DisplayMetrics> mSurfaceDisplayMetricsCache = new ConcurrentHashMap<>();

private final BatchEventDispatchedListener mBatchEventDispatchedListener;

private final List<UIManagerListener> mListeners = new CopyOnWriteArrayList<>();
Expand Down Expand Up @@ -550,21 +559,52 @@ private NativeArray measureLines(
ReadableMapBuffer attributedString,
ReadableMapBuffer paragraphAttributes,
float width,
float height) {
float height,
float pointScaleFactor,
float fontScale) {
ViewManager textViewManager = mViewManagerRegistry.get(ReactTextViewManager.REACT_CLASS);
DisplayMetrics metrics = surfaceDisplayMetrics(pointScaleFactor, fontScale);

return (NativeArray)
TextLayoutManager.measureLines(
mReactApplicationContext.getAssets(),
ReactTypefaceUtils.getFontWeightAdjustment(mReactApplicationContext),
attributedString,
paragraphAttributes,
PixelUtil.toPixelFromDIP(width),
PixelUtil.toPixelFromDIP(height),
PixelUtil.toPixelFromDIP(width, metrics),
PixelUtil.toPixelFromDIP(height, metrics),
textViewManager instanceof ReactTextViewManagerCallback
? (ReactTextViewManagerCallback) textViewManager
: null,
mTextEffectRegistry);
mTextEffectRegistry,
metrics);
}

/**
* The {@link DisplayMetrics} text should be measured against for the surface currently being laid
* out.
*
* <p>{@code pointScaleFactor} and {@code fontScale} originate from the surface's {@code
* LayoutContext}, which {@link com.facebook.react.runtime.ReactSurfaceImpl} derives from the
* Activity's resources — i.e. from the display the surface is actually on. {@link
* DisplayMetricsHolder} in contrast always describes the device's primary display, so on a
* secondary display (Samsung DeX, desktop mode, an external monitor, a freeform window) the two
* disagree and text is measured at one scale but mounted at another.
*/
private DisplayMetrics surfaceDisplayMetrics(float pointScaleFactor, float fontScale) {
if (!ReactNativeFeatureFlags.enablePerSurfaceTextScaleAndroid()) {
return DisplayMetricsHolder.getScreenDisplayMetrics();
}

long key = (((long) Float.floatToRawIntBits(pointScaleFactor)) << 32) | (Float.floatToRawIntBits(fontScale) & 0xFFFFFFFFL);
DisplayMetrics cached = mSurfaceDisplayMetricsCache.get(key);
if (cached != null) {
return cached;
}

DisplayMetrics metrics = PixelUtil.displayMetricsFor(pointScaleFactor, fontScale);
DisplayMetrics existing = mSurfaceDisplayMetricsCache.putIfAbsent(key, metrics);
return existing != null ? existing : metrics;
}

public @Nullable Integer getColor(int surfaceId, String[] resourcePaths) {
Expand Down Expand Up @@ -639,24 +679,28 @@ public long measureText(
float maxWidth,
float minHeight,
float maxHeight,
@Nullable float[] attachmentsPositions) {
@Nullable float[] attachmentsPositions,
float pointScaleFactor,
float fontScale) {

ViewManager textViewManager = mViewManagerRegistry.get(ReactTextViewManager.REACT_CLASS);
DisplayMetrics metrics = surfaceDisplayMetrics(pointScaleFactor, fontScale);

return TextLayoutManager.measureText(
mReactApplicationContext.getAssets(),
ReactTypefaceUtils.getFontWeightAdjustment(mReactApplicationContext),
attributedString,
paragraphAttributes,
getYogaSize(minWidth, maxWidth),
getYogaSize(minWidth, maxWidth, metrics),
getYogaMeasureMode(minWidth, maxWidth),
getYogaSize(minHeight, maxHeight),
getYogaSize(minHeight, maxHeight, metrics),
getYogaMeasureMode(minHeight, maxHeight),
textViewManager instanceof ReactTextViewManagerCallback
? (ReactTextViewManagerCallback) textViewManager
: null,
attachmentsPositions,
mTextEffectRegistry);
mTextEffectRegistry,
metrics);
}

@AnyThread
Expand All @@ -668,22 +712,26 @@ public PreparedLayout prepareTextLayout(
float minWidth,
float maxWidth,
float minHeight,
float maxHeight) {
float maxHeight,
float pointScaleFactor,
float fontScale) {
ViewManager textViewManager = mViewManagerRegistry.get(ReactTextViewManager.REACT_CLASS);
DisplayMetrics metrics = surfaceDisplayMetrics(pointScaleFactor, fontScale);

return TextLayoutManager.createPreparedLayout(
mReactApplicationContext.getAssets(),
ReactTypefaceUtils.getFontWeightAdjustment(mReactApplicationContext),
attributedString,
paragraphAttributes,
getYogaSize(minWidth, maxWidth),
getYogaSize(minWidth, maxWidth, metrics),
getYogaMeasureMode(minWidth, maxWidth),
getYogaSize(minHeight, maxHeight),
getYogaSize(minHeight, maxHeight, metrics),
getYogaMeasureMode(minHeight, maxHeight),
textViewManager instanceof ReactTextViewManagerCallback
? (ReactTextViewManagerCallback) textViewManager
: null,
mTextEffectRegistry);
mTextEffectRegistry,
metrics);
}

@AnyThread
Expand All @@ -697,7 +745,8 @@ public PreparedLayout reusePreparedLayoutWithNewReactTags(
preparedLayout.getVerticalOffset(),
reactTags,
preparedLayout.getTextBreakStrategy(),
preparedLayout.getJustificationMode());
preparedLayout.getJustificationMode(),
preparedLayout.getDisplayMetrics());
}

@AnyThread
Expand All @@ -709,11 +758,15 @@ public float[] measurePreparedLayout(
float maxWidth,
float minHeight,
float maxHeight) {
// A prepared layout is in physical pixels of the display it was laid out on, so its constraints
// have to be converted with the metrics it was prepared with rather than the primary display's.
DisplayMetrics metrics = preparedLayout.getDisplayMetrics();

return TextLayoutManager.measurePreparedLayout(
preparedLayout,
getYogaSize(minWidth, maxWidth),
getYogaSize(minWidth, maxWidth, metrics),
getYogaMeasureMode(minWidth, maxWidth),
getYogaSize(minHeight, maxHeight),
getYogaSize(minHeight, maxHeight, metrics),
getYogaMeasureMode(minHeight, maxHeight));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package com.facebook.react.fabric.mounting

import android.util.DisplayMetrics
import android.view.View.MeasureSpec
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.facebook.yoga.YogaMeasureMode
Expand Down Expand Up @@ -40,6 +41,20 @@ internal interface LayoutMetricsConversions {
maxSize.dpToPx()
}

/**
* Same as [getYogaSize], but converts using [metrics] rather than the process-wide
* [com.facebook.react.uimanager.DisplayMetricsHolder], which always tracks the primary display.
*/
@JvmStatic
fun getYogaSize(minSize: Float, maxSize: Float, metrics: DisplayMetrics): Float =
if (minSize == maxSize) {
maxSize.dpToPx(metrics)
} else if (maxSize.isInfinite()) {
Float.POSITIVE_INFINITY
} else {
maxSize.dpToPx(metrics)
}

@JvmStatic
fun getYogaMeasureMode(minSize: Float, maxSize: Float): YogaMeasureMode =
if (minSize == maxSize) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<2fc347cdb33327437d29e5fd91e24011>>
* @generated SignedSource<<9d2dd4be9427f8e3bc4ffdcb61f23c76>>
*/

/**
Expand Down Expand Up @@ -276,6 +276,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enableNativeCSSParsing(): Boolean = accessor.enableNativeCSSParsing()

/**
* Measures and mounts text using the density of the display the surface is on, instead of the process-wide DisplayMetricsHolder (which always tracks the primary display).
*/
@JvmStatic
public fun enablePerSurfaceTextScaleAndroid(): Boolean = accessor.enablePerSurfaceTextScaleAndroid()

/**
* Enables caching text layout artifacts for later reuse
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<a506d2515fce404e19bd61099cc60117>>
* @generated SignedSource<<f2921974a2554c78a6a12f833e4cf5cb>>
*/

/**
Expand Down Expand Up @@ -61,6 +61,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var enableMountingCoordinatorPullModelAndroidCache: Boolean? = null
private var enableMutationObserverByDefaultCache: Boolean? = null
private var enableNativeCSSParsingCache: Boolean? = null
private var enablePerSurfaceTextScaleAndroidCache: Boolean? = null
private var enablePreparedTextLayoutCache: Boolean? = null
private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null
private var enableRuntimeSchedulerQueueClearingOnErrorCache: Boolean? = null
Expand Down Expand Up @@ -477,6 +478,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}

override fun enablePerSurfaceTextScaleAndroid(): Boolean {
var cached = enablePerSurfaceTextScaleAndroidCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enablePerSurfaceTextScaleAndroid()
enablePerSurfaceTextScaleAndroidCache = cached
}
return cached
}

override fun enablePreparedTextLayout(): Boolean {
var cached = enablePreparedTextLayoutCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<1ef72233f02973021b83bd2e2aa1f69b>>
* @generated SignedSource<<6f38ac16b33db913b5ffbde151c2fc0c>>
*/

/**
Expand Down Expand Up @@ -110,6 +110,8 @@ public object ReactNativeFeatureFlagsCxxInterop {

@DoNotStrip @JvmStatic public external fun enableNativeCSSParsing(): Boolean

@DoNotStrip @JvmStatic public external fun enablePerSurfaceTextScaleAndroid(): Boolean

@DoNotStrip @JvmStatic public external fun enablePreparedTextLayout(): Boolean

@DoNotStrip @JvmStatic public external fun enablePropsUpdateReconciliationAndroid(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<eb9958ddc04dd1cd8d1add366f5f7741>>
* @generated SignedSource<<53359f6086cfbdae9c9cfec04548b6ff>>
*/

/**
Expand Down Expand Up @@ -105,6 +105,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun enableNativeCSSParsing(): Boolean = false

override fun enablePerSurfaceTextScaleAndroid(): Boolean = false

override fun enablePreparedTextLayout(): Boolean = false

override fun enablePropsUpdateReconciliationAndroid(): Boolean = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<9ae32c46a5a6310ef96eb91c9ea5b12e>>
* @generated SignedSource<<911cf02f383e704acc90b8302041d5a1>>
*/

/**
Expand Down Expand Up @@ -65,6 +65,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var enableMountingCoordinatorPullModelAndroidCache: Boolean? = null
private var enableMutationObserverByDefaultCache: Boolean? = null
private var enableNativeCSSParsingCache: Boolean? = null
private var enablePerSurfaceTextScaleAndroidCache: Boolean? = null
private var enablePreparedTextLayoutCache: Boolean? = null
private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null
private var enableRuntimeSchedulerQueueClearingOnErrorCache: Boolean? = null
Expand Down Expand Up @@ -522,6 +523,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}

override fun enablePerSurfaceTextScaleAndroid(): Boolean {
var cached = enablePerSurfaceTextScaleAndroidCache
if (cached == null) {
cached = currentProvider.enablePerSurfaceTextScaleAndroid()
accessedFeatureFlags.add("enablePerSurfaceTextScaleAndroid")
enablePerSurfaceTextScaleAndroidCache = cached
}
return cached
}

override fun enablePreparedTextLayout(): Boolean {
var cached = enablePreparedTextLayoutCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<1a1d47f2d85404c776e55db40f7dbc6e>>
* @generated SignedSource<<0bffb447de4a420f3df5d2b43d62be21>>
*/

/**
Expand Down Expand Up @@ -105,6 +105,8 @@ public interface ReactNativeFeatureFlagsProvider {

@DoNotStrip public fun enableNativeCSSParsing(): Boolean

@DoNotStrip public fun enablePerSurfaceTextScaleAndroid(): Boolean

@DoNotStrip public fun enablePreparedTextLayout(): Boolean

@DoNotStrip public fun enablePropsUpdateReconciliationAndroid(): Boolean
Expand Down
Loading
Loading