Skip to content

Commit f85b30b

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Underpinnings for Caching Text Layouts (#51065)
Summary: Pull Request resolved: #51065 This adds infrastructure to let us start storing cached Android text layouts as part of a `ParagraphShadowNode`. After this, we will clear them out, and propagate them to state. Right now, the flag doesn't do much, apart from extra work. This is done by adding `TextLayoutManagerExtended::supportsPreparedLayout()`, and `TextLayoutManager::PreparedLayout` types, to shim between platforms, then on Android, we add a `PreparedLayout`, which is for now just an Android layout, with extra field (`maxNumberOfLines`, for some reason not exposed on recent versions). Android `TextLayoutManager` java side is split a little bit, so that we reuse all the existing logic for prepared layouts. I tried to set up the boundary, so that we don't reserialize a MapBuffer after preparation, and for simplicity, this means source of truth for attachment count, and attachment sizes, now lives on the layout. This means we need to change boundary a bit, where we are no longer able to pass in a buffer to fill from C++ side of attachment positions. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D73970149 fbshipit-source-id: ff71c227e062c16fe52a4eb3ba2acbebf3d96e56
1 parent 2562440 commit f85b30b

30 files changed

Lines changed: 530 additions & 59 deletions

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2392,12 +2392,14 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid
23922392
public fun initialize ()V
23932393
public fun invalidate ()V
23942394
public fun markActiveTouchForTag (II)V
2395+
public fun measurePreparedLayout (Lcom/facebook/react/views/text/PreparedLayout;FFFF)[F
23952396
public fun onAllAnimationsComplete ()V
23962397
public fun onAnimationStarted ()V
23972398
public fun onHostDestroy ()V
23982399
public fun onHostPause ()V
23992400
public fun onHostResume ()V
24002401
public fun onRequestEventBeat ()V
2402+
public fun prepareLayout (ILcom/facebook/react/common/mapbuffer/ReadableMapBuffer;Lcom/facebook/react/common/mapbuffer/ReadableMapBuffer;FF)Lcom/facebook/react/views/text/PreparedLayout;
24012403
public fun prependUIBlock (Lcom/facebook/react/fabric/interop/UIBlock;)V
24022404
public fun profileNextBatch ()V
24032405
public fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
import android.content.Context;
2323
import android.graphics.Point;
2424
import android.os.SystemClock;
25+
import android.text.Layout;
2526
import android.view.View;
2627
import android.view.accessibility.AccessibilityEvent;
2728
import androidx.annotation.AnyThread;
2829
import androidx.annotation.Nullable;
2930
import androidx.annotation.UiThread;
31+
import androidx.core.util.Preconditions;
3032
import androidx.core.view.ViewCompat.FocusRealDirection;
3133
import com.facebook.common.logging.FLog;
3234
import com.facebook.infer.annotation.Assertions;
@@ -49,6 +51,7 @@
4951
import com.facebook.react.bridge.UIManagerListener;
5052
import com.facebook.react.bridge.UiThreadUtil;
5153
import com.facebook.react.bridge.WritableMap;
54+
import com.facebook.react.common.ReactConstants;
5255
import com.facebook.react.common.annotations.UnstableReactNativeAPI;
5356
import com.facebook.react.common.build.ReactBuildConfig;
5457
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
@@ -86,6 +89,7 @@
8689
import com.facebook.react.uimanager.events.FabricEventDispatcher;
8790
import com.facebook.react.uimanager.events.RCTEventEmitter;
8891
import com.facebook.react.uimanager.events.SynchronousEventReceiver;
92+
import com.facebook.react.views.text.PreparedLayout;
8993
import com.facebook.react.views.text.TextLayoutManager;
9094
import java.util.ArrayList;
9195
import java.util.HashMap;
@@ -648,6 +652,49 @@ private long measureMapBuffer(
648652
attachmentsPositions);
649653
}
650654

655+
@AnyThread
656+
@ThreadConfined(ANY)
657+
public PreparedLayout prepareLayout(
658+
int surfaceId,
659+
ReadableMapBuffer attributedString,
660+
ReadableMapBuffer paragraphAttributes,
661+
float maxWidth,
662+
float maxHeight) {
663+
SurfaceMountingManager surfaceMountingManager =
664+
mMountingManager.getSurfaceManagerEnforced(surfaceId, "prepareLayout");
665+
Layout layout =
666+
TextLayoutManager.createLayout(
667+
Preconditions.checkNotNull(surfaceMountingManager.getContext()),
668+
attributedString,
669+
paragraphAttributes,
670+
PixelUtil.toPixelFromDIP(maxWidth),
671+
PixelUtil.toPixelFromDIP(maxHeight),
672+
null /* T219881133: Migrate away from ReactTextViewManagerCallback */);
673+
674+
int maximumNumberOfLines =
675+
paragraphAttributes.contains(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
676+
? paragraphAttributes.getInt(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
677+
: ReactConstants.UNSET;
678+
679+
return new PreparedLayout(layout, maximumNumberOfLines);
680+
}
681+
682+
@AnyThread
683+
@ThreadConfined(ANY)
684+
public float[] measurePreparedLayout(
685+
PreparedLayout preparedLayout,
686+
float minWidth,
687+
float maxWidth,
688+
float minHeight,
689+
float maxHeight) {
690+
return TextLayoutManager.measurePreparedLayout(
691+
preparedLayout,
692+
getYogaSize(minWidth, maxWidth),
693+
getYogaMeasureMode(minWidth, maxWidth),
694+
getYogaSize(minHeight, maxHeight),
695+
getYogaMeasureMode(minHeight, maxHeight));
696+
}
697+
651698
/**
652699
* @param surfaceId {@link int} surface ID
653700
* @param defaultTextInputPadding {@link float[]} output parameter will contain the default theme

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<31638ef8ac6992a354785b74af8fbe0d>>
7+
* @generated SignedSource<<453f8c0a593b173c197fcf54ed834a1b>>
88
*/
99

1010
/**
@@ -180,6 +180,12 @@ public object ReactNativeFeatureFlags {
180180
@JvmStatic
181181
public fun enableNewBackgroundAndBorderDrawables(): Boolean = accessor.enableNewBackgroundAndBorderDrawables()
182182

183+
/**
184+
* Enables caching text layout artifacts for later reuse
185+
*/
186+
@JvmStatic
187+
public fun enablePreparedTextLayout(): Boolean = accessor.enablePreparedTextLayout()
188+
183189
/**
184190
* When enabled, Android will receive prop updates based on the differences between the last rendered shadow node and the last committed shadow node.
185191
*/

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<6b382661025db56592b44255f5a8694c>>
7+
* @generated SignedSource<<a51441451ec25033040ba044ee3371fc>>
88
*/
99

1010
/**
@@ -45,6 +45,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
4545
private var enableNativeCSSParsingCache: Boolean? = null
4646
private var enableNetworkEventReportingCache: Boolean? = null
4747
private var enableNewBackgroundAndBorderDrawablesCache: Boolean? = null
48+
private var enablePreparedTextLayoutCache: Boolean? = null
4849
private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null
4950
private var enableResourceTimingAPICache: Boolean? = null
5051
private var enableSynchronousStateUpdatesCache: Boolean? = null
@@ -292,6 +293,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
292293
return cached
293294
}
294295

296+
override fun enablePreparedTextLayout(): Boolean {
297+
var cached = enablePreparedTextLayoutCache
298+
if (cached == null) {
299+
cached = ReactNativeFeatureFlagsCxxInterop.enablePreparedTextLayout()
300+
enablePreparedTextLayoutCache = cached
301+
}
302+
return cached
303+
}
304+
295305
override fun enablePropsUpdateReconciliationAndroid(): Boolean {
296306
var cached = enablePropsUpdateReconciliationAndroidCache
297307
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<8276fd1166cdd235f11a0b490bb7d924>>
7+
* @generated SignedSource<<9d0b02395a08331bca956ea600602a31>>
88
*/
99

1010
/**
@@ -78,6 +78,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
7878

7979
@DoNotStrip @JvmStatic public external fun enableNewBackgroundAndBorderDrawables(): Boolean
8080

81+
@DoNotStrip @JvmStatic public external fun enablePreparedTextLayout(): Boolean
82+
8183
@DoNotStrip @JvmStatic public external fun enablePropsUpdateReconciliationAndroid(): Boolean
8284

8385
@DoNotStrip @JvmStatic public external fun enableResourceTimingAPI(): Boolean

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<ffadd7912aed2d95b0c6199aa8902690>>
7+
* @generated SignedSource<<cf12cdfdfb343e79247379b5549ae92a>>
88
*/
99

1010
/**
@@ -73,6 +73,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
7373

7474
override fun enableNewBackgroundAndBorderDrawables(): Boolean = true
7575

76+
override fun enablePreparedTextLayout(): Boolean = false
77+
7678
override fun enablePropsUpdateReconciliationAndroid(): Boolean = false
7779

7880
override fun enableResourceTimingAPI(): Boolean = false

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<5dc41059d71d3a345be45a6a233b05a0>>
7+
* @generated SignedSource<<4c81ed8a06c192eb4007219d163650e5>>
88
*/
99

1010
/**
@@ -49,6 +49,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
4949
private var enableNativeCSSParsingCache: Boolean? = null
5050
private var enableNetworkEventReportingCache: Boolean? = null
5151
private var enableNewBackgroundAndBorderDrawablesCache: Boolean? = null
52+
private var enablePreparedTextLayoutCache: Boolean? = null
5253
private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null
5354
private var enableResourceTimingAPICache: Boolean? = null
5455
private var enableSynchronousStateUpdatesCache: Boolean? = null
@@ -321,6 +322,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
321322
return cached
322323
}
323324

325+
override fun enablePreparedTextLayout(): Boolean {
326+
var cached = enablePreparedTextLayoutCache
327+
if (cached == null) {
328+
cached = currentProvider.enablePreparedTextLayout()
329+
accessedFeatureFlags.add("enablePreparedTextLayout")
330+
enablePreparedTextLayoutCache = cached
331+
}
332+
return cached
333+
}
334+
324335
override fun enablePropsUpdateReconciliationAndroid(): Boolean {
325336
var cached = enablePropsUpdateReconciliationAndroidCache
326337
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<35f6d9ae3c445c81fc4bab7509fd3179>>
7+
* @generated SignedSource<<2482f57e0652cfaa4806b5333c50ad9f>>
88
*/
99

1010
/**
@@ -73,6 +73,8 @@ public interface ReactNativeFeatureFlagsProvider {
7373

7474
@DoNotStrip public fun enableNewBackgroundAndBorderDrawables(): Boolean
7575

76+
@DoNotStrip public fun enablePreparedTextLayout(): Boolean
77+
7678
@DoNotStrip public fun enablePropsUpdateReconciliationAndroid(): Boolean
7779

7880
@DoNotStrip public fun enableResourceTimingAPI(): Boolean
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.text
9+
10+
import android.text.Layout
11+
import com.facebook.proguard.annotations.DoNotStrip
12+
13+
/**
14+
* Encapsulates an {android.text.Layout} along with any additional state needed to render or measure
15+
* it.
16+
*/
17+
@DoNotStrip
18+
internal class PreparedLayout(public val layout: Layout, public val maximumNumberOfLines: Int)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.facebook.react.bridge.ReactSoftExceptionLogger;
3232
import com.facebook.react.bridge.WritableArray;
3333
import com.facebook.react.common.ReactConstants;
34+
import com.facebook.react.common.annotations.UnstableReactNativeAPI;
3435
import com.facebook.react.common.mapbuffer.MapBuffer;
3536
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
3637
import com.facebook.react.uimanager.PixelUtil;
@@ -503,7 +504,8 @@ private static void updateTextPaint(
503504
}
504505
}
505506

506-
private static Layout createLayout(
507+
@UnstableReactNativeAPI
508+
public static Layout createLayout(
507509
@NonNull Context context,
508510
MapBuffer attributedString,
509511
MapBuffer paragraphAttributes,
@@ -732,6 +734,47 @@ public static long measureText(
732734
return YogaMeasureOutput.make(widthInSP, heightInSP);
733735
}
734736

737+
@UnstableReactNativeAPI
738+
public static float[] measurePreparedLayout(
739+
PreparedLayout preparedLayout,
740+
float width,
741+
YogaMeasureMode widthYogaMeasureMode,
742+
float height,
743+
YogaMeasureMode heightYogaMeasureMode) {
744+
Layout layout = preparedLayout.getLayout();
745+
Spanned text = (Spanned) layout.getText();
746+
int maximumNumberOfLines = preparedLayout.getMaximumNumberOfLines();
747+
748+
int calculatedLineCount = calculateLineCount(layout, maximumNumberOfLines);
749+
float calculatedWidth =
750+
calculateWidth(layout, text, width, widthYogaMeasureMode, calculatedLineCount);
751+
float calculatedHeight =
752+
calculateHeight(layout, text, height, heightYogaMeasureMode, calculatedLineCount);
753+
754+
ArrayList<Float> retList = new ArrayList<>();
755+
retList.add(PixelUtil.toDIPFromPixel(calculatedWidth));
756+
retList.add(PixelUtil.toDIPFromPixel(calculatedHeight));
757+
758+
AttachmentMetrics metrics = new AttachmentMetrics();
759+
int lastAttachmentFoundInSpan;
760+
for (int i = 0; i < text.length(); i = lastAttachmentFoundInSpan) {
761+
lastAttachmentFoundInSpan =
762+
nextAttachmentMetrics(layout, text, calculatedWidth, calculatedLineCount, i, metrics);
763+
if (metrics.wasFound) {
764+
retList.add(PixelUtil.toDIPFromPixel(metrics.top));
765+
retList.add(PixelUtil.toDIPFromPixel(metrics.left));
766+
retList.add(PixelUtil.toDIPFromPixel(metrics.width));
767+
retList.add(PixelUtil.toDIPFromPixel(metrics.height));
768+
}
769+
}
770+
771+
float[] ret = new float[retList.size()];
772+
for (int i = 0; i < retList.size(); i++) {
773+
ret[i] = retList.get(i);
774+
}
775+
return ret;
776+
}
777+
735778
private static int calculateLineCount(Layout layout, int maximumNumberOfLines) {
736779
return maximumNumberOfLines == ReactConstants.UNSET || maximumNumberOfLines == 0
737780
? layout.getLineCount()

0 commit comments

Comments
 (0)