Skip to content

Commit 92b8981

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Mimimize EditText Spans 9/9: Remove addSpansForMeasurement() (#36575)
Summary: Pull Request resolved: #36575 This is part of a series of changes to minimize the number of spans committed to EditText, as a mitigation for platform issues on Samsung devices. See this [GitHub thread]( #35936 (comment)) for greater context on the platform behavior. D23670779 addedd a previous mechanism to add spans for measurement caching, like we needed to do as part of this change. It is called in more specific cases (e.g. when there is a text hint but no text), but it edits the live EditText spannable instead of the cache copy, and does not handle nested text at all. We are already adding spans back to the input after this, behind everything else, and can replace it with the code we have been adding. Changelog: [Android][Fixed] - Mimimize EditText Spans 9/9: Remove `addSpansForMeasurement()` Reviewed By: javache Differential Revision: D44298159 fbshipit-source-id: 1af44a39de7550b7e66e45db9ebc3523ae9ff002
1 parent ef4ae32 commit 92b8981

File tree

3 files changed

+24
-120
lines changed

3 files changed

+24
-120
lines changed

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ public class ReactTextUpdate {
3131
private final int mSelectionEnd;
3232
private final int mJustificationMode;
3333

34-
public boolean mContainsMultipleFragments;
35-
3634
/**
3735
* @deprecated Use a non-deprecated constructor for ReactTextUpdate instead. This one remains
3836
* because it's being used by a unit test that isn't currently open source.
@@ -142,13 +140,11 @@ public static ReactTextUpdate buildReactTextUpdateFromState(
142140
int jsEventCounter,
143141
int textAlign,
144142
int textBreakStrategy,
145-
int justificationMode,
146-
boolean containsMultipleFragments) {
143+
int justificationMode) {
147144

148145
ReactTextUpdate reactTextUpdate =
149146
new ReactTextUpdate(
150147
text, jsEventCounter, false, textAlign, textBreakStrategy, justificationMode);
151-
reactTextUpdate.mContainsMultipleFragments = containsMultipleFragments;
152148
return reactTextUpdate;
153149
}
154150

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java

Lines changed: 21 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@
6464
import com.facebook.react.views.text.TextLayoutManager;
6565
import com.facebook.react.views.view.ReactViewBackgroundManager;
6666
import java.util.ArrayList;
67-
import java.util.List;
6867
import java.util.Objects;
6968

7069
/**
@@ -89,7 +88,6 @@ public class ReactEditText extends AppCompatEditText
8988
// *TextChanged events should be triggered. This is less expensive than removing the text
9089
// listeners and adding them back again after the text change is completed.
9190
protected boolean mIsSettingTextFromJS;
92-
protected boolean mIsSettingTextFromCacheUpdate = false;
9391
private int mDefaultGravityHorizontal;
9492
private int mDefaultGravityVertical;
9593

@@ -369,7 +367,7 @@ protected void onSelectionChanged(int selStart, int selEnd) {
369367
}
370368

371369
super.onSelectionChanged(selStart, selEnd);
372-
if (!mIsSettingTextFromCacheUpdate && mSelectionWatcher != null && hasFocus()) {
370+
if (mSelectionWatcher != null && hasFocus()) {
373371
mSelectionWatcher.onSelectionChanged(selStart, selEnd);
374372
}
375373
}
@@ -610,7 +608,7 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) {
610608
SpannableStringBuilder spannableStringBuilder =
611609
new SpannableStringBuilder(reactTextUpdate.getText());
612610

613-
manageSpans(spannableStringBuilder, reactTextUpdate.mContainsMultipleFragments);
611+
manageSpans(spannableStringBuilder);
614612
stripStyleEquivalentSpans(spannableStringBuilder);
615613

616614
mContainsImages = reactTextUpdate.containsImages();
@@ -639,7 +637,7 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) {
639637
}
640638

641639
// Update cached spans (in Fabric only).
642-
updateCachedSpannable(false);
640+
updateCachedSpannable();
643641
}
644642

645643
/**
@@ -648,8 +646,7 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) {
648646
* will adapt to the new text, hence why {@link SpannableStringBuilder#replace} never removes
649647
* them.
650648
*/
651-
private void manageSpans(
652-
SpannableStringBuilder spannableStringBuilder, boolean skipAddSpansForMeasurements) {
649+
private void manageSpans(SpannableStringBuilder spannableStringBuilder) {
653650
Object[] spans = getText().getSpans(0, length(), Object.class);
654651
for (int spanIdx = 0; spanIdx < spans.length; spanIdx++) {
655652
Object span = spans[spanIdx];
@@ -677,13 +674,6 @@ private void manageSpans(
677674
spannableStringBuilder.setSpan(span, spanStart, spanEnd, spanFlags);
678675
}
679676
}
680-
681-
// In Fabric only, apply necessary styles to entire span
682-
// If the Spannable was constructed from multiple fragments, we don't apply any spans that could
683-
// impact the whole Spannable, because that would override "local" styles per-fragment
684-
if (!skipAddSpansForMeasurements) {
685-
addSpansForMeasurement(getText());
686-
}
687677
}
688678

689679
// TODO: Replace with Predicate<T> and lambdas once Java 8 builds in OSS
@@ -785,10 +775,10 @@ private <T> void stripSpansOfKind(
785775
}
786776

787777
/**
788-
* Copy back styles represented as attributes to the underlying span, for later measurement
789-
* outside the ReactEditText.
778+
* Copy styles represented as attributes to the underlying span, for later measurement or other
779+
* usage outside the ReactEditText.
790780
*/
791-
private void restoreStyleEquivalentSpans(SpannableStringBuilder workingText) {
781+
private void addSpansFromStyleAttributes(SpannableStringBuilder workingText) {
792782
int spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
793783

794784
// Set all bits for SPAN_PRIORITY so that this span has the highest possible priority
@@ -844,6 +834,11 @@ private void restoreStyleEquivalentSpans(SpannableStringBuilder workingText) {
844834
workingText.length(),
845835
spanFlags);
846836
}
837+
838+
float lineHeight = mTextAttributes.getEffectiveLineHeight();
839+
if (!Float.isNaN(lineHeight)) {
840+
workingText.setSpan(new CustomLineHeightSpan(lineHeight), 0, workingText.length(), spanFlags);
841+
}
847842
}
848843

849844
private static boolean sameTextForSpan(
@@ -862,73 +857,6 @@ private static boolean sameTextForSpan(
862857
return true;
863858
}
864859

865-
// This is hacked in for Fabric. When we delete non-Fabric code, we might be able to simplify or
866-
// clean this up a bit.
867-
private void addSpansForMeasurement(Spannable spannable) {
868-
if (!mFabricViewStateManager.hasStateWrapper()) {
869-
return;
870-
}
871-
872-
boolean originalDisableTextDiffing = mDisableTextDiffing;
873-
mDisableTextDiffing = true;
874-
875-
int start = 0;
876-
int end = spannable.length();
877-
878-
// Remove duplicate spans we might add here
879-
Object[] spans = spannable.getSpans(0, length(), Object.class);
880-
for (Object span : spans) {
881-
int spanFlags = spannable.getSpanFlags(span);
882-
boolean isInclusive =
883-
(spanFlags & Spanned.SPAN_INCLUSIVE_INCLUSIVE) == Spanned.SPAN_INCLUSIVE_INCLUSIVE
884-
|| (spanFlags & Spanned.SPAN_INCLUSIVE_EXCLUSIVE) == Spanned.SPAN_INCLUSIVE_EXCLUSIVE;
885-
if (isInclusive
886-
&& span instanceof ReactSpan
887-
&& spannable.getSpanStart(span) == start
888-
&& spannable.getSpanEnd(span) == end) {
889-
spannable.removeSpan(span);
890-
}
891-
}
892-
893-
List<TextLayoutManager.SetSpanOperation> ops = new ArrayList<>();
894-
895-
if (!Float.isNaN(mTextAttributes.getLetterSpacing())) {
896-
ops.add(
897-
new TextLayoutManager.SetSpanOperation(
898-
start, end, new CustomLetterSpacingSpan(mTextAttributes.getLetterSpacing())));
899-
}
900-
ops.add(
901-
new TextLayoutManager.SetSpanOperation(
902-
start, end, new ReactAbsoluteSizeSpan((int) mTextAttributes.getEffectiveFontSize())));
903-
if (mFontStyle != UNSET || mFontWeight != UNSET || mFontFamily != null) {
904-
ops.add(
905-
new TextLayoutManager.SetSpanOperation(
906-
start,
907-
end,
908-
new CustomStyleSpan(
909-
mFontStyle,
910-
mFontWeight,
911-
null, // TODO: do we need to support FontFeatureSettings / fontVariant?
912-
mFontFamily,
913-
getReactContext(ReactEditText.this).getAssets())));
914-
}
915-
if (!Float.isNaN(mTextAttributes.getEffectiveLineHeight())) {
916-
ops.add(
917-
new TextLayoutManager.SetSpanOperation(
918-
start, end, new CustomLineHeightSpan(mTextAttributes.getEffectiveLineHeight())));
919-
}
920-
921-
int priority = 0;
922-
for (TextLayoutManager.SetSpanOperation op : ops) {
923-
// Actual order of calling {@code execute} does NOT matter,
924-
// but the {@code priority} DOES matter.
925-
op.execute(spannable, priority);
926-
priority++;
927-
}
928-
929-
mDisableTextDiffing = originalDisableTextDiffing;
930-
}
931-
932860
protected boolean showSoftKeyboard() {
933861
return mInputMethodManager.showSoftInput(this, 0);
934862
}
@@ -1215,7 +1143,7 @@ public FabricViewStateManager getFabricViewStateManager() {
12151143
* TextLayoutManager.java with some very minor modifications. There's some duplication between
12161144
* here and TextLayoutManager, so there might be an opportunity for refactor.
12171145
*/
1218-
private void updateCachedSpannable(boolean resetStyles) {
1146+
private void updateCachedSpannable() {
12191147
// Noops in non-Fabric
12201148
if (mFabricViewStateManager == null || !mFabricViewStateManager.hasStateWrapper()) {
12211149
return;
@@ -1225,12 +1153,6 @@ private void updateCachedSpannable(boolean resetStyles) {
12251153
return;
12261154
}
12271155

1228-
if (resetStyles) {
1229-
mIsSettingTextFromCacheUpdate = true;
1230-
addSpansForMeasurement(getText());
1231-
mIsSettingTextFromCacheUpdate = false;
1232-
}
1233-
12341156
Editable currentText = getText();
12351157
boolean haveText = currentText != null && currentText.length() > 0;
12361158

@@ -1273,7 +1195,6 @@ private void updateCachedSpannable(boolean resetStyles) {
12731195
// - android.app.Activity.dispatchKeyEvent (Activity.java:3447)
12741196
try {
12751197
sb.append(currentText.subSequence(0, currentText.length()));
1276-
restoreStyleEquivalentSpans(sb);
12771198
} catch (IndexOutOfBoundsException e) {
12781199
ReactSoftExceptionLogger.logSoftException(TAG, e);
12791200
}
@@ -1289,11 +1210,9 @@ private void updateCachedSpannable(boolean resetStyles) {
12891210
// Measure something so we have correct height, even if there's no string.
12901211
sb.append("I");
12911212
}
1292-
1293-
// Make sure that all text styles are applied when we're measurable the hint or "blank" text
1294-
addSpansForMeasurement(sb);
12951213
}
12961214

1215+
addSpansFromStyleAttributes(sb);
12971216
TextLayoutManager.setCachedSpannabledForTag(getId(), sb);
12981217
}
12991218

@@ -1308,7 +1227,7 @@ void setEventDispatcher(@Nullable EventDispatcher eventDispatcher) {
13081227
private class TextWatcherDelegator implements TextWatcher {
13091228
@Override
13101229
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1311-
if (!mIsSettingTextFromCacheUpdate && !mIsSettingTextFromJS && mListeners != null) {
1230+
if (!mIsSettingTextFromJS && mListeners != null) {
13121231
for (TextWatcher listener : mListeners) {
13131232
listener.beforeTextChanged(s, start, count, after);
13141233
}
@@ -1322,23 +1241,20 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {
13221241
TAG, "onTextChanged[" + getId() + "]: " + s + " " + start + " " + before + " " + count);
13231242
}
13241243

1325-
if (!mIsSettingTextFromCacheUpdate) {
1326-
if (!mIsSettingTextFromJS && mListeners != null) {
1327-
for (TextWatcher listener : mListeners) {
1328-
listener.onTextChanged(s, start, before, count);
1329-
}
1244+
if (!mIsSettingTextFromJS && mListeners != null) {
1245+
for (TextWatcher listener : mListeners) {
1246+
listener.onTextChanged(s, start, before, count);
13301247
}
1331-
1332-
updateCachedSpannable(
1333-
!mIsSettingTextFromJS && !mIsSettingTextFromState && start == 0 && before == 0);
13341248
}
13351249

1250+
updateCachedSpannable();
1251+
13361252
onContentSizeChange();
13371253
}
13381254

13391255
@Override
13401256
public void afterTextChanged(Editable s) {
1341-
if (!mIsSettingTextFromCacheUpdate && !mIsSettingTextFromJS && mListeners != null) {
1257+
if (!mIsSettingTextFromJS && mListeners != null) {
13421258
for (TextWatcher listener : mListeners) {
13431259
listener.afterTextChanged(s);
13441260
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,9 +1350,6 @@ public Object updateState(
13501350
TextLayoutManager.getOrCreateSpannableForText(
13511351
view.getContext(), attributedString, mReactTextViewManagerCallback);
13521352

1353-
boolean containsMultipleFragments =
1354-
attributedString.getArray("fragments").toArrayList().size() > 1;
1355-
13561353
int textBreakStrategy =
13571354
TextAttributeProps.getTextBreakStrategy(
13581355
paragraphAttributes.getString(ViewProps.TEXT_BREAK_STRATEGY));
@@ -1365,8 +1362,7 @@ public Object updateState(
13651362
TextAttributeProps.getTextAlignment(
13661363
props, TextLayoutManager.isRTL(attributedString), view.getGravityHorizontal()),
13671364
textBreakStrategy,
1368-
TextAttributeProps.getJustificationMode(props, currentJustificationMode),
1369-
containsMultipleFragments);
1365+
TextAttributeProps.getJustificationMode(props, currentJustificationMode));
13701366
}
13711367

13721368
public Object getReactTextUpdate(ReactEditText view, ReactStylesDiffMap props, MapBuffer state) {
@@ -1387,9 +1383,6 @@ public Object getReactTextUpdate(ReactEditText view, ReactStylesDiffMap props, M
13871383
TextLayoutManagerMapBuffer.getOrCreateSpannableForText(
13881384
view.getContext(), attributedString, mReactTextViewManagerCallback);
13891385

1390-
boolean containsMultipleFragments =
1391-
attributedString.getMapBuffer(TextLayoutManagerMapBuffer.AS_KEY_FRAGMENTS).getCount() > 1;
1392-
13931386
int textBreakStrategy =
13941387
TextAttributeProps.getTextBreakStrategy(
13951388
paragraphAttributes.getString(TextLayoutManagerMapBuffer.PA_KEY_TEXT_BREAK_STRATEGY));
@@ -1402,7 +1395,6 @@ public Object getReactTextUpdate(ReactEditText view, ReactStylesDiffMap props, M
14021395
TextAttributeProps.getTextAlignment(
14031396
props, TextLayoutManagerMapBuffer.isRTL(attributedString), view.getGravityHorizontal()),
14041397
textBreakStrategy,
1405-
TextAttributeProps.getJustificationMode(props, currentJustificationMode),
1406-
containsMultipleFragments);
1398+
TextAttributeProps.getJustificationMode(props, currentJustificationMode));
14071399
}
14081400
}

0 commit comments

Comments
 (0)