From 0556e86d09404105dc7ff695686b8b7c01911c5c Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 5 Dec 2019 13:16:09 -0800 Subject: [PATCH] TextInput: support modifying TextInputs with multiple Fragments (Cxx side) Summary: Support for modifying AndroidTextInputs with multiple Fragments was added on the Java side in a previous diff. This diff adds support on the C++ side for the following scenario: A is initially given contents via children notes, which represents multiple Fragments (that could have different TextAttributes like color, font size, backgroundcolor, etc). The Android EditText view must get this initial data, and then all updates after that on the Android side must flow to C++ so that the C++ ShadowNode can perform layout and measurement with up-to-date data. At the same time, the node could be updated from the JS side. All else equal, this would cause the native Android EditText to be replaced with the old, original contents of the that may not have been updated at all from the JS side. To mitigate this, we keep track of two AttributedStrings with Fragments on the C++ side: the AttributedString representing the values coming from children, from JS (`treeAttributedString`); and the AttributedString representing the current value the user is interacting with (`attributedString`). If the children from JS don't change, we don't update Android/Java with that AttributedString. If the children from JS do change, we overwrite any user input with the tree from JS. Changelog: [Internal] Reviewed By: shergin, mdvacca Differential Revision: D18785976 fbshipit-source-id: a1f3a935e02379cabca8ab62a39cb3c0cf3fbca5 --- .../AndroidTextInputShadowNode.cpp | 36 +++++++++++++------ .../AndroidTextInputShadowNode.h | 4 ++- .../AndroidTextInputState.cpp | 1 + .../androidtextinput/AndroidTextInputState.h | 12 +++++++ 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp index 1446c2d3909..cad08f0f9e8 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp @@ -29,8 +29,7 @@ void AndroidTextInputShadowNode::setContextContainer( contextContainer_ = contextContainer; } -AttributedString AndroidTextInputShadowNode::getAttributedString( - bool usePlaceholders) const { +AttributedString AndroidTextInputShadowNode::getAttributedString() const { // Use BaseTextShadowNode to get attributed string from children auto childTextAttributes = TextAttributes::defaultTextAttributes(); childTextAttributes.apply(getProps()->textAttributes); @@ -61,16 +60,21 @@ AttributedString AndroidTextInputShadowNode::getAttributedString( return attributedString; } + return getPlaceholderAttributedString(false); +} + +// For measurement purposes, we want to make sure that there's at least a +// single character in the string so that the measured height is greater +// than zero. Otherwise, empty TextInputs with no placeholder don't +// display at all. +AttributedString AndroidTextInputShadowNode::getPlaceholderAttributedString( + bool ensureMinimumLength) const { // Return placeholder text, since text and children are empty. auto textAttributedString = AttributedString{}; auto fragment = AttributedString::Fragment{}; fragment.string = getProps()->placeholder; - // For measurement purposes, we want to make sure that there's at least a - // single character in the string so that the measured height is greater - // than zero. Otherwise, empty TextInputs with no placeholder don't - // display at all. - if (fragment.string.empty() && usePlaceholders) { + if (fragment.string.empty() && ensureMinimumLength) { fragment.string = " "; } @@ -92,7 +96,7 @@ void AndroidTextInputShadowNode::setTextLayoutManager( void AndroidTextInputShadowNode::updateStateIfNeeded() { ensureUnsealed(); - auto attributedString = getAttributedString(false); + auto reactTreeAttributedString = getAttributedString(); auto const &state = getStateData(); assert(textLayoutManager_); @@ -100,13 +104,17 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() { (!state.layoutManager || state.layoutManager == textLayoutManager_) && "`StateData` refers to a different `TextLayoutManager`"); - if (state.attributedString == attributedString && + // Tree is often out of sync with the value of the TextInput. + // This is by design - don't change the value of the TextInput in the State, + // and therefore in Java, unless the tree itself changes. + if (state.reactTreeAttributedString == reactTreeAttributedString && state.layoutManager == textLayoutManager_) { return; } setStateData(AndroidTextInputState{state.mostRecentEventCount, - attributedString, + reactTreeAttributedString, + reactTreeAttributedString, getProps()->paragraphAttributes, textLayoutManager_}); } @@ -115,7 +123,13 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() { Size AndroidTextInputShadowNode::measure( LayoutConstraints layoutConstraints) const { - AttributedString attributedString = getAttributedString(true); + auto const &state = getStateData(); + + AttributedString attributedString = state.attributedString; + + if (attributedString.isEmpty()) { + attributedString = getPlaceholderAttributedString(true); + } if (attributedString.isEmpty()) { return {0, 0}; diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h index 796260c8c85..334ebb06648 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h @@ -37,7 +37,9 @@ class AndroidTextInputShadowNode : public ConcreteViewShadowNode< /* * Returns a `AttributedString` which represents text content of the node. */ - AttributedString getAttributedString(bool usePlaceholders) const; + AttributedString getAttributedString() const; + AttributedString getPlaceholderAttributedString( + bool ensureMinimumLength) const; /* * Associates a shared TextLayoutManager with the node. diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp index 4a53da92f24..4e37be0aa6f 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp @@ -15,6 +15,7 @@ namespace react { #ifdef ANDROID folly::dynamic AndroidTextInputState::getDynamic() const { + // Java doesn't need all fields, so we don't pass them along. folly::dynamic newState = folly::dynamic::object(); newState["mostRecentEventCount"] = mostRecentEventCount; newState["attributedString"] = toDynamic(attributedString); diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h index 8d356637396..b305fefb654 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h @@ -31,6 +31,15 @@ class AndroidTextInputState final { */ AttributedString attributedString{}; + /* + * All content of component represented as an `AttributedString`. + * This stores the previous computed *from the React tree*. This usually + * doesn't change as the TextInput contents are being updated. If it does + * change, we need to wipe out current contents of the TextInput and replace + * with the new value from the tree. + */ + AttributedString reactTreeAttributedString{}; + /* * Represents all visual attributes of a paragraph of text represented as * a ParagraphAttributes. @@ -79,10 +88,12 @@ class AndroidTextInputState final { AndroidTextInputState( int64_t mostRecentEventCount, AttributedString const &attributedString, + AttributedString const &reactTreeAttributedString, ParagraphAttributes const ¶graphAttributes, SharedTextLayoutManager const &layoutManager) : mostRecentEventCount(mostRecentEventCount), attributedString(attributedString), + reactTreeAttributedString(reactTreeAttributedString), paragraphAttributes(paragraphAttributes), layoutManager(layoutManager) {} AndroidTextInputState() = default; @@ -92,6 +103,7 @@ class AndroidTextInputState final { : mostRecentEventCount((int64_t)data["mostRecentEventCount"].getInt()), attributedString( updateAttributedString(previousState.attributedString, data)), + reactTreeAttributedString(previousState.reactTreeAttributedString), paragraphAttributes(previousState.paragraphAttributes), layoutManager(previousState.layoutManager){}; folly::dynamic getDynamic() const;