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