Skip to content

Commit 6865e5a

Browse files
Preparation for sharing common ShadowNode functionality in BaseTextInputShadowNode for Android (facebook#48582)
Summary: Pull Request resolved: facebook#48582 [Changelog] [Internal] - Preparation for sharing common ShadowNode functionality in BaseTextInputShadowNode for Android As a preparation for facebook#48165 this change aligns the order of methods between: - BaseTextInputShadowNode.h - AndroidTextInputShadowNode.h to make it easier for future changes to look at the delta between both implementations. The goal is land facebook#48582 which aligns the RN iOS and RN Android implementation Reviewed By: NickGerleman Differential Revision: D68001423 fbshipit-source-id: 5a5efa6542de676bd175744e7313c2b819e67f11
1 parent e53b76b commit 6865e5a

File tree

3 files changed

+169
-172
lines changed

3 files changed

+169
-172
lines changed

packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,32 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<
120120
top;
121121
}
122122

123+
/*
124+
* Determines the constraints to use while measure the underlying text
125+
*/
126+
LayoutConstraints getTextConstraints(
127+
const LayoutConstraints& layoutConstraints) const {
128+
if (BaseShadowNode::getConcreteProps().multiline) {
129+
return layoutConstraints;
130+
} else {
131+
// A single line TextInput acts as a horizontal scroller of infinitely
132+
// expandable text, so we want to measure the text as if it is allowed to
133+
// infinitely expand horizontally, and later clamp to the constraints of
134+
// the input.
135+
return LayoutConstraints{
136+
.minimumSize = layoutConstraints.minimumSize,
137+
.maximumSize =
138+
Size{
139+
.width = std::numeric_limits<Float>::infinity(),
140+
.height = layoutConstraints.maximumSize.height,
141+
},
142+
.layoutDirection = layoutConstraints.layoutDirection,
143+
};
144+
}
145+
}
146+
147+
std::shared_ptr<const TextLayoutManager> textLayoutManager_;
148+
123149
private:
124150
/*
125151
* Creates a `State` object if needed.
@@ -150,48 +176,22 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<
150176
AttributedString getAttributedString(
151177
const LayoutContext& layoutContext) const {
152178
const auto& props = BaseShadowNode::getConcreteProps();
153-
auto textAttributes =
179+
const auto textAttributes =
154180
props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier);
155-
auto attributedString = AttributedString{};
156181

182+
AttributedString attributedString;
157183
attributedString.appendFragment(AttributedString::Fragment{
158184
.string = props.text,
159185
.textAttributes = textAttributes,
160-
// TODO: Is this really meant to be by value?
161186
.parentShadowView = ShadowView(*this)});
162187

163188
auto attachments = BaseTextShadowNode::Attachments{};
164189
BaseTextShadowNode::buildAttributedString(
165190
textAttributes, *this, attributedString, attachments);
166191
attributedString.setBaseTextAttributes(textAttributes);
167-
168192
return attributedString;
169193
}
170194

171-
/*
172-
* Determines the constraints to use while measure the underlying text
173-
*/
174-
LayoutConstraints getTextConstraints(
175-
const LayoutConstraints& layoutConstraints) const {
176-
if (BaseShadowNode::getConcreteProps().multiline) {
177-
return layoutConstraints;
178-
} else {
179-
// A single line TextInput acts as a horizontal scroller of infinitely
180-
// expandable text, so we want to measure the text as if it is allowed to
181-
// infinitely expand horizontally, and later clamp to the constraints of
182-
// the input.
183-
return LayoutConstraints{
184-
.minimumSize = layoutConstraints.minimumSize,
185-
.maximumSize =
186-
Size{
187-
.width = std::numeric_limits<Float>::infinity(),
188-
.height = layoutConstraints.maximumSize.height,
189-
},
190-
.layoutDirection = layoutConstraints.layoutDirection,
191-
};
192-
}
193-
}
194-
195195
/*
196196
* Returns an `AttributedStringBox` which represents text content that should
197197
* be used for measuring purposes. It might contain actual text value,
@@ -232,8 +232,6 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<
232232
}
233233
return AttributedStringBox{attributedString};
234234
}
235-
236-
std::shared_ptr<const TextLayoutManager> textLayoutManager_;
237235
};
238236

239237
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp

Lines changed: 126 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -20,92 +20,81 @@ namespace facebook::react {
2020

2121
extern const char AndroidTextInputComponentName[] = "AndroidTextInput";
2222

23-
AttributedString AndroidTextInputShadowNode::getAttributedString() const {
24-
// Use BaseTextShadowNode to get attributed string from children
25-
auto childTextAttributes = TextAttributes::defaultTextAttributes();
26-
childTextAttributes.apply(getConcreteProps().textAttributes);
27-
// Don't propagate the background color of the TextInput onto the attributed
28-
// string. Android tries to render shadow of the background alongside the
29-
// shadow of the text which results in weird artifacts.
30-
childTextAttributes.backgroundColor = HostPlatformColor::UndefinedColor;
23+
void AndroidTextInputShadowNode::setTextLayoutManager(
24+
std::shared_ptr<const TextLayoutManager> textLayoutManager) {
25+
ensureUnsealed();
26+
textLayoutManager_ = std::move(textLayoutManager);
27+
}
3128

32-
auto attributedString = AttributedString{};
33-
auto attachments = BaseTextShadowNode::Attachments{};
34-
BaseTextShadowNode::buildAttributedString(
35-
childTextAttributes, *this, attributedString, attachments);
36-
attributedString.setBaseTextAttributes(childTextAttributes);
29+
Size AndroidTextInputShadowNode::measureContent(
30+
const LayoutContext& layoutContext,
31+
const LayoutConstraints& layoutConstraints) const {
32+
auto textConstraints = getTextConstraints(layoutConstraints);
3733

38-
// BaseTextShadowNode only gets children. We must detect and prepend text
39-
// value attributes manually.
40-
if (!getConcreteProps().text.empty()) {
41-
auto textAttributes = TextAttributes::defaultTextAttributes();
42-
textAttributes.apply(getConcreteProps().textAttributes);
43-
auto fragment = AttributedString::Fragment{};
44-
fragment.string = getConcreteProps().text;
45-
fragment.textAttributes = textAttributes;
46-
// If the TextInput opacity is 0 < n < 1, the opacity of the TextInput and
47-
// text value's background will stack. This is a hack/workaround to prevent
48-
// that effect.
49-
fragment.textAttributes.backgroundColor = clearColor();
50-
fragment.parentShadowView = ShadowView(*this);
51-
attributedString.prependFragment(std::move(fragment));
34+
if (getStateData().cachedAttributedStringId != 0) {
35+
auto textSize = textLayoutManager_
36+
->measureCachedSpannableById(
37+
getStateData().cachedAttributedStringId,
38+
getConcreteProps().paragraphAttributes,
39+
textConstraints)
40+
.size;
41+
return layoutConstraints.clamp(textSize);
5242
}
5343

54-
return attributedString;
55-
}
56-
57-
// For measurement purposes, we want to make sure that there's at least a
58-
// single character in the string so that the measured height is greater
59-
// than zero. Otherwise, empty TextInputs with no placeholder don't
60-
// display at all.
61-
// TODO T67606511: We will redefine the measurement of empty strings as part
62-
// of T67606511
63-
AttributedString AndroidTextInputShadowNode::getPlaceholderAttributedString()
64-
const {
65-
// Return placeholder text, since text and children are empty.
66-
auto textAttributedString = AttributedString{};
67-
auto fragment = AttributedString::Fragment{};
68-
fragment.string = getConcreteProps().placeholder;
44+
// Layout is called right after measure.
45+
// Measure is marked as `const`, and `layout` is not; so State can be
46+
// updated during layout, but not during `measure`. If State is out-of-date
47+
// in layout, it's too late: measure will have already operated on old
48+
// State. Thus, we use the same value here that we *will* use in layout to
49+
// update the state.
50+
AttributedString attributedString = getMostRecentAttributedString();
6951

70-
if (fragment.string.empty()) {
71-
fragment.string = BaseTextShadowNode::getEmptyPlaceholder();
52+
if (attributedString.isEmpty()) {
53+
attributedString = getPlaceholderAttributedString();
7254
}
7355

74-
auto textAttributes = TextAttributes::defaultTextAttributes();
75-
textAttributes.apply(getConcreteProps().textAttributes);
76-
77-
// If there's no text, it's possible that this Fragment isn't actually
78-
// appended to the AttributedString (see implementation of appendFragment)
79-
fragment.textAttributes = textAttributes;
80-
fragment.parentShadowView = ShadowView(*this);
81-
textAttributedString.appendFragment(std::move(fragment));
56+
if (attributedString.isEmpty() && getStateData().mostRecentEventCount != 0) {
57+
return {.width = 0, .height = 0};
58+
}
8259

83-
return textAttributedString;
60+
TextLayoutContext textLayoutContext;
61+
textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor;
62+
auto textSize = textLayoutManager_
63+
->measure(
64+
AttributedStringBox{attributedString},
65+
getConcreteProps().paragraphAttributes,
66+
textLayoutContext,
67+
textConstraints)
68+
.size;
69+
return layoutConstraints.clamp(textSize);
8470
}
8571

86-
void AndroidTextInputShadowNode::setTextLayoutManager(
87-
std::shared_ptr<const TextLayoutManager> textLayoutManager) {
88-
ensureUnsealed();
89-
textLayoutManager_ = std::move(textLayoutManager);
72+
void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) {
73+
updateStateIfNeeded();
74+
ConcreteViewShadowNode::layout(layoutContext);
9075
}
9176

92-
AttributedString AndroidTextInputShadowNode::getMostRecentAttributedString()
93-
const {
94-
const auto& state = getStateData();
77+
Float AndroidTextInputShadowNode::baseline(
78+
const LayoutContext& /*layoutContext*/,
79+
Size size) const {
80+
AttributedString attributedString = getMostRecentAttributedString();
9581

96-
auto reactTreeAttributedString = getAttributedString();
82+
if (attributedString.isEmpty()) {
83+
attributedString = getPlaceholderAttributedString();
84+
}
9785

98-
// Sometimes the treeAttributedString will only differ from the state
99-
// not by inherent properties (string or prop attributes), but by the frame of
100-
// the parent which has changed Thus, we can't directly compare the entire
101-
// AttributedString
102-
bool treeAttributedStringChanged =
103-
!state.reactTreeAttributedString.compareTextAttributesWithoutFrame(
104-
reactTreeAttributedString);
86+
// Yoga expects a baseline relative to the Node's border-box edge instead of
87+
// the content, so we need to adjust by the padding and border widths, which
88+
// have already been set by the time of baseline alignment
89+
auto top = YGNodeLayoutGetBorder(&yogaNode_, YGEdgeTop) +
90+
YGNodeLayoutGetPadding(&yogaNode_, YGEdgeTop);
10591

106-
return (
107-
!treeAttributedStringChanged ? state.attributedStringBox.getValue()
108-
: reactTreeAttributedString);
92+
AttributedStringBox attributedStringBox{attributedString};
93+
return textLayoutManager_->baseline(
94+
attributedStringBox,
95+
getConcreteProps().paragraphAttributes,
96+
size) +
97+
top;
10998
}
11099

111100
LayoutConstraints AndroidTextInputShadowNode::getTextConstraints(
@@ -165,77 +154,86 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() {
165154
newEventCount});
166155
}
167156

168-
#pragma mark - LayoutableShadowNode
157+
AttributedString AndroidTextInputShadowNode::getAttributedString() const {
158+
// Use BaseTextShadowNode to get attributed string from children
159+
auto childTextAttributes = TextAttributes::defaultTextAttributes();
160+
childTextAttributes.apply(getConcreteProps().textAttributes);
161+
// Don't propagate the background color of the TextInput onto the attributed
162+
// string. Android tries to render shadow of the background alongside the
163+
// shadow of the text which results in weird artifacts.
164+
childTextAttributes.backgroundColor = HostPlatformColor::UndefinedColor;
169165

170-
Size AndroidTextInputShadowNode::measureContent(
171-
const LayoutContext& layoutContext,
172-
const LayoutConstraints& layoutConstraints) const {
173-
auto textConstraints = getTextConstraints(layoutConstraints);
166+
auto attributedString = AttributedString{};
167+
auto attachments = BaseTextShadowNode::Attachments{};
168+
BaseTextShadowNode::buildAttributedString(
169+
childTextAttributes, *this, attributedString, attachments);
170+
attributedString.setBaseTextAttributes(childTextAttributes);
174171

175-
if (getStateData().cachedAttributedStringId != 0) {
176-
auto textSize = textLayoutManager_
177-
->measureCachedSpannableById(
178-
getStateData().cachedAttributedStringId,
179-
getConcreteProps().paragraphAttributes,
180-
textConstraints)
181-
.size;
182-
return layoutConstraints.clamp(textSize);
172+
// BaseTextShadowNode only gets children. We must detect and prepend text
173+
// value attributes manually.
174+
if (!getConcreteProps().text.empty()) {
175+
auto textAttributes = TextAttributes::defaultTextAttributes();
176+
textAttributes.apply(getConcreteProps().textAttributes);
177+
auto fragment = AttributedString::Fragment{};
178+
fragment.string = getConcreteProps().text;
179+
fragment.textAttributes = textAttributes;
180+
// If the TextInput opacity is 0 < n < 1, the opacity of the TextInput and
181+
// text value's background will stack. This is a hack/workaround to prevent
182+
// that effect.
183+
fragment.textAttributes.backgroundColor = clearColor();
184+
fragment.parentShadowView = ShadowView(*this);
185+
attributedString.prependFragment(std::move(fragment));
183186
}
184187

185-
// Layout is called right after measure.
186-
// Measure is marked as `const`, and `layout` is not; so State can be
187-
// updated during layout, but not during `measure`. If State is out-of-date
188-
// in layout, it's too late: measure will have already operated on old
189-
// State. Thus, we use the same value here that we *will* use in layout to
190-
// update the state.
191-
AttributedString attributedString = getMostRecentAttributedString();
188+
return attributedString;
189+
}
192190

193-
if (attributedString.isEmpty()) {
194-
attributedString = getPlaceholderAttributedString();
195-
}
191+
AttributedString AndroidTextInputShadowNode::getMostRecentAttributedString()
192+
const {
193+
const auto& state = getStateData();
196194

197-
if (attributedString.isEmpty() && getStateData().mostRecentEventCount != 0) {
198-
return {0, 0};
199-
}
195+
auto reactTreeAttributedString = getAttributedString();
200196

201-
TextLayoutContext textLayoutContext;
202-
textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor;
203-
auto textSize = textLayoutManager_
204-
->measure(
205-
AttributedStringBox{attributedString},
206-
getConcreteProps().paragraphAttributes,
207-
textLayoutContext,
208-
textConstraints)
209-
.size;
210-
return layoutConstraints.clamp(textSize);
197+
// Sometimes the treeAttributedString will only differ from the state
198+
// not by inherent properties (string or prop attributes), but by the frame of
199+
// the parent which has changed Thus, we can't directly compare the entire
200+
// AttributedString
201+
bool treeAttributedStringChanged =
202+
!state.reactTreeAttributedString.compareTextAttributesWithoutFrame(
203+
reactTreeAttributedString);
204+
205+
return (
206+
!treeAttributedStringChanged ? state.attributedStringBox.getValue()
207+
: reactTreeAttributedString);
211208
}
212209

213-
Float AndroidTextInputShadowNode::baseline(
214-
const LayoutContext& layoutContext,
215-
Size size) const {
216-
AttributedString attributedString = getMostRecentAttributedString();
210+
// For measurement purposes, we want to make sure that there's at least a
211+
// single character in the string so that the measured height is greater
212+
// than zero. Otherwise, empty TextInputs with no placeholder don't
213+
// display at all.
214+
// TODO T67606511: We will redefine the measurement of empty strings as part
215+
// of T67606511
216+
AttributedString AndroidTextInputShadowNode::getPlaceholderAttributedString()
217+
const {
218+
// Return placeholder text, since text and children are empty.
219+
auto textAttributedString = AttributedString{};
220+
auto fragment = AttributedString::Fragment{};
221+
fragment.string = getConcreteProps().placeholder;
217222

218-
if (attributedString.isEmpty()) {
219-
attributedString = getPlaceholderAttributedString();
223+
if (fragment.string.empty()) {
224+
fragment.string = BaseTextShadowNode::getEmptyPlaceholder();
220225
}
221226

222-
// Yoga expects a baseline relative to the Node's border-box edge instead of
223-
// the content, so we need to adjust by the padding and border widths, which
224-
// have already been set by the time of baseline alignment
225-
auto top = YGNodeLayoutGetBorder(&yogaNode_, YGEdgeTop) +
226-
YGNodeLayoutGetPadding(&yogaNode_, YGEdgeTop);
227+
auto textAttributes = TextAttributes::defaultTextAttributes();
228+
textAttributes.apply(getConcreteProps().textAttributes);
227229

228-
AttributedStringBox attributedStringBox{attributedString};
229-
return textLayoutManager_->baseline(
230-
attributedStringBox,
231-
getConcreteProps().paragraphAttributes,
232-
size) +
233-
top;
234-
}
230+
// If there's no text, it's possible that this Fragment isn't actually
231+
// appended to the AttributedString (see implementation of appendFragment)
232+
fragment.textAttributes = textAttributes;
233+
fragment.parentShadowView = ShadowView(*this);
234+
textAttributedString.appendFragment(std::move(fragment));
235235

236-
void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) {
237-
updateStateIfNeeded();
238-
ConcreteViewShadowNode::layout(layoutContext);
236+
return textAttributedString;
239237
}
240238

241239
} // namespace facebook::react

0 commit comments

Comments
 (0)