Skip to content

Commit 88a59e7

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Mimimize EditText Spans 9/9: Remove addSpansForMeasurement() (facebook#36575)
Summary: Pull Request resolved: facebook#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]( facebook#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 (when there is no text, a hint, or some other case I don't fully understand), edits the live EditText spannable, 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()` Differential Revision: D44298159 fbshipit-source-id: cfabdf4885eec9a3e4b28c2e66ad4878fc5044ce
1 parent 485d5e1 commit 88a59e7

File tree

2 files changed

+21
-108
lines changed

2 files changed

+21
-108
lines changed

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

Lines changed: 0 additions & 3 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.
@@ -148,7 +146,6 @@ public static ReactTextUpdate buildReactTextUpdateFromState(
148146
ReactTextUpdate reactTextUpdate =
149147
new ReactTextUpdate(
150148
text, jsEventCounter, false, textAlign, textBreakStrategy, justificationMode);
151-
reactTextUpdate.mContainsMultipleFragments = containsMultipleFragments;
152149
return reactTextUpdate;
153150
}
154151

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

6968
/**
7069
* A wrapper around the EditText that lets us better control what happens when an EditText gets
@@ -88,7 +87,6 @@ public class ReactEditText extends AppCompatEditText
8887
// *TextChanged events should be triggered. This is less expensive than removing the text
8988
// listeners and adding them back again after the text change is completed.
9089
protected boolean mIsSettingTextFromJS;
91-
protected boolean mIsSettingTextFromCacheUpdate = false;
9290
private int mDefaultGravityHorizontal;
9391
private int mDefaultGravityVertical;
9492

@@ -368,7 +366,7 @@ protected void onSelectionChanged(int selStart, int selEnd) {
368366
}
369367

370368
super.onSelectionChanged(selStart, selEnd);
371-
if (!mIsSettingTextFromCacheUpdate && mSelectionWatcher != null && hasFocus()) {
369+
if (mSelectionWatcher != null && hasFocus()) {
372370
mSelectionWatcher.onSelectionChanged(selStart, selEnd);
373371
}
374372
}
@@ -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
@@ -800,10 +790,10 @@ private <T> void stripSpansOfKind(
800790
}
801791

802792
/**
803-
* Copy back styles represented as attributes to the underlying span, for later measurement
804-
* outside the ReactEditText.
793+
* Copy styles represented as attributes to the underlying span, for later measurement or other
794+
* usage outside the ReactEditText.
805795
*/
806-
private void restoreStyleEquivalentSpans(SpannableStringBuilder workingText) {
796+
private void addSpansFromStyleAttributes(SpannableStringBuilder workingText) {
807797
int spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
808798

809799
// Set all bits for SPAN_PRIORITY so that this span has the highest possible priority
@@ -859,6 +849,11 @@ private void restoreStyleEquivalentSpans(SpannableStringBuilder workingText) {
859849
workingText.length(),
860850
spanFlags);
861851
}
852+
853+
float lineHeight = mTextAttributes.getEffectiveLineHeight();
854+
if (!Float.isNaN(lineHeight)) {
855+
workingText.setSpan(new CustomLineHeightSpan(lineHeight), 0, workingText.length(), spanFlags);
856+
}
862857
}
863858

864859
private static boolean sameTextForSpan(
@@ -877,73 +872,6 @@ private static boolean sameTextForSpan(
877872
return true;
878873
}
879874

880-
// This is hacked in for Fabric. When we delete non-Fabric code, we might be able to simplify or
881-
// clean this up a bit.
882-
private void addSpansForMeasurement(Spannable spannable) {
883-
if (!mFabricViewStateManager.hasStateWrapper()) {
884-
return;
885-
}
886-
887-
boolean originalDisableTextDiffing = mDisableTextDiffing;
888-
mDisableTextDiffing = true;
889-
890-
int start = 0;
891-
int end = spannable.length();
892-
893-
// Remove duplicate spans we might add here
894-
Object[] spans = spannable.getSpans(0, length(), Object.class);
895-
for (Object span : spans) {
896-
int spanFlags = spannable.getSpanFlags(span);
897-
boolean isInclusive =
898-
(spanFlags & Spanned.SPAN_INCLUSIVE_INCLUSIVE) == Spanned.SPAN_INCLUSIVE_INCLUSIVE
899-
|| (spanFlags & Spanned.SPAN_INCLUSIVE_EXCLUSIVE) == Spanned.SPAN_INCLUSIVE_EXCLUSIVE;
900-
if (isInclusive
901-
&& span instanceof ReactSpan
902-
&& spannable.getSpanStart(span) == start
903-
&& spannable.getSpanEnd(span) == end) {
904-
spannable.removeSpan(span);
905-
}
906-
}
907-
908-
List<TextLayoutManager.SetSpanOperation> ops = new ArrayList<>();
909-
910-
if (!Float.isNaN(mTextAttributes.getLetterSpacing())) {
911-
ops.add(
912-
new TextLayoutManager.SetSpanOperation(
913-
start, end, new CustomLetterSpacingSpan(mTextAttributes.getLetterSpacing())));
914-
}
915-
ops.add(
916-
new TextLayoutManager.SetSpanOperation(
917-
start, end, new ReactAbsoluteSizeSpan((int) mTextAttributes.getEffectiveFontSize())));
918-
if (mFontStyle != UNSET || mFontWeight != UNSET || mFontFamily != null) {
919-
ops.add(
920-
new TextLayoutManager.SetSpanOperation(
921-
start,
922-
end,
923-
new CustomStyleSpan(
924-
mFontStyle,
925-
mFontWeight,
926-
null, // TODO: do we need to support FontFeatureSettings / fontVariant?
927-
mFontFamily,
928-
getReactContext(ReactEditText.this).getAssets())));
929-
}
930-
if (!Float.isNaN(mTextAttributes.getEffectiveLineHeight())) {
931-
ops.add(
932-
new TextLayoutManager.SetSpanOperation(
933-
start, end, new CustomLineHeightSpan(mTextAttributes.getEffectiveLineHeight())));
934-
}
935-
936-
int priority = 0;
937-
for (TextLayoutManager.SetSpanOperation op : ops) {
938-
// Actual order of calling {@code execute} does NOT matter,
939-
// but the {@code priority} DOES matter.
940-
op.execute(spannable, priority);
941-
priority++;
942-
}
943-
944-
mDisableTextDiffing = originalDisableTextDiffing;
945-
}
946-
947875
protected boolean showSoftKeyboard() {
948876
return mInputMethodManager.showSoftInput(this, 0);
949877
}
@@ -1230,7 +1158,7 @@ public FabricViewStateManager getFabricViewStateManager() {
12301158
* TextLayoutManager.java with some very minor modifications. There's some duplication between
12311159
* here and TextLayoutManager, so there might be an opportunity for refactor.
12321160
*/
1233-
private void updateCachedSpannable(boolean resetStyles) {
1161+
private void updateCachedSpannable() {
12341162
// Noops in non-Fabric
12351163
if (mFabricViewStateManager == null || !mFabricViewStateManager.hasStateWrapper()) {
12361164
return;
@@ -1240,12 +1168,6 @@ private void updateCachedSpannable(boolean resetStyles) {
12401168
return;
12411169
}
12421170

1243-
if (resetStyles) {
1244-
mIsSettingTextFromCacheUpdate = true;
1245-
addSpansForMeasurement(getText());
1246-
mIsSettingTextFromCacheUpdate = false;
1247-
}
1248-
12491171
Editable currentText = getText();
12501172
boolean haveText = currentText != null && currentText.length() > 0;
12511173

@@ -1288,7 +1210,6 @@ private void updateCachedSpannable(boolean resetStyles) {
12881210
// - android.app.Activity.dispatchKeyEvent (Activity.java:3447)
12891211
try {
12901212
sb.append(currentText.subSequence(0, currentText.length()));
1291-
restoreStyleEquivalentSpans(sb);
12921213
} catch (IndexOutOfBoundsException e) {
12931214
ReactSoftExceptionLogger.logSoftException(TAG, e);
12941215
}
@@ -1304,11 +1225,9 @@ private void updateCachedSpannable(boolean resetStyles) {
13041225
// Measure something so we have correct height, even if there's no string.
13051226
sb.append("I");
13061227
}
1307-
1308-
// Make sure that all text styles are applied when we're measurable the hint or "blank" text
1309-
addSpansForMeasurement(sb);
13101228
}
13111229

1230+
addSpansFromStyleAttributes(sb);
13121231
TextLayoutManager.setCachedSpannabledForTag(getId(), sb);
13131232
}
13141233

@@ -1323,7 +1242,7 @@ void setEventDispatcher(@Nullable EventDispatcher eventDispatcher) {
13231242
private class TextWatcherDelegator implements TextWatcher {
13241243
@Override
13251244
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1326-
if (!mIsSettingTextFromCacheUpdate && !mIsSettingTextFromJS && mListeners != null) {
1245+
if (!mIsSettingTextFromJS && mListeners != null) {
13271246
for (TextWatcher listener : mListeners) {
13281247
listener.beforeTextChanged(s, start, count, after);
13291248
}
@@ -1337,23 +1256,20 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {
13371256
TAG, "onTextChanged[" + getId() + "]: " + s + " " + start + " " + before + " " + count);
13381257
}
13391258

1340-
if (!mIsSettingTextFromCacheUpdate) {
1341-
if (!mIsSettingTextFromJS && mListeners != null) {
1342-
for (TextWatcher listener : mListeners) {
1343-
listener.onTextChanged(s, start, before, count);
1344-
}
1259+
if (!mIsSettingTextFromJS && mListeners != null) {
1260+
for (TextWatcher listener : mListeners) {
1261+
listener.onTextChanged(s, start, before, count);
13451262
}
1346-
1347-
updateCachedSpannable(
1348-
!mIsSettingTextFromJS && !mIsSettingTextFromState && start == 0 && before == 0);
13491263
}
13501264

1265+
updateCachedSpannable();
1266+
13511267
onContentSizeChange();
13521268
}
13531269

13541270
@Override
13551271
public void afterTextChanged(Editable s) {
1356-
if (!mIsSettingTextFromCacheUpdate && !mIsSettingTextFromJS && mListeners != null) {
1272+
if (!mIsSettingTextFromJS && mListeners != null) {
13571273
for (TextWatcher listener : mListeners) {
13581274
listener.afterTextChanged(s);
13591275
}

0 commit comments

Comments
 (0)