8
8
package com .facebook .react .views .textinput ;
9
9
10
10
import static com .facebook .react .uimanager .UIManagerHelper .getReactContext ;
11
+ import static com .facebook .react .views .text .TextAttributeProps .UNSET ;
11
12
12
13
import android .content .Context ;
13
14
import android .graphics .Rect ;
17
18
import android .os .Bundle ;
18
19
import android .text .Editable ;
19
20
import android .text .InputType ;
20
- import android .text .SpannableString ;
21
+ import android .text .Spannable ;
21
22
import android .text .SpannableStringBuilder ;
22
23
import android .text .Spanned ;
23
24
import android .text .TextUtils ;
41
42
import com .facebook .react .bridge .ReactContext ;
42
43
import com .facebook .react .uimanager .FabricViewStateManager ;
43
44
import com .facebook .react .uimanager .UIManagerModule ;
45
+ import com .facebook .react .views .text .CustomLetterSpacingSpan ;
46
+ import com .facebook .react .views .text .CustomLineHeightSpan ;
47
+ import com .facebook .react .views .text .CustomStyleSpan ;
48
+ import com .facebook .react .views .text .ReactAbsoluteSizeSpan ;
44
49
import com .facebook .react .views .text .ReactSpan ;
45
50
import com .facebook .react .views .text .ReactTextUpdate ;
46
51
import com .facebook .react .views .text .ReactTypefaceUtils ;
49
54
import com .facebook .react .views .text .TextLayoutManager ;
50
55
import com .facebook .react .views .view .ReactViewBackgroundManager ;
51
56
import java .util .ArrayList ;
57
+ import java .util .List ;
52
58
53
59
/**
54
60
* A wrapper around the EditText that lets us better control what happens when an EditText gets
@@ -70,6 +76,7 @@ public class ReactEditText extends AppCompatEditText
70
76
// *TextChanged events should be triggered. This is less expensive than removing the text
71
77
// listeners and adding them back again after the text change is completed.
72
78
protected boolean mIsSettingTextFromJS ;
79
+ protected boolean mIsSettingTextFromCacheUpdate = false ;
73
80
private int mDefaultGravityHorizontal ;
74
81
private int mDefaultGravityVertical ;
75
82
@@ -325,7 +332,7 @@ public void setSelection(int start, int end) {
325
332
@ Override
326
333
protected void onSelectionChanged (int selStart , int selEnd ) {
327
334
super .onSelectionChanged (selStart , selEnd );
328
- if (mSelectionWatcher != null && hasFocus ()) {
335
+ if (! mIsSettingTextFromCacheUpdate && mSelectionWatcher != null && hasFocus ()) {
329
336
mSelectionWatcher .onSelectionChanged (selStart , selEnd );
330
337
}
331
338
}
@@ -502,7 +509,7 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) {
502
509
SpannableStringBuilder spannableStringBuilder =
503
510
new SpannableStringBuilder (reactTextUpdate .getText ());
504
511
505
- manageSpans (spannableStringBuilder );
512
+ manageSpans (spannableStringBuilder , reactTextUpdate . mContainsMultipleFragments );
506
513
mContainsImages = reactTextUpdate .containsImages ();
507
514
508
515
// When we update text, we trigger onChangeText code that will
@@ -528,10 +535,8 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) {
528
535
}
529
536
}
530
537
531
- // Update cached spans (in Fabric only)
532
- if (this .getFabricViewStateManager () != null ) {
533
- TextLayoutManager .setCachedSpannabledForTag (getId (), spannableStringBuilder );
534
- }
538
+ // Update cached spans (in Fabric only).
539
+ updateCachedSpannable (false );
535
540
}
536
541
537
542
/**
@@ -540,30 +545,42 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) {
540
545
* will adapt to the new text, hence why {@link SpannableStringBuilder#replace} never removes
541
546
* them.
542
547
*/
543
- private void manageSpans (SpannableStringBuilder spannableStringBuilder ) {
548
+ private void manageSpans (
549
+ SpannableStringBuilder spannableStringBuilder , boolean skipAddSpansForMeasurements ) {
544
550
Object [] spans = getText ().getSpans (0 , length (), Object .class );
545
551
for (int spanIdx = 0 ; spanIdx < spans .length ; spanIdx ++) {
552
+ Object span = spans [spanIdx ];
553
+ int spanFlags = getText ().getSpanFlags (span );
554
+ boolean isExclusiveExclusive =
555
+ (spanFlags & Spanned .SPAN_EXCLUSIVE_EXCLUSIVE ) == Spanned .SPAN_EXCLUSIVE_EXCLUSIVE ;
556
+
546
557
// Remove all styling spans we might have previously set
547
- if (spans [ spanIdx ] instanceof ReactSpan ) {
548
- getText ().removeSpan (spans [ spanIdx ] );
558
+ if (span instanceof ReactSpan ) {
559
+ getText ().removeSpan (span );
549
560
}
550
561
551
- if (( getText (). getSpanFlags ( spans [ spanIdx ]) & Spanned . SPAN_EXCLUSIVE_EXCLUSIVE )
552
- != Spanned . SPAN_EXCLUSIVE_EXCLUSIVE ) {
562
+ // We only add spans back for EXCLUSIVE_EXCLUSIVE spans
563
+ if (! isExclusiveExclusive ) {
553
564
continue ;
554
565
}
555
- Object span = spans [spanIdx ];
556
- final int spanStart = getText ().getSpanStart (spans [spanIdx ]);
557
- final int spanEnd = getText ().getSpanEnd (spans [spanIdx ]);
558
- final int spanFlags = getText ().getSpanFlags (spans [spanIdx ]);
566
+
567
+ final int spanStart = getText ().getSpanStart (span );
568
+ final int spanEnd = getText ().getSpanEnd (span );
559
569
560
570
// Make sure the span is removed from existing text, otherwise the spans we set will be
561
571
// ignored or it will cover text that has changed.
562
- getText ().removeSpan (spans [ spanIdx ] );
572
+ getText ().removeSpan (span );
563
573
if (sameTextForSpan (getText (), spannableStringBuilder , spanStart , spanEnd )) {
564
574
spannableStringBuilder .setSpan (span , spanStart , spanEnd , spanFlags );
565
575
}
566
576
}
577
+
578
+ // In Fabric only, apply necessary styles to entire span
579
+ // If the Spannable was constructed from multiple fragments, we don't apply any spans that could
580
+ // impact the whole Spannable, because that would override "local" styles per-fragment
581
+ if (!skipAddSpansForMeasurements ) {
582
+ addSpansForMeasurement (getText ());
583
+ }
567
584
}
568
585
569
586
private static boolean sameTextForSpan (
@@ -582,6 +599,75 @@ private static boolean sameTextForSpan(
582
599
return true ;
583
600
}
584
601
602
+ // This is hacked in for Fabric. When we delete non-Fabric code, we might be able to simplify or
603
+ // clean this up a bit.
604
+ private void addSpansForMeasurement (Spannable spannable ) {
605
+ if (!mFabricViewStateManager .hasStateWrapper ()) {
606
+ return ;
607
+ }
608
+
609
+ boolean originalDisableTextDiffing = mDisableTextDiffing ;
610
+ mDisableTextDiffing = true ;
611
+
612
+ int start = 0 ;
613
+ int end = spannable .length ();
614
+
615
+ // Remove duplicate spans we might add here
616
+ Object [] spans = spannable .getSpans (0 , length (), Object .class );
617
+ for (Object span : spans ) {
618
+ int spanFlags = spannable .getSpanFlags (span );
619
+ boolean isInclusive =
620
+ (spanFlags & Spanned .SPAN_INCLUSIVE_INCLUSIVE ) == Spanned .SPAN_INCLUSIVE_INCLUSIVE
621
+ || (spanFlags & Spanned .SPAN_INCLUSIVE_EXCLUSIVE ) == Spanned .SPAN_INCLUSIVE_EXCLUSIVE ;
622
+ if (isInclusive
623
+ && span instanceof ReactSpan
624
+ && spannable .getSpanStart (span ) == start
625
+ && spannable .getSpanEnd (span ) == end ) {
626
+ spannable .removeSpan (span );
627
+ }
628
+ }
629
+
630
+ List <TextLayoutManager .SetSpanOperation > ops = new ArrayList <>();
631
+
632
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .LOLLIPOP ) {
633
+ if (!Float .isNaN (mTextAttributes .getLetterSpacing ())) {
634
+ ops .add (
635
+ new TextLayoutManager .SetSpanOperation (
636
+ start , end , new CustomLetterSpacingSpan (mTextAttributes .getLetterSpacing ())));
637
+ }
638
+ }
639
+ ops .add (
640
+ new TextLayoutManager .SetSpanOperation (
641
+ start , end , new ReactAbsoluteSizeSpan ((int ) mTextAttributes .getEffectiveFontSize ())));
642
+ if (mFontStyle != UNSET || mFontWeight != UNSET || mFontFamily != null ) {
643
+ ops .add (
644
+ new TextLayoutManager .SetSpanOperation (
645
+ start ,
646
+ end ,
647
+ new CustomStyleSpan (
648
+ mFontStyle ,
649
+ mFontWeight ,
650
+ null , // TODO: do we need to support FontFeatureSettings / fontVariant?
651
+ mFontFamily ,
652
+ getReactContext (ReactEditText .this ).getAssets ())));
653
+ }
654
+ if (!Float .isNaN (mTextAttributes .getEffectiveLineHeight ())) {
655
+ ops .add (
656
+ new TextLayoutManager .SetSpanOperation (
657
+ start , end , new CustomLineHeightSpan (mTextAttributes .getEffectiveLineHeight ())));
658
+ }
659
+
660
+ int priority = 0 ;
661
+ for (TextLayoutManager .SetSpanOperation op : ops ) {
662
+ // Actual order of calling {@code execute} does NOT matter,
663
+ // but the {@code priority} DOES matter.
664
+ op .execute (spannable , priority );
665
+ priority ++;
666
+ }
667
+
668
+ mDisableTextDiffing = originalDisableTextDiffing ;
669
+ }
670
+
585
671
protected boolean showSoftKeyboard () {
586
672
return mInputMethodManager .showSoftInput (this , 0 );
587
673
}
@@ -842,14 +928,67 @@ public FabricViewStateManager getFabricViewStateManager() {
842
928
return mFabricViewStateManager ;
843
929
}
844
930
931
+ /**
932
+ * Update the cached Spannable used in TextLayoutManager to measure the text in Fabric. This is
933
+ * mostly copied from ReactTextInputShadowNode.java (the non-Fabric version) and
934
+ * TextLayoutManager.java with some very minor modifications. There's some duplication between
935
+ * here and TextLayoutManager, so there might be an opportunity for refactor.
936
+ */
937
+ private void updateCachedSpannable (boolean resetStyles ) {
938
+ // Noops in non-Fabric
939
+ if (getFabricViewStateManager () == null ) {
940
+ return ;
941
+ }
942
+ // If this view doesn't have an ID yet, we don't have a cache key, so bail here
943
+ if (getId () == -1 ) {
944
+ return ;
945
+ }
946
+
947
+ if (resetStyles ) {
948
+ mIsSettingTextFromCacheUpdate = true ;
949
+ addSpansForMeasurement (getText ());
950
+ mIsSettingTextFromCacheUpdate = false ;
951
+ }
952
+
953
+ Editable currentText = getText ();
954
+ boolean haveText = currentText != null && currentText .length () > 0 ;
955
+
956
+ SpannableStringBuilder sb = new SpannableStringBuilder ();
957
+
958
+ // A note of caution: appending currentText to sb appends all the spans of currentText - not
959
+ // copies of the Spans, but the actual span objects. Any modifications to sb after that point
960
+ // can modify the spans of sb/currentText, impact the text or spans visible on screen, and
961
+ // also call the TextChangeWatcher methods.
962
+ if (haveText ) {
963
+ sb .append (currentText );
964
+ }
965
+
966
+ // If we don't have text, make sure we have *something* to measure.
967
+ // Hint has the same dimensions - the only thing that's different is background or foreground
968
+ // color
969
+ if (!haveText ) {
970
+ if (getHint () != null && getHint ().length () > 0 ) {
971
+ sb .append (getHint ());
972
+ } else {
973
+ // Measure something so we have correct height, even if there's no string.
974
+ sb .append ("I" );
975
+ }
976
+
977
+ // Make sure that all text styles are applied when we're measurable the hint or "blank" text
978
+ addSpansForMeasurement (sb );
979
+ }
980
+
981
+ TextLayoutManager .setCachedSpannabledForTag (getId (), sb );
982
+ }
983
+
845
984
/**
846
985
* This class will redirect *TextChanged calls to the listeners only in the case where the text is
847
986
* changed by the user, and not explicitly set by JS.
848
987
*/
849
988
private class TextWatcherDelegator implements TextWatcher {
850
989
@ Override
851
990
public void beforeTextChanged (CharSequence s , int start , int count , int after ) {
852
- if (!mIsSettingTextFromJS && mListeners != null ) {
991
+ if (!mIsSettingTextFromCacheUpdate && ! mIsSettingTextFromJS && mListeners != null ) {
853
992
for (TextWatcher listener : mListeners ) {
854
993
listener .beforeTextChanged (s , start , count , after );
855
994
}
@@ -858,22 +997,23 @@ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
858
997
859
998
@ Override
860
999
public void onTextChanged (CharSequence s , int start , int before , int count ) {
861
- if (!mIsSettingTextFromJS && mListeners != null ) {
862
- for (TextWatcher listener : mListeners ) {
863
- listener .onTextChanged (s , start , before , count );
1000
+ if (!mIsSettingTextFromCacheUpdate ) {
1001
+ if (!mIsSettingTextFromJS && mListeners != null ) {
1002
+ for (TextWatcher listener : mListeners ) {
1003
+ listener .onTextChanged (s , start , before , count );
1004
+ }
864
1005
}
865
- }
866
1006
867
- if ( getFabricViewStateManager () != null ) {
868
- TextLayoutManager . setCachedSpannabledForTag ( getId (), new SpannableString ( getText ()) );
1007
+ updateCachedSpannable (
1008
+ ! mIsSettingTextFromJS && ! mIsSettingTextFromState && start == 0 && before == 0 );
869
1009
}
870
1010
871
1011
onContentSizeChange ();
872
1012
}
873
1013
874
1014
@ Override
875
1015
public void afterTextChanged (Editable s ) {
876
- if (!mIsSettingTextFromJS && mListeners != null ) {
1016
+ if (!mIsSettingTextFromCacheUpdate && ! mIsSettingTextFromJS && mListeners != null ) {
877
1017
for (TextWatcher listener : mListeners ) {
878
1018
listener .afterTextChanged (s );
879
1019
}
0 commit comments