diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLetterSpacingSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLetterSpacingSpan.java index 3b9cf58e33d3a1..d537cd5dccc101 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLetterSpacingSpan.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLetterSpacingSpan.java @@ -37,6 +37,10 @@ public void updateMeasureState(TextPaint paint) { apply(paint); } + public float getSpacing() { + return mLetterSpacing; + } + private void apply(TextPaint paint) { if (!Float.isNaN(mLetterSpacing)) { paint.setLetterSpacing(mLetterSpacing); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java index b249126cf95750..7866390bfa09bd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomStyleSpan.java @@ -71,6 +71,10 @@ public int getWeight() { return mFontFamily; } + public @Nullable String getFontFeatureSettings() { + return mFeatureSettings; + } + private static void apply( Paint paint, int style, diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java index f1591a5fe35d2b..82c9f5d75ac517 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java @@ -31,8 +31,6 @@ public class ReactTextUpdate { private final int mSelectionEnd; private final int mJustificationMode; - public boolean mContainsMultipleFragments; - /** * @deprecated Use a non-deprecated constructor for ReactTextUpdate instead. This one remains * because it's being used by a unit test that isn't currently open source. @@ -142,13 +140,11 @@ public static ReactTextUpdate buildReactTextUpdateFromState( int jsEventCounter, int textAlign, int textBreakStrategy, - int justificationMode, - boolean containsMultipleFragments) { + int justificationMode) { ReactTextUpdate reactTextUpdate = new ReactTextUpdate( text, jsEventCounter, false, textAlign, textBreakStrategy, justificationMode); - reactTextUpdate.mContainsMultipleFragments = containsMultipleFragments; return reactTextUpdate; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java index ecbf0d45489b8a..d86cf9a095adfa 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java @@ -106,6 +106,11 @@ public static boolean isRTL(MapBuffer attributedString) { MapBuffer fragment = fragments.getMapBuffer((short) 0); MapBuffer textAttributes = fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES); + + if (!textAttributes.contains(TextAttributeProps.TA_KEY_LAYOUT_DIRECTION)) { + return false; + } + return TextAttributeProps.getLayoutDirection( textAttributes.getString(TextAttributeProps.TA_KEY_LAYOUT_DIRECTION)) == LayoutDirection.RTL; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK index c393044683735b..ef99a74bf377eb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/BUCK @@ -9,7 +9,8 @@ rn_android_library( "pfh:ReactNative_CommonInfrastructurePlaceholder", "supermodule:xplat/default/public.react_native.infra", ], - language = "JAVA", + language = "KOTLIN", + pure_kotlin = False, required_for_source_only_abi = True, visibility = [ "PUBLIC", @@ -29,7 +30,9 @@ rn_android_library( react_native_target("java/com/facebook/react/views/imagehelper:imagehelper"), react_native_target("java/com/facebook/react/views/scroll:scroll"), react_native_target("java/com/facebook/react/views/text:text"), + react_native_target("java/com/facebook/react/common/mapbuffer:mapbuffer"), react_native_target("java/com/facebook/react/views/view:view"), + react_native_target("java/com/facebook/react/config:config"), ], exported_deps = [ react_native_dep("third-party/android/androidx:appcompat"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 77ab6bb365b15e..b078c8f87b45e7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -11,6 +11,8 @@ import static com.facebook.react.views.text.TextAttributeProps.UNSET; import android.content.Context; +import android.graphics.Color; +import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; @@ -50,15 +52,19 @@ import com.facebook.react.views.text.CustomLineHeightSpan; import com.facebook.react.views.text.CustomStyleSpan; import com.facebook.react.views.text.ReactAbsoluteSizeSpan; +import com.facebook.react.views.text.ReactBackgroundColorSpan; +import com.facebook.react.views.text.ReactForegroundColorSpan; import com.facebook.react.views.text.ReactSpan; +import com.facebook.react.views.text.ReactStrikethroughSpan; import com.facebook.react.views.text.ReactTextUpdate; import com.facebook.react.views.text.ReactTypefaceUtils; +import com.facebook.react.views.text.ReactUnderlineSpan; import com.facebook.react.views.text.TextAttributes; import com.facebook.react.views.text.TextInlineImageSpan; import com.facebook.react.views.text.TextLayoutManager; import com.facebook.react.views.view.ReactViewBackgroundManager; import java.util.ArrayList; -import java.util.List; +import java.util.Objects; /** * A wrapper around the EditText that lets us better control what happens when an EditText gets @@ -82,7 +88,6 @@ public class ReactEditText extends AppCompatEditText // *TextChanged events should be triggered. This is less expensive than removing the text // listeners and adding them back again after the text change is completed. protected boolean mIsSettingTextFromJS; - protected boolean mIsSettingTextFromCacheUpdate = false; private int mDefaultGravityHorizontal; private int mDefaultGravityVertical; @@ -363,7 +368,7 @@ protected void onSelectionChanged(int selStart, int selEnd) { } super.onSelectionChanged(selStart, selEnd); - if (!mIsSettingTextFromCacheUpdate && mSelectionWatcher != null && hasFocus()) { + if (mSelectionWatcher != null && hasFocus()) { mSelectionWatcher.onSelectionChanged(selStart, selEnd); } } @@ -482,6 +487,14 @@ public void setFontStyle(String fontStyleString) { } } + @Override + public void setFontFeatureSettings(String fontFeatureSettings) { + if (!Objects.equals(fontFeatureSettings, getFontFeatureSettings())) { + super.setFontFeatureSettings(fontFeatureSettings); + mTypefaceDirty = true; + } + } + public void maybeUpdateTypeface() { if (!mTypefaceDirty) { return; @@ -493,6 +506,17 @@ public void maybeUpdateTypeface() { ReactTypefaceUtils.applyStyles( getTypeface(), mFontStyle, mFontWeight, mFontFamily, getContext().getAssets()); setTypeface(newTypeface); + + // Match behavior of CustomStyleSpan and enable SUBPIXEL_TEXT_FLAG when setting anything + // nonstandard + if (mFontStyle != UNSET + || mFontWeight != UNSET + || mFontFamily != null + || getFontFeatureSettings() != null) { + setPaintFlags(getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG); + } else { + setPaintFlags(getPaintFlags() & (~Paint.SUBPIXEL_TEXT_FLAG)); + } } // VisibleForTesting from {@link TextInputEventsTestCase}. @@ -554,10 +578,8 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(reactTextUpdate.getText()); - manageSpans(spannableStringBuilder, reactTextUpdate.mContainsMultipleFragments); - - // Mitigation for https://github.com/facebook/react-native/issues/35936 (S318090) - stripAbsoluteSizeSpans(spannableStringBuilder); + manageSpans(spannableStringBuilder); + stripStyleEquivalentSpans(spannableStringBuilder); mContainsImages = reactTextUpdate.containsImages(); @@ -585,7 +607,7 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { } // Update cached spans (in Fabric only). - updateCachedSpannable(false); + updateCachedSpannable(); } /** @@ -594,8 +616,7 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { * will adapt to the new text, hence why {@link SpannableStringBuilder#replace} never removes * them. */ - private void manageSpans( - SpannableStringBuilder spannableStringBuilder, boolean skipAddSpansForMeasurements) { + private void manageSpans(SpannableStringBuilder spannableStringBuilder) { Object[] spans = getText().getSpans(0, length(), Object.class); for (int spanIdx = 0; spanIdx < spans.length; spanIdx++) { Object span = spans[spanIdx]; @@ -623,33 +644,170 @@ private void manageSpans( spannableStringBuilder.setSpan(span, spanStart, spanEnd, spanFlags); } } + } + + // TODO: Replace with Predicate and lambdas once Java 8 builds in OSS + interface SpanPredicate { + boolean test(T span); + } + + /** + * Remove spans from the SpannableStringBuilder which can be represented by TextAppearance + * attributes on the underlying EditText. This works around instability on Samsung devices with + * the presence of spans https://github.com/facebook/react-native/issues/35936 (S318090) + */ + private void stripStyleEquivalentSpans(SpannableStringBuilder sb) { + stripSpansOfKind( + sb, + ReactAbsoluteSizeSpan.class, + new SpanPredicate() { + @Override + public boolean test(ReactAbsoluteSizeSpan span) { + return span.getSize() == mTextAttributes.getEffectiveFontSize(); + } + }); + + stripSpansOfKind( + sb, + ReactBackgroundColorSpan.class, + new SpanPredicate() { + @Override + public boolean test(ReactBackgroundColorSpan span) { + return span.getBackgroundColor() == mReactBackgroundManager.getBackgroundColor(); + } + }); + + stripSpansOfKind( + sb, + ReactForegroundColorSpan.class, + new SpanPredicate() { + @Override + public boolean test(ReactForegroundColorSpan span) { + return span.getForegroundColor() == getCurrentTextColor(); + } + }); + + stripSpansOfKind( + sb, + ReactStrikethroughSpan.class, + new SpanPredicate() { + @Override + public boolean test(ReactStrikethroughSpan span) { + return (getPaintFlags() & Paint.STRIKE_THRU_TEXT_FLAG) != 0; + } + }); - // In Fabric only, apply necessary styles to entire span - // If the Spannable was constructed from multiple fragments, we don't apply any spans that could - // impact the whole Spannable, because that would override "local" styles per-fragment - if (!skipAddSpansForMeasurements) { - addSpansForMeasurement(getText()); + stripSpansOfKind( + sb, + ReactUnderlineSpan.class, + new SpanPredicate() { + @Override + public boolean test(ReactUnderlineSpan span) { + return (getPaintFlags() & Paint.UNDERLINE_TEXT_FLAG) != 0; + } + }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + stripSpansOfKind( + sb, + CustomLetterSpacingSpan.class, + new SpanPredicate() { + @Override + public boolean test(CustomLetterSpacingSpan span) { + return span.getSpacing() == mTextAttributes.getEffectiveLetterSpacing(); + } + }); } + + stripSpansOfKind( + sb, + CustomStyleSpan.class, + new SpanPredicate() { + @Override + public boolean test(CustomStyleSpan span) { + return span.getStyle() == mFontStyle + && Objects.equals(span.getFontFamily(), mFontFamily) + && span.getWeight() == mFontWeight + && Objects.equals(span.getFontFeatureSettings(), getFontFeatureSettings()); + } + }); } - private void stripAbsoluteSizeSpans(SpannableStringBuilder sb) { - // We have already set a font size on the EditText itself. We can safely remove sizing spans - // which are the same as the set font size, and not otherwise overlapped. - final int effectiveFontSize = mTextAttributes.getEffectiveFontSize(); - ReactAbsoluteSizeSpan[] spans = sb.getSpans(0, sb.length(), ReactAbsoluteSizeSpan.class); + private void stripSpansOfKind( + SpannableStringBuilder sb, Class clazz, SpanPredicate shouldStrip) { + T[] spans = sb.getSpans(0, sb.length(), clazz); - outerLoop: - for (ReactAbsoluteSizeSpan span : spans) { - ReactAbsoluteSizeSpan[] overlappingSpans = - sb.getSpans(sb.getSpanStart(span), sb.getSpanEnd(span), ReactAbsoluteSizeSpan.class); + for (T span : spans) { + if (shouldStrip.test(span)) { + sb.removeSpan(span); + } + } + } - for (ReactAbsoluteSizeSpan overlappingSpan : overlappingSpans) { - if (span.getSize() != effectiveFontSize) { - continue outerLoop; - } + /** + * Copy styles represented as attributes to the underlying span, for later measurement or other + * usage outside the ReactEditText. + */ + private void addSpansFromStyleAttributes(SpannableStringBuilder workingText) { + int spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE; + + // Set all bits for SPAN_PRIORITY so that this span has the highest possible priority + // (least precedence). This ensures the span is behind any overlapping spans. + spanFlags |= Spannable.SPAN_PRIORITY; + + workingText.setSpan( + new ReactAbsoluteSizeSpan(mTextAttributes.getEffectiveFontSize()), + 0, + workingText.length(), + spanFlags); + + workingText.setSpan( + new ReactForegroundColorSpan(getCurrentTextColor()), 0, workingText.length(), spanFlags); + + int backgroundColor = mReactBackgroundManager.getBackgroundColor(); + if (backgroundColor != Color.TRANSPARENT) { + workingText.setSpan( + new ReactBackgroundColorSpan(backgroundColor), 0, workingText.length(), spanFlags); + } + + if ((getPaintFlags() & Paint.STRIKE_THRU_TEXT_FLAG) != 0) { + workingText.setSpan(new ReactStrikethroughSpan(), 0, workingText.length(), spanFlags); + } + + if ((getPaintFlags() & Paint.UNDERLINE_TEXT_FLAG) != 0) { + workingText.setSpan(new ReactUnderlineSpan(), 0, workingText.length(), spanFlags); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + float effectiveLetterSpacing = mTextAttributes.getEffectiveLetterSpacing(); + if (!Float.isNaN(effectiveLetterSpacing)) { + workingText.setSpan( + new CustomLetterSpacingSpan(effectiveLetterSpacing), + 0, + workingText.length(), + spanFlags); } + } + + if (mFontStyle != UNSET + || mFontWeight != UNSET + || mFontFamily != null + || getFontFeatureSettings() != null) { + workingText.setSpan( + new CustomStyleSpan( + mFontStyle, + mFontWeight, + getFontFeatureSettings(), + mFontFamily, + getContext().getAssets()), + 0, + workingText.length(), + spanFlags); + } - sb.removeSpan(span); + float lineHeight = mTextAttributes.getEffectiveLineHeight(); + if (!Float.isNaN(lineHeight)) { + workingText.setSpan(new CustomLineHeightSpan(lineHeight), 0, workingText.length(), spanFlags); } } @@ -669,73 +827,6 @@ private static boolean sameTextForSpan( return true; } - // This is hacked in for Fabric. When we delete non-Fabric code, we might be able to simplify or - // clean this up a bit. - private void addSpansForMeasurement(Spannable spannable) { - if (!mFabricViewStateManager.hasStateWrapper()) { - return; - } - - boolean originalDisableTextDiffing = mDisableTextDiffing; - mDisableTextDiffing = true; - - int start = 0; - int end = spannable.length(); - - // Remove duplicate spans we might add here - Object[] spans = spannable.getSpans(0, length(), Object.class); - for (Object span : spans) { - int spanFlags = spannable.getSpanFlags(span); - boolean isInclusive = - (spanFlags & Spanned.SPAN_INCLUSIVE_INCLUSIVE) == Spanned.SPAN_INCLUSIVE_INCLUSIVE - || (spanFlags & Spanned.SPAN_INCLUSIVE_EXCLUSIVE) == Spanned.SPAN_INCLUSIVE_EXCLUSIVE; - if (isInclusive - && span instanceof ReactSpan - && spannable.getSpanStart(span) == start - && spannable.getSpanEnd(span) == end) { - spannable.removeSpan(span); - } - } - - List ops = new ArrayList<>(); - - if (!Float.isNaN(mTextAttributes.getLetterSpacing())) { - ops.add( - new TextLayoutManager.SetSpanOperation( - start, end, new CustomLetterSpacingSpan(mTextAttributes.getLetterSpacing()))); - } - ops.add( - new TextLayoutManager.SetSpanOperation( - start, end, new ReactAbsoluteSizeSpan((int) mTextAttributes.getEffectiveFontSize()))); - if (mFontStyle != UNSET || mFontWeight != UNSET || mFontFamily != null) { - ops.add( - new TextLayoutManager.SetSpanOperation( - start, - end, - new CustomStyleSpan( - mFontStyle, - mFontWeight, - null, // TODO: do we need to support FontFeatureSettings / fontVariant? - mFontFamily, - getReactContext(ReactEditText.this).getAssets()))); - } - if (!Float.isNaN(mTextAttributes.getEffectiveLineHeight())) { - ops.add( - new TextLayoutManager.SetSpanOperation( - start, end, new CustomLineHeightSpan(mTextAttributes.getEffectiveLineHeight()))); - } - - int priority = 0; - for (TextLayoutManager.SetSpanOperation op : ops) { - // Actual order of calling {@code execute} does NOT matter, - // but the {@code priority} DOES matter. - op.execute(spannable, priority); - priority++; - } - - mDisableTextDiffing = originalDisableTextDiffing; - } - protected boolean showSoftKeyboard() { return mInputMethodManager.showSoftInput(this, 0); } @@ -1000,7 +1091,9 @@ protected void applyTextAttributes() { float effectiveLetterSpacing = mTextAttributes.getEffectiveLetterSpacing(); if (!Float.isNaN(effectiveLetterSpacing)) { - setLetterSpacing(effectiveLetterSpacing); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setLetterSpacing(effectiveLetterSpacing); + } } } @@ -1015,7 +1108,7 @@ public FabricViewStateManager getFabricViewStateManager() { * TextLayoutManager.java with some very minor modifications. There's some duplication between * here and TextLayoutManager, so there might be an opportunity for refactor. */ - private void updateCachedSpannable(boolean resetStyles) { + private void updateCachedSpannable() { // Noops in non-Fabric if (mFabricViewStateManager == null || !mFabricViewStateManager.hasStateWrapper()) { return; @@ -1025,12 +1118,6 @@ private void updateCachedSpannable(boolean resetStyles) { return; } - if (resetStyles) { - mIsSettingTextFromCacheUpdate = true; - addSpansForMeasurement(getText()); - mIsSettingTextFromCacheUpdate = false; - } - Editable currentText = getText(); boolean haveText = currentText != null && currentText.length() > 0; @@ -1088,11 +1175,9 @@ private void updateCachedSpannable(boolean resetStyles) { // Measure something so we have correct height, even if there's no string. sb.append("I"); } - - // Make sure that all text styles are applied when we're measurable the hint or "blank" text - addSpansForMeasurement(sb); } + addSpansFromStyleAttributes(sb); TextLayoutManager.setCachedSpannabledForTag(getId(), sb); } @@ -1107,7 +1192,7 @@ void setEventDispatcher(@Nullable EventDispatcher eventDispatcher) { private class TextWatcherDelegator implements TextWatcher { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { - if (!mIsSettingTextFromCacheUpdate && !mIsSettingTextFromJS && mListeners != null) { + if (!mIsSettingTextFromJS && mListeners != null) { for (TextWatcher listener : mListeners) { listener.beforeTextChanged(s, start, count, after); } @@ -1121,23 +1206,20 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { TAG, "onTextChanged[" + getId() + "]: " + s + " " + start + " " + before + " " + count); } - if (!mIsSettingTextFromCacheUpdate) { - if (!mIsSettingTextFromJS && mListeners != null) { - for (TextWatcher listener : mListeners) { - listener.onTextChanged(s, start, before, count); - } + if (!mIsSettingTextFromJS && mListeners != null) { + for (TextWatcher listener : mListeners) { + listener.onTextChanged(s, start, before, count); } - - updateCachedSpannable( - !mIsSettingTextFromJS && !mIsSettingTextFromState && start == 0 && before == 0); } + updateCachedSpannable(); + onContentSizeChange(); } @Override public void afterTextChanged(Editable s) { - if (!mIsSettingTextFromCacheUpdate && !mIsSettingTextFromJS && mListeners != null) { + if (!mIsSettingTextFromJS && mListeners != null) { for (TextWatcher listener : mListeners) { listener.afterTextChanged(s); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index f3134f9cb1dc8d..d34961afda55ee 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -13,6 +13,7 @@ import android.content.res.ColorStateList; import android.graphics.BlendMode; import android.graphics.BlendModeColorFilter; +import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Build; @@ -44,6 +45,8 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.MapBuilder; +import com.facebook.react.common.mapbuffer.MapBuffer; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.uimanager.BaseViewManager; import com.facebook.react.uimanager.FabricViewStateManager; @@ -66,9 +69,11 @@ import com.facebook.react.views.text.ReactBaseTextShadowNode; import com.facebook.react.views.text.ReactTextUpdate; import com.facebook.react.views.text.ReactTextViewManagerCallback; +import com.facebook.react.views.text.ReactTypefaceUtils; import com.facebook.react.views.text.TextAttributeProps; import com.facebook.react.views.text.TextInlineImageSpan; import com.facebook.react.views.text.TextLayoutManager; +import com.facebook.react.views.text.TextLayoutManagerMapBuffer; import com.facebook.react.views.text.TextTransform; import com.facebook.yoga.YogaConstants; import java.lang.reflect.Field; @@ -83,6 +88,12 @@ public class ReactTextInputManager extends BaseViewManager 1; - int textBreakStrategy = TextAttributeProps.getTextBreakStrategy(paragraphAttributes.getString("textBreakStrategy")); @@ -1313,7 +1347,37 @@ public Object updateState( state.getInt("mostRecentEventCount"), TextAttributeProps.getTextAlignment(props, TextLayoutManager.isRTL(attributedString)), textBreakStrategy, - TextAttributeProps.getJustificationMode(props), - containsMultipleFragments); + TextAttributeProps.getJustificationMode(props, currentJustificationMode)); + } + + public Object getReactTextUpdate(ReactEditText view, ReactStylesDiffMap props, MapBuffer state) { + // If native wants to update the state wrapper but the state data hasn't actually + // changed, the MapBuffer may be empty + if (state.getCount() == 0) { + return null; + } + + MapBuffer attributedString = state.getMapBuffer(TX_STATE_KEY_ATTRIBUTED_STRING); + MapBuffer paragraphAttributes = state.getMapBuffer(TX_STATE_KEY_PARAGRAPH_ATTRIBUTES); + if (attributedString == null || paragraphAttributes == null) { + throw new IllegalArgumentException( + "Invalid TextInput State (MapBuffer) was received as a parameters"); + } + + Spannable spanned = + TextLayoutManagerMapBuffer.getOrCreateSpannableForText( + view.getContext(), attributedString, mReactTextViewManagerCallback); + + int textBreakStrategy = + TextAttributeProps.getTextBreakStrategy( + paragraphAttributes.getString(TextLayoutManagerMapBuffer.PA_KEY_TEXT_BREAK_STRATEGY)); + + return ReactTextUpdate.buildReactTextUpdateFromState( + spanned, + state.getInt(TX_STATE_KEY_MOST_RECENT_EVENT_COUNT), + TextAttributeProps.getTextAlignment( + props, TextLayoutManagerMapBuffer.isRTL(attributedString)), + textBreakStrategy, + TextAttributeProps.getJustificationMode(props, currentJustificationMode)); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java index 4a5fce52c3a9e5..9b479fa42214be 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java @@ -19,6 +19,7 @@ public class ReactViewBackgroundManager { private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable; private View mView; + private int mColor = Color.TRANSPARENT; public ReactViewBackgroundManager(View view) { this.mView = view; @@ -56,6 +57,10 @@ public void setBackgroundColor(int color) { } } + public int getBackgroundColor() { + return mColor; + } + public void setBorderWidth(int position, float width) { getOrCreateReactViewBackground().setBorderWidth(position, width); } diff --git a/ReactCommon/react/renderer/components/text/ParagraphState.h b/ReactCommon/react/renderer/components/text/ParagraphState.h index 76dcbcf1cee0c5..24a8e1258e8bb1 100644 --- a/ReactCommon/react/renderer/components/text/ParagraphState.h +++ b/ReactCommon/react/renderer/components/text/ParagraphState.h @@ -20,6 +20,15 @@ namespace facebook { namespace react { +#ifdef ANDROID +// constants for Text State serialization +constexpr static MapBuffer::Key TX_STATE_KEY_ATTRIBUTED_STRING = 0; +constexpr static MapBuffer::Key TX_STATE_KEY_PARAGRAPH_ATTRIBUTES = 1; +// Used for TextInput only +constexpr static MapBuffer::Key TX_STATE_KEY_HASH = 2; +constexpr static MapBuffer::Key TX_STATE_KEY_MOST_RECENT_EVENT_COUNT = 3; +#endif + /* * State for component. * Represents what to render and how to render. diff --git a/ReactCommon/react/renderer/components/text/conversions.h b/ReactCommon/react/renderer/components/text/conversions.h index 712b8f2fde2325..a4a90aa37c9eeb 100644 --- a/ReactCommon/react/renderer/components/text/conversions.h +++ b/ReactCommon/react/renderer/components/text/conversions.h @@ -26,21 +26,13 @@ inline folly::dynamic toDynamic(ParagraphState const ¶graphState) { return newState; } -// constants for Text State serialization -constexpr static MapBuffer::Key TX_STATE_KEY_ATTRIBUTED_STRING = 0; -constexpr static MapBuffer::Key TX_STATE_KEY_PARAGRAPH_ATTRIBUTES = 1; -// Used for TextInput -constexpr static MapBuffer::Key TX_STATE_KEY_HASH = 2; -constexpr static MapBuffer::Key TX_STATE_KEY_MOST_RECENT_EVENT_COUNT = 3; - inline MapBuffer toMapBuffer(ParagraphState const ¶graphState) { auto builder = MapBufferBuilder(); auto attStringMapBuffer = toMapBuffer(paragraphState.attributedString); builder.putMapBuffer(TX_STATE_KEY_ATTRIBUTED_STRING, attStringMapBuffer); auto paMapBuffer = toMapBuffer(paragraphState.paragraphAttributes); builder.putMapBuffer(TX_STATE_KEY_PARAGRAPH_ATTRIBUTES, paMapBuffer); - // TODO: Used for TextInput - builder.putInt(TX_STATE_KEY_HASH, 1234); + builder.putInt(TX_STATE_KEY_HASH, attStringMapBuffer.getInt(AS_KEY_HASH)); return builder.build(); } #endif diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.cpp b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.cpp index a2deb59db337d9..c57e8b7970f501 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.cpp +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.cpp @@ -10,6 +10,11 @@ #include #include +#ifdef ANDROID +#include +#include +#endif + #include namespace facebook { @@ -89,6 +94,23 @@ folly::dynamic AndroidTextInputState::getDynamic() const { } return newState; } + +MapBuffer AndroidTextInputState::getMapBuffer() const { + auto builder = MapBufferBuilder(); + // See comment in getDynamic block. + if (cachedAttributedStringId == 0) { + builder.putInt(TX_STATE_KEY_MOST_RECENT_EVENT_COUNT, mostRecentEventCount); + + auto attStringMapBuffer = toMapBuffer(attributedString); + builder.putMapBuffer(TX_STATE_KEY_ATTRIBUTED_STRING, attStringMapBuffer); + auto paMapBuffer = toMapBuffer(paragraphAttributes); + builder.putMapBuffer(TX_STATE_KEY_PARAGRAPH_ATTRIBUTES, paMapBuffer); + + builder.putInt(TX_STATE_KEY_HASH, attStringMapBuffer.getInt(AS_KEY_HASH)); + } + return builder.build(); +} + #endif } // namespace react diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h index 4dc50ac9410188..f46d6829e1aff7 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h @@ -94,9 +94,7 @@ class AndroidTextInputState final { AndroidTextInputState const &previousState, folly::dynamic const &data); folly::dynamic getDynamic() const; - MapBuffer getMapBuffer() const { - return MapBufferBuilder::EMPTY(); - }; + MapBuffer getMapBuffer() const; }; } // namespace react