Skip to content

Commit 746275f

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Incorporate maxLines and ellipsization into text layout (#51007)
Summary: Pull Request resolved: #51007 Right now, we fully layout text, then use max lines to determine a metric to use when calculating size. Android API 23+ which we fully target allows incorporating ellipsization and maxlines directly into the layout. This will let us directly draw the layout when using maxLines later. This may also let Android optimize line-breaking a bit, when we hit truncation. Special care is taken not to set this when we are in `adjustsFontSizeToFit` path, so that line count will flow over, signifing overflow. I think the main user-facing change is that `onTextLayout` events will have measures post-ellipsization. Changelog: [Android][Changed] - Incorporate maxLines and ellipsization into text layout Reviewed By: joevilches Differential Revision: D73811573 fbshipit-source-id: df83295d0902ae8b043ce57b06cbb9c8f0c194fc
1 parent 6f0a0a5 commit 746275f

23 files changed

+197
-32
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6533,6 +6533,7 @@ public class com/facebook/react/views/text/TextAttributeProps {
65336533
public fun getEffectiveFontSize ()I
65346534
public fun getEffectiveLetterSpacing ()F
65356535
public fun getEffectiveLineHeight ()F
6536+
public static fun getEllipsizeMode (Ljava/lang/String;)Landroid/text/TextUtils$TruncateAt;
65366537
public fun getFontFamily ()Ljava/lang/String;
65376538
public fun getFontFeatureSettings ()Ljava/lang/String;
65386539
public fun getFontStyle ()I

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<<519e6095d6c91fbbb4225a066c9cbe26>>
7+
* @generated SignedSource<<1172f5f2fda3a7d7835f619376f65b3a>>
88
*/
99

1010
/**
@@ -222,6 +222,12 @@ public object ReactNativeFeatureFlags {
222222
@JvmStatic
223223
public fun fuseboxNetworkInspectionEnabled(): Boolean = accessor.fuseboxNetworkInspectionEnabled()
224224

225+
/**
226+
* Set maxLines and ellipsization during Android layout creation
227+
*/
228+
@JvmStatic
229+
public fun incorporateMaxLinesDuringAndroidLayout(): Boolean = accessor.incorporateMaxLinesDuringAndroidLayout()
230+
225231
/**
226232
* Enables storing js caller stack when creating promise in native module. This is useful in case of Promise rejection and tracing the cause.
227233
*/

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<<a70b3ffda510a41144b061036d4f1d48>>
7+
* @generated SignedSource<<b045498e0c54e6c12cdf6a6dc25fd109>>
88
*/
99

1010
/**
@@ -52,6 +52,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
5252
private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null
5353
private var fuseboxEnabledReleaseCache: Boolean? = null
5454
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
55+
private var incorporateMaxLinesDuringAndroidLayoutCache: Boolean? = null
5556
private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null
5657
private var updateRuntimeShadowNodeReferencesOnCommitCache: Boolean? = null
5758
private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null
@@ -352,6 +353,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
352353
return cached
353354
}
354355

356+
override fun incorporateMaxLinesDuringAndroidLayout(): Boolean {
357+
var cached = incorporateMaxLinesDuringAndroidLayoutCache
358+
if (cached == null) {
359+
cached = ReactNativeFeatureFlagsCxxInterop.incorporateMaxLinesDuringAndroidLayout()
360+
incorporateMaxLinesDuringAndroidLayoutCache = cached
361+
}
362+
return cached
363+
}
364+
355365
override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean {
356366
var cached = traceTurboModulePromiseRejectionsOnAndroidCache
357367
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<<6107b2e4611a6cd6ad67420eee468601>>
7+
* @generated SignedSource<<007721988dfdf572f8ac218744341e60>>
88
*/
99

1010
/**
@@ -92,6 +92,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
9292

9393
@DoNotStrip @JvmStatic public external fun fuseboxNetworkInspectionEnabled(): Boolean
9494

95+
@DoNotStrip @JvmStatic public external fun incorporateMaxLinesDuringAndroidLayout(): Boolean
96+
9597
@DoNotStrip @JvmStatic public external fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean
9698

9799
@DoNotStrip @JvmStatic public external fun updateRuntimeShadowNodeReferencesOnCommit(): 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<<ad976c46e40f6d8a945041388865ec60>>
7+
* @generated SignedSource<<6b10c147b3f1753448f599ae4fb43387>>
88
*/
99

1010
/**
@@ -87,6 +87,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
8787

8888
override fun fuseboxNetworkInspectionEnabled(): Boolean = false
8989

90+
override fun incorporateMaxLinesDuringAndroidLayout(): Boolean = true
91+
9092
override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean = false
9193

9294
override fun updateRuntimeShadowNodeReferencesOnCommit(): 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<<580a0bc36f98e9680387089a9740bba8>>
7+
* @generated SignedSource<<b64966fc245f53645726747ec93a15d1>>
88
*/
99

1010
/**
@@ -56,6 +56,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
5656
private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null
5757
private var fuseboxEnabledReleaseCache: Boolean? = null
5858
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
59+
private var incorporateMaxLinesDuringAndroidLayoutCache: Boolean? = null
5960
private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null
6061
private var updateRuntimeShadowNodeReferencesOnCommitCache: Boolean? = null
6162
private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null
@@ -388,6 +389,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
388389
return cached
389390
}
390391

392+
override fun incorporateMaxLinesDuringAndroidLayout(): Boolean {
393+
var cached = incorporateMaxLinesDuringAndroidLayoutCache
394+
if (cached == null) {
395+
cached = currentProvider.incorporateMaxLinesDuringAndroidLayout()
396+
accessedFeatureFlags.add("incorporateMaxLinesDuringAndroidLayout")
397+
incorporateMaxLinesDuringAndroidLayoutCache = cached
398+
}
399+
return cached
400+
}
401+
391402
override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean {
392403
var cached = traceTurboModulePromiseRejectionsOnAndroidCache
393404
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<<b4a944a07650cf46f90d86419b11bbee>>
7+
* @generated SignedSource<<1e9beb93c98a0a82d030b7a3ea1cf7e4>>
88
*/
99

1010
/**
@@ -87,6 +87,8 @@ public interface ReactNativeFeatureFlagsProvider {
8787

8888
@DoNotStrip public fun fuseboxNetworkInspectionEnabled(): Boolean
8989

90+
@DoNotStrip public fun incorporateMaxLinesDuringAndroidLayout(): Boolean
91+
9092
@DoNotStrip public fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean
9193

9294
@DoNotStrip public fun updateRuntimeShadowNodeReferencesOnCommit(): Boolean

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,4 +788,24 @@ public static int getHyphenationFrequency(@Nullable String hyphenationFrequency)
788788
}
789789
return androidHyphenationFrequency;
790790
}
791+
792+
public static @Nullable TextUtils.TruncateAt getEllipsizeMode(@Nullable String ellipsizeMode) {
793+
@Nullable TextUtils.TruncateAt truncateAt = TextUtils.TruncateAt.START;
794+
if (ellipsizeMode != null) {
795+
switch (ellipsizeMode) {
796+
case "head":
797+
truncateAt = TextUtils.TruncateAt.START;
798+
break;
799+
case "middle":
800+
truncateAt = TextUtils.TruncateAt.MIDDLE;
801+
break;
802+
case "tail":
803+
truncateAt = TextUtils.TruncateAt.END;
804+
break;
805+
case "clip":
806+
truncateAt = null;
807+
}
808+
}
809+
return truncateAt;
810+
}
791811
}

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import android.text.StaticLayout;
2020
import android.text.TextDirectionHeuristics;
2121
import android.text.TextPaint;
22+
import android.text.TextUtils;
2223
import android.util.LayoutDirection;
2324
import android.view.Gravity;
2425
import android.view.View;
@@ -32,6 +33,7 @@
3233
import com.facebook.react.common.ReactConstants;
3334
import com.facebook.react.common.build.ReactBuildConfig;
3435
import com.facebook.react.common.mapbuffer.MapBuffer;
36+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
3537
import com.facebook.react.uimanager.PixelUtil;
3638
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
3739
import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role;
@@ -385,6 +387,8 @@ private static Layout createLayout(
385387
int hyphenationFrequency,
386388
Layout.Alignment alignment,
387389
int justificationMode,
390+
@Nullable TextUtils.TruncateAt ellipsizeMode,
391+
int maxNumberOfLines,
388392
TextPaint paint) {
389393
Layout layout;
390394

@@ -414,6 +418,11 @@ private static Layout createLayout(
414418
.setTextDirection(
415419
isScriptRTL ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR);
416420

421+
if (ReactNativeFeatureFlags.incorporateMaxLinesDuringAndroidLayout()
422+
&& maxNumberOfLines != ReactConstants.UNSET) {
423+
builder.setEllipsize(ellipsizeMode).setMaxLines(maxNumberOfLines);
424+
}
425+
417426
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
418427
builder.setUseLineSpacingFromFallbacks(true);
419428
}
@@ -447,6 +456,11 @@ private static Layout createLayout(
447456
.setTextDirection(
448457
isScriptRTL ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR);
449458

459+
if (ReactNativeFeatureFlags.incorporateMaxLinesDuringAndroidLayout()
460+
&& maxNumberOfLines != ReactConstants.UNSET) {
461+
builder.setEllipsize(ellipsizeMode).setMaxLines(maxNumberOfLines);
462+
}
463+
450464
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
451465
builder.setJustificationMode(justificationMode);
452466
}
@@ -536,6 +550,12 @@ private static Layout createLayout(
536550
paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES)
537551
? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
538552
: ReactConstants.UNSET;
553+
@Nullable
554+
TextUtils.TruncateAt ellipsizeMode =
555+
paragraphAttributes.contains(PA_KEY_ELLIPSIZE_MODE)
556+
? TextAttributeProps.getEllipsizeMode(
557+
paragraphAttributes.getString(PA_KEY_ELLIPSIZE_MODE))
558+
: null;
539559

540560
@Nullable String alignmentAttr = getTextAlignmentAttr(attributedString);
541561
Layout.Alignment alignment = getTextAlignment(attributedString, text, alignmentAttr);
@@ -573,6 +593,8 @@ private static Layout createLayout(
573593
hyphenationFrequency,
574594
alignment,
575595
justificationMode,
596+
ellipsizeMode,
597+
maximumNumberOfLines,
576598
paint);
577599
}
578600

@@ -602,6 +624,8 @@ private static Layout createLayout(
602624
hyphenationFrequency,
603625
alignment,
604626
justificationMode,
627+
null,
628+
ReactConstants.UNSET,
605629
paint);
606630

607631
// Minimum font size is 4pts to match the iOS implementation.
@@ -654,6 +678,8 @@ private static Layout createLayout(
654678
hyphenationFrequency,
655679
alignment,
656680
justificationMode,
681+
null,
682+
ReactConstants.UNSET,
657683
paint);
658684
}
659685
}
@@ -677,7 +703,7 @@ public static long measureText(
677703
width,
678704
height,
679705
reactTextViewManagerCallback);
680-
Spannable text = (Spannable) layout.getText();
706+
Spanned text = (Spanned) layout.getText();
681707

682708
if (text == null) {
683709
return 0;
@@ -726,6 +752,8 @@ public static long measureText(
726752

727753
float calculatedHeight = height;
728754
if (heightYogaMeasureMode != YogaMeasureMode.EXACTLY) {
755+
// StaticLayout only seems to change its height in response to maxLines when ellipsizing, so
756+
// we must truncate
729757
calculatedHeight = layout.getLineBottom(calculatedLineCount - 1);
730758
if (heightYogaMeasureMode == YogaMeasureMode.AT_MOST && calculatedHeight > height) {
731759
calculatedHeight = height;

packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp

Lines changed: 15 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<<b07fb300852699f3066090549af0b994>>
7+
* @generated SignedSource<<ac6b60d9d2062eda500e291fb2167076>>
88
*/
99

1010
/**
@@ -231,6 +231,12 @@ class ReactNativeFeatureFlagsJavaProvider
231231
return method(javaProvider_);
232232
}
233233

234+
bool incorporateMaxLinesDuringAndroidLayout() override {
235+
static const auto method =
236+
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("incorporateMaxLinesDuringAndroidLayout");
237+
return method(javaProvider_);
238+
}
239+
234240
bool traceTurboModulePromiseRejectionsOnAndroid() override {
235241
static const auto method =
236242
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("traceTurboModulePromiseRejectionsOnAndroid");
@@ -461,6 +467,11 @@ bool JReactNativeFeatureFlagsCxxInterop::fuseboxNetworkInspectionEnabled(
461467
return ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled();
462468
}
463469

470+
bool JReactNativeFeatureFlagsCxxInterop::incorporateMaxLinesDuringAndroidLayout(
471+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
472+
return ReactNativeFeatureFlags::incorporateMaxLinesDuringAndroidLayout();
473+
}
474+
464475
bool JReactNativeFeatureFlagsCxxInterop::traceTurboModulePromiseRejectionsOnAndroid(
465476
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
466477
return ReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid();
@@ -643,6 +654,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
643654
makeNativeMethod(
644655
"fuseboxNetworkInspectionEnabled",
645656
JReactNativeFeatureFlagsCxxInterop::fuseboxNetworkInspectionEnabled),
657+
makeNativeMethod(
658+
"incorporateMaxLinesDuringAndroidLayout",
659+
JReactNativeFeatureFlagsCxxInterop::incorporateMaxLinesDuringAndroidLayout),
646660
makeNativeMethod(
647661
"traceTurboModulePromiseRejectionsOnAndroid",
648662
JReactNativeFeatureFlagsCxxInterop::traceTurboModulePromiseRejectionsOnAndroid),

0 commit comments

Comments
 (0)