Skip to content

Commit 181bd38

Browse files
NickGerlemankelset
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 # Conflicts: # ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
1 parent 77bd902 commit 181bd38

File tree

3 files changed

+24
-120
lines changed

3 files changed

+24
-120
lines changed

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

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
}
@@ -1210,7 +1138,7 @@ public FabricViewStateManager getFabricViewStateManager() {
12101138
* TextLayoutManager.java with some very minor modifications. There's some duplication between
12111139
* here and TextLayoutManager, so there might be an opportunity for refactor.
12121140
*/
1213-
private void updateCachedSpannable(boolean resetStyles) {
1141+
private void updateCachedSpannable() {
12141142
// Noops in non-Fabric
12151143
if (mFabricViewStateManager == null || !mFabricViewStateManager.hasStateWrapper()) {
12161144
return;
@@ -1220,12 +1148,6 @@ private void updateCachedSpannable(boolean resetStyles) {
12201148
return;
12211149
}
12221150

1223-
if (resetStyles) {
1224-
mIsSettingTextFromCacheUpdate = true;
1225-
addSpansForMeasurement(getText());
1226-
mIsSettingTextFromCacheUpdate = false;
1227-
}
1228-
12291151
Editable currentText = getText();
12301152
boolean haveText = currentText != null && currentText.length() > 0;
12311153

@@ -1268,7 +1190,6 @@ private void updateCachedSpannable(boolean resetStyles) {
12681190
// - android.app.Activity.dispatchKeyEvent (Activity.java:3447)
12691191
try {
12701192
sb.append(currentText.subSequence(0, currentText.length()));
1271-
restoreStyleEquivalentSpans(sb);
12721193
} catch (IndexOutOfBoundsException e) {
12731194
ReactSoftExceptionLogger.logSoftException(TAG, e);
12741195
}
@@ -1284,11 +1205,9 @@ private void updateCachedSpannable(boolean resetStyles) {
12841205
// Measure something so we have correct height, even if there's no string.
12851206
sb.append("I");
12861207
}
1287-
1288-
// Make sure that all text styles are applied when we're measurable the hint or "blank" text
1289-
addSpansForMeasurement(sb);
12901208
}
12911209

1210+
addSpansFromStyleAttributes(sb);
12921211
TextLayoutManager.setCachedSpannabledForTag(getId(), sb);
12931212
}
12941213

@@ -1303,7 +1222,7 @@ void setEventDispatcher(@Nullable EventDispatcher eventDispatcher) {
13031222
private class TextWatcherDelegator implements TextWatcher {
13041223
@Override
13051224
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1306-
if (!mIsSettingTextFromCacheUpdate && !mIsSettingTextFromJS && mListeners != null) {
1225+
if (!mIsSettingTextFromJS && mListeners != null) {
13071226
for (TextWatcher listener : mListeners) {
13081227
listener.beforeTextChanged(s, start, count, after);
13091228
}
@@ -1317,23 +1236,20 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {
13171236
TAG, "onTextChanged[" + getId() + "]: " + s + " " + start + " " + before + " " + count);
13181237
}
13191238

1320-
if (!mIsSettingTextFromCacheUpdate) {
1321-
if (!mIsSettingTextFromJS && mListeners != null) {
1322-
for (TextWatcher listener : mListeners) {
1323-
listener.onTextChanged(s, start, before, count);
1324-
}
1239+
if (!mIsSettingTextFromJS && mListeners != null) {
1240+
for (TextWatcher listener : mListeners) {
1241+
listener.onTextChanged(s, start, before, count);
13251242
}
1326-
1327-
updateCachedSpannable(
1328-
!mIsSettingTextFromJS && !mIsSettingTextFromState && start == 0 && before == 0);
13291243
}
13301244

1245+
updateCachedSpannable();
1246+
13311247
onContentSizeChange();
13321248
}
13331249

13341250
@Override
13351251
public void afterTextChanged(Editable s) {
1336-
if (!mIsSettingTextFromCacheUpdate && !mIsSettingTextFromJS && mListeners != null) {
1252+
if (!mIsSettingTextFromJS && mListeners != null) {
13371253
for (TextWatcher listener : mListeners) {
13381254
listener.afterTextChanged(s);
13391255
}

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
@@ -1343,9 +1343,6 @@ public Object updateState(
13431343
TextLayoutManager.getOrCreateSpannableForText(
13441344
view.getContext(), attributedString, mReactTextViewManagerCallback);
13451345

1346-
boolean containsMultipleFragments =
1347-
attributedString.getArray("fragments").toArrayList().size() > 1;
1348-
13491346
int textBreakStrategy =
13501347
TextAttributeProps.getTextBreakStrategy(paragraphAttributes.getString("textBreakStrategy"));
13511348

@@ -1354,8 +1351,7 @@ public Object updateState(
13541351
state.getInt("mostRecentEventCount"),
13551352
TextAttributeProps.getTextAlignment(props, TextLayoutManager.isRTL(attributedString)),
13561353
textBreakStrategy,
1357-
TextAttributeProps.getJustificationMode(props),
1358-
containsMultipleFragments);
1354+
TextAttributeProps.getJustificationMode(props, currentJustificationMode));
13591355
}
13601356

13611357
public Object getReactTextUpdate(ReactEditText view, ReactStylesDiffMap props, MapBuffer state) {
@@ -1376,9 +1372,6 @@ public Object getReactTextUpdate(ReactEditText view, ReactStylesDiffMap props, M
13761372
TextLayoutManagerMapBuffer.getOrCreateSpannableForText(
13771373
view.getContext(), attributedString, mReactTextViewManagerCallback);
13781374

1379-
boolean containsMultipleFragments =
1380-
attributedString.getMapBuffer(TextLayoutManagerMapBuffer.AS_KEY_FRAGMENTS).getCount() > 1;
1381-
13821375
int textBreakStrategy =
13831376
TextAttributeProps.getTextBreakStrategy(
13841377
paragraphAttributes.getString(TextLayoutManagerMapBuffer.PA_KEY_TEXT_BREAK_STRATEGY));
@@ -1389,7 +1382,6 @@ public Object getReactTextUpdate(ReactEditText view, ReactStylesDiffMap props, M
13891382
TextAttributeProps.getTextAlignment(
13901383
props, TextLayoutManagerMapBuffer.isRTL(attributedString)),
13911384
textBreakStrategy,
1392-
TextAttributeProps.getJustificationMode(props),
1393-
containsMultipleFragments);
1385+
TextAttributeProps.getJustificationMode(props, currentJustificationMode));
13941386
}
13951387
}

0 commit comments

Comments
 (0)