11
11
import static com .facebook .react .views .text .TextAttributeProps .UNSET ;
12
12
13
13
import android .content .Context ;
14
+ import android .graphics .Color ;
15
+ import android .graphics .Paint ;
14
16
import android .graphics .Rect ;
15
17
import android .graphics .Typeface ;
16
18
import android .graphics .drawable .Drawable ;
50
52
import com .facebook .react .views .text .CustomLineHeightSpan ;
51
53
import com .facebook .react .views .text .CustomStyleSpan ;
52
54
import com .facebook .react .views .text .ReactAbsoluteSizeSpan ;
55
+ import com .facebook .react .views .text .ReactBackgroundColorSpan ;
56
+ import com .facebook .react .views .text .ReactForegroundColorSpan ;
53
57
import com .facebook .react .views .text .ReactSpan ;
58
+ import com .facebook .react .views .text .ReactStrikethroughSpan ;
54
59
import com .facebook .react .views .text .ReactTextUpdate ;
55
60
import com .facebook .react .views .text .ReactTypefaceUtils ;
61
+ import com .facebook .react .views .text .ReactUnderlineSpan ;
56
62
import com .facebook .react .views .text .TextAttributes ;
57
63
import com .facebook .react .views .text .TextInlineImageSpan ;
58
64
import com .facebook .react .views .text .TextLayoutManager ;
59
65
import com .facebook .react .views .view .ReactViewBackgroundManager ;
60
66
import java .util .ArrayList ;
61
67
import java .util .List ;
68
+ import java .util .Objects ;
62
69
63
70
/**
64
71
* A wrapper around the EditText that lets us better control what happens when an EditText gets
@@ -477,6 +484,14 @@ public void setFontStyle(String fontStyleString) {
477
484
}
478
485
}
479
486
487
+ @ Override
488
+ public void setFontFeatureSettings (String fontFeatureSettings ) {
489
+ if (!Objects .equals (fontFeatureSettings , getFontFeatureSettings ())) {
490
+ super .setFontFeatureSettings (fontFeatureSettings );
491
+ mTypefaceDirty = true ;
492
+ }
493
+ }
494
+
480
495
public void maybeUpdateTypeface () {
481
496
if (!mTypefaceDirty ) {
482
497
return ;
@@ -488,6 +503,17 @@ public void maybeUpdateTypeface() {
488
503
ReactTypefaceUtils .applyStyles (
489
504
getTypeface (), mFontStyle , mFontWeight , mFontFamily , getContext ().getAssets ());
490
505
setTypeface (newTypeface );
506
+
507
+ // Match behavior of CustomStyleSpan and enable SUBPIXEL_TEXT_FLAG when setting anything
508
+ // nonstandard
509
+ if (mFontStyle != UNSET
510
+ || mFontWeight != UNSET
511
+ || mFontFamily != null
512
+ || getFontFeatureSettings () != null ) {
513
+ setPaintFlags (getPaintFlags () | Paint .SUBPIXEL_TEXT_FLAG );
514
+ } else {
515
+ setPaintFlags (getPaintFlags () & (~Paint .SUBPIXEL_TEXT_FLAG ));
516
+ }
491
517
}
492
518
493
519
// VisibleForTesting from {@link TextInputEventsTestCase}.
@@ -550,9 +576,7 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) {
550
576
new SpannableStringBuilder (reactTextUpdate .getText ());
551
577
552
578
manageSpans (spannableStringBuilder , reactTextUpdate .mContainsMultipleFragments );
553
-
554
- // Mitigation for https://github.com/facebook/react-native/issues/35936 (S318090)
555
- stripAbsoluteSizeSpans (spannableStringBuilder );
579
+ stripStyleEquivalentSpans (spannableStringBuilder );
556
580
557
581
mContainsImages = reactTextUpdate .containsImages ();
558
582
@@ -627,24 +651,163 @@ private void manageSpans(
627
651
}
628
652
}
629
653
630
- private void stripAbsoluteSizeSpans (SpannableStringBuilder sb ) {
631
- // We have already set a font size on the EditText itself. We can safely remove sizing spans
632
- // which are the same as the set font size, and not otherwise overlapped.
633
- final int effectiveFontSize = mTextAttributes .getEffectiveFontSize ();
634
- ReactAbsoluteSizeSpan [] spans = sb .getSpans (0 , sb .length (), ReactAbsoluteSizeSpan .class );
654
+ // TODO: Replace with Predicate<T> and lambdas once Java 8 builds in OSS
655
+ interface SpanPredicate <T > {
656
+ boolean test (T span );
657
+ }
658
+
659
+ /**
660
+ * Remove spans from the SpannableStringBuilder which can be represented by TextAppearance
661
+ * attributes on the underlying EditText. This works around instability on Samsung devices with
662
+ * the presence of spans https://github.com/facebook/react-native/issues/35936 (S318090)
663
+ */
664
+ private void stripStyleEquivalentSpans (SpannableStringBuilder sb ) {
665
+ stripSpansOfKind (
666
+ sb ,
667
+ ReactAbsoluteSizeSpan .class ,
668
+ new SpanPredicate <ReactAbsoluteSizeSpan >() {
669
+ @ Override
670
+ public boolean test (ReactAbsoluteSizeSpan span ) {
671
+ return span .getSize () == mTextAttributes .getEffectiveFontSize ();
672
+ }
673
+ });
674
+
675
+ stripSpansOfKind (
676
+ sb ,
677
+ ReactBackgroundColorSpan .class ,
678
+ new SpanPredicate <ReactBackgroundColorSpan >() {
679
+ @ Override
680
+ public boolean test (ReactBackgroundColorSpan span ) {
681
+ return span .getBackgroundColor () == mReactBackgroundManager .getBackgroundColor ();
682
+ }
683
+ });
635
684
636
- outerLoop :
637
- for (ReactAbsoluteSizeSpan span : spans ) {
638
- ReactAbsoluteSizeSpan [] overlappingSpans =
639
- sb .getSpans (sb .getSpanStart (span ), sb .getSpanEnd (span ), ReactAbsoluteSizeSpan .class );
685
+ stripSpansOfKind (
686
+ sb ,
687
+ ReactForegroundColorSpan .class ,
688
+ new SpanPredicate <ReactForegroundColorSpan >() {
689
+ @ Override
690
+ public boolean test (ReactForegroundColorSpan span ) {
691
+ return span .getForegroundColor () == getCurrentTextColor ();
692
+ }
693
+ });
640
694
641
- for (ReactAbsoluteSizeSpan overlappingSpan : overlappingSpans ) {
642
- if (span .getSize () != effectiveFontSize ) {
643
- continue outerLoop ;
644
- }
695
+ stripSpansOfKind (
696
+ sb ,
697
+ ReactStrikethroughSpan .class ,
698
+ new SpanPredicate <ReactStrikethroughSpan >() {
699
+ @ Override
700
+ public boolean test (ReactStrikethroughSpan span ) {
701
+ return (getPaintFlags () & Paint .STRIKE_THRU_TEXT_FLAG ) != 0 ;
702
+ }
703
+ });
704
+
705
+ stripSpansOfKind (
706
+ sb ,
707
+ ReactUnderlineSpan .class ,
708
+ new SpanPredicate <ReactUnderlineSpan >() {
709
+ @ Override
710
+ public boolean test (ReactUnderlineSpan span ) {
711
+ return (getPaintFlags () & Paint .UNDERLINE_TEXT_FLAG ) != 0 ;
712
+ }
713
+ });
714
+
715
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .LOLLIPOP ) {
716
+ stripSpansOfKind (
717
+ sb ,
718
+ CustomLetterSpacingSpan .class ,
719
+ new SpanPredicate <CustomLetterSpacingSpan >() {
720
+ @ Override
721
+ public boolean test (CustomLetterSpacingSpan span ) {
722
+ return span .getSpacing () == mTextAttributes .getEffectiveLetterSpacing ();
723
+ }
724
+ });
725
+ }
726
+
727
+ stripSpansOfKind (
728
+ sb ,
729
+ CustomStyleSpan .class ,
730
+ new SpanPredicate <CustomStyleSpan >() {
731
+ @ Override
732
+ public boolean test (CustomStyleSpan span ) {
733
+ return span .getStyle () == mFontStyle
734
+ && Objects .equals (span .getFontFamily (), mFontFamily )
735
+ && span .getWeight () == mFontWeight
736
+ && Objects .equals (span .getFontFeatureSettings (), getFontFeatureSettings ());
737
+ }
738
+ });
739
+ }
740
+
741
+ private <T > void stripSpansOfKind (
742
+ SpannableStringBuilder sb , Class <T > clazz , SpanPredicate <T > shouldStrip ) {
743
+ T [] spans = sb .getSpans (0 , sb .length (), clazz );
744
+
745
+ for (T span : spans ) {
746
+ if (shouldStrip .test (span )) {
747
+ sb .removeSpan (span );
748
+ }
749
+ }
750
+ }
751
+
752
+ /**
753
+ * Copy back styles represented as attributes to the underlying span, for later measurement
754
+ * outside the ReactEditText.
755
+ */
756
+ private void restoreStyleEquivalentSpans (SpannableStringBuilder workingText ) {
757
+ int spanFlags = Spannable .SPAN_INCLUSIVE_INCLUSIVE ;
758
+
759
+ // Set all bits for SPAN_PRIORITY so that this span has the highest possible priority
760
+ // (least precedence). This ensures the span is behind any overlapping spans.
761
+ spanFlags |= Spannable .SPAN_PRIORITY ;
762
+
763
+ workingText .setSpan (
764
+ new ReactAbsoluteSizeSpan (mTextAttributes .getEffectiveFontSize ()),
765
+ 0 ,
766
+ workingText .length (),
767
+ spanFlags );
768
+
769
+ workingText .setSpan (
770
+ new ReactForegroundColorSpan (getCurrentTextColor ()), 0 , workingText .length (), spanFlags );
771
+
772
+ int backgroundColor = mReactBackgroundManager .getBackgroundColor ();
773
+ if (backgroundColor != Color .TRANSPARENT ) {
774
+ workingText .setSpan (
775
+ new ReactBackgroundColorSpan (backgroundColor ), 0 , workingText .length (), spanFlags );
776
+ }
777
+
778
+ if ((getPaintFlags () & Paint .STRIKE_THRU_TEXT_FLAG ) != 0 ) {
779
+ workingText .setSpan (new ReactStrikethroughSpan (), 0 , workingText .length (), spanFlags );
780
+ }
781
+
782
+ if ((getPaintFlags () & Paint .UNDERLINE_TEXT_FLAG ) != 0 ) {
783
+ workingText .setSpan (new ReactUnderlineSpan (), 0 , workingText .length (), spanFlags );
784
+ }
785
+
786
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .LOLLIPOP ) {
787
+ float effectiveLetterSpacing = mTextAttributes .getEffectiveLetterSpacing ();
788
+ if (!Float .isNaN (effectiveLetterSpacing )) {
789
+ workingText .setSpan (
790
+ new CustomLetterSpacingSpan (effectiveLetterSpacing ),
791
+ 0 ,
792
+ workingText .length (),
793
+ spanFlags );
645
794
}
795
+ }
646
796
647
- sb .removeSpan (span );
797
+ if (mFontStyle != UNSET
798
+ || mFontWeight != UNSET
799
+ || mFontFamily != null
800
+ || getFontFeatureSettings () != null ) {
801
+ workingText .setSpan (
802
+ new CustomStyleSpan (
803
+ mFontStyle ,
804
+ mFontWeight ,
805
+ getFontFeatureSettings (),
806
+ mFontFamily ,
807
+ getContext ().getAssets ()),
808
+ 0 ,
809
+ workingText .length (),
810
+ spanFlags );
648
811
}
649
812
}
650
813
@@ -994,7 +1157,9 @@ protected void applyTextAttributes() {
994
1157
995
1158
float effectiveLetterSpacing = mTextAttributes .getEffectiveLetterSpacing ();
996
1159
if (!Float .isNaN (effectiveLetterSpacing )) {
997
- setLetterSpacing (effectiveLetterSpacing );
1160
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .LOLLIPOP ) {
1161
+ setLetterSpacing (effectiveLetterSpacing );
1162
+ }
998
1163
}
999
1164
}
1000
1165
@@ -1067,6 +1232,7 @@ private void updateCachedSpannable(boolean resetStyles) {
1067
1232
// - android.app.Activity.dispatchKeyEvent (Activity.java:3447)
1068
1233
try {
1069
1234
sb .append (currentText .subSequence (0 , currentText .length ()));
1235
+ restoreStyleEquivalentSpans (sb );
1070
1236
} catch (IndexOutOfBoundsException e ) {
1071
1237
ReactSoftExceptionLogger .logSoftException (TAG , e );
1072
1238
}
0 commit comments