Skip to content

Commit 0556e86

Browse files
JoshuaGrossfacebook-github-bot
authored andcommitted
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 <TextInput> is initially given contents via children <Text> 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 <TextInput> 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 <TextInput> 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 <TextInput> 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
1 parent 0bae474 commit 0556e86

File tree

4 files changed

+41
-12
lines changed

4 files changed

+41
-12
lines changed

ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ void AndroidTextInputShadowNode::setContextContainer(
2929
contextContainer_ = contextContainer;
3030
}
3131

32-
AttributedString AndroidTextInputShadowNode::getAttributedString(
33-
bool usePlaceholders) const {
32+
AttributedString AndroidTextInputShadowNode::getAttributedString() const {
3433
// Use BaseTextShadowNode to get attributed string from children
3534
auto childTextAttributes = TextAttributes::defaultTextAttributes();
3635
childTextAttributes.apply(getProps()->textAttributes);
@@ -61,16 +60,21 @@ AttributedString AndroidTextInputShadowNode::getAttributedString(
6160
return attributedString;
6261
}
6362

63+
return getPlaceholderAttributedString(false);
64+
}
65+
66+
// For measurement purposes, we want to make sure that there's at least a
67+
// single character in the string so that the measured height is greater
68+
// than zero. Otherwise, empty TextInputs with no placeholder don't
69+
// display at all.
70+
AttributedString AndroidTextInputShadowNode::getPlaceholderAttributedString(
71+
bool ensureMinimumLength) const {
6472
// Return placeholder text, since text and children are empty.
6573
auto textAttributedString = AttributedString{};
6674
auto fragment = AttributedString::Fragment{};
6775
fragment.string = getProps()->placeholder;
6876

69-
// For measurement purposes, we want to make sure that there's at least a
70-
// single character in the string so that the measured height is greater
71-
// than zero. Otherwise, empty TextInputs with no placeholder don't
72-
// display at all.
73-
if (fragment.string.empty() && usePlaceholders) {
77+
if (fragment.string.empty() && ensureMinimumLength) {
7478
fragment.string = " ";
7579
}
7680

@@ -92,21 +96,25 @@ void AndroidTextInputShadowNode::setTextLayoutManager(
9296
void AndroidTextInputShadowNode::updateStateIfNeeded() {
9397
ensureUnsealed();
9498

95-
auto attributedString = getAttributedString(false);
99+
auto reactTreeAttributedString = getAttributedString();
96100
auto const &state = getStateData();
97101

98102
assert(textLayoutManager_);
99103
assert(
100104
(!state.layoutManager || state.layoutManager == textLayoutManager_) &&
101105
"`StateData` refers to a different `TextLayoutManager`");
102106

103-
if (state.attributedString == attributedString &&
107+
// Tree is often out of sync with the value of the TextInput.
108+
// This is by design - don't change the value of the TextInput in the State,
109+
// and therefore in Java, unless the tree itself changes.
110+
if (state.reactTreeAttributedString == reactTreeAttributedString &&
104111
state.layoutManager == textLayoutManager_) {
105112
return;
106113
}
107114

108115
setStateData(AndroidTextInputState{state.mostRecentEventCount,
109-
attributedString,
116+
reactTreeAttributedString,
117+
reactTreeAttributedString,
110118
getProps()->paragraphAttributes,
111119
textLayoutManager_});
112120
}
@@ -115,7 +123,13 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() {
115123

116124
Size AndroidTextInputShadowNode::measure(
117125
LayoutConstraints layoutConstraints) const {
118-
AttributedString attributedString = getAttributedString(true);
126+
auto const &state = getStateData();
127+
128+
AttributedString attributedString = state.attributedString;
129+
130+
if (attributedString.isEmpty()) {
131+
attributedString = getPlaceholderAttributedString(true);
132+
}
119133

120134
if (attributedString.isEmpty()) {
121135
return {0, 0};

ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ class AndroidTextInputShadowNode : public ConcreteViewShadowNode<
3737
/*
3838
* Returns a `AttributedString` which represents text content of the node.
3939
*/
40-
AttributedString getAttributedString(bool usePlaceholders) const;
40+
AttributedString getAttributedString() const;
41+
AttributedString getPlaceholderAttributedString(
42+
bool ensureMinimumLength) const;
4143

4244
/*
4345
* Associates a shared TextLayoutManager with the node.

ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace react {
1515

1616
#ifdef ANDROID
1717
folly::dynamic AndroidTextInputState::getDynamic() const {
18+
// Java doesn't need all fields, so we don't pass them along.
1819
folly::dynamic newState = folly::dynamic::object();
1920
newState["mostRecentEventCount"] = mostRecentEventCount;
2021
newState["attributedString"] = toDynamic(attributedString);

ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ class AndroidTextInputState final {
3131
*/
3232
AttributedString attributedString{};
3333

34+
/*
35+
* All content of <TextInput> component represented as an `AttributedString`.
36+
* This stores the previous computed *from the React tree*. This usually
37+
* doesn't change as the TextInput contents are being updated. If it does
38+
* change, we need to wipe out current contents of the TextInput and replace
39+
* with the new value from the tree.
40+
*/
41+
AttributedString reactTreeAttributedString{};
42+
3443
/*
3544
* Represents all visual attributes of a paragraph of text represented as
3645
* a ParagraphAttributes.
@@ -79,10 +88,12 @@ class AndroidTextInputState final {
7988
AndroidTextInputState(
8089
int64_t mostRecentEventCount,
8190
AttributedString const &attributedString,
91+
AttributedString const &reactTreeAttributedString,
8292
ParagraphAttributes const &paragraphAttributes,
8393
SharedTextLayoutManager const &layoutManager)
8494
: mostRecentEventCount(mostRecentEventCount),
8595
attributedString(attributedString),
96+
reactTreeAttributedString(reactTreeAttributedString),
8697
paragraphAttributes(paragraphAttributes),
8798
layoutManager(layoutManager) {}
8899
AndroidTextInputState() = default;
@@ -92,6 +103,7 @@ class AndroidTextInputState final {
92103
: mostRecentEventCount((int64_t)data["mostRecentEventCount"].getInt()),
93104
attributedString(
94105
updateAttributedString(previousState.attributedString, data)),
106+
reactTreeAttributedString(previousState.reactTreeAttributedString),
95107
paragraphAttributes(previousState.paragraphAttributes),
96108
layoutManager(previousState.layoutManager){};
97109
folly::dynamic getDynamic() const;

0 commit comments

Comments
 (0)