Skip to content

Commit 9b646c8

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Fix incorrect height of single line TextInput without definite size (facebook#48523)
Summary: Pull Request resolved: facebook#48523 Current AndroidTextInputShadowNode logic measures the height of the TextInput by fitting text into the constraints of the TextInput box. This results in the wrong height for single line TextInputs, since a single line TextInput is infinitely horizontally scrollable (whearas the outer TextInput component itself has a fixed width). After this change, we measure text under single line textinputs with an infinite width constraint, then clamp to the final constraints of the TextInput, to better emulate what is happening under the hood. iOS ended up solving this in a slightly different way, by measuring paragraph with `maximumNumberOfLines={1}` when not multiline, but think this is a bit more fraught. E.g. up until recently, it would have meant that the width could have been less than max width, depending on where line-breaking happened. I ended up duplicating the new logic to use for both instead (D66914447 will eventually deduplicate). Changelog: [Android][Fixed] - Fix incorrect height of single line TextInputs without definite size Reviewed By: christophpurrer Differential Revision: D67916827 fbshipit-source-id: b827185c4640835481794cb985c2b62dcf643abe
1 parent 8310d65 commit 9b646c8

File tree

5 files changed

+85
-40
lines changed

5 files changed

+85
-40
lines changed

packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -234,17 +234,6 @@ TextAttributes BaseTextInputProps::getEffectiveTextAttributes(
234234
return result;
235235
}
236236

237-
ParagraphAttributes BaseTextInputProps::getEffectiveParagraphAttributes()
238-
const {
239-
auto result = paragraphAttributes;
240-
241-
if (!multiline) {
242-
result.maximumNumberOfLines = 1;
243-
}
244-
245-
return result;
246-
}
247-
248237
SubmitBehavior BaseTextInputProps::getNonDefaultSubmitBehavior() const {
249238
if (submitBehavior == SubmitBehavior::Default) {
250239
return multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ class BaseTextInputProps : public ViewProps, public BaseTextProps {
3838
*/
3939
TextAttributes getEffectiveTextAttributes(Float fontSizeMultiplier) const;
4040

41-
ParagraphAttributes getEffectiveParagraphAttributes() const;
42-
4341
#pragma mark - Props
4442

4543
/*

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

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,19 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<
7272
const LayoutContext& layoutContext,
7373
const LayoutConstraints& layoutConstraints) const override {
7474
const auto& props = BaseShadowNode::getConcreteProps();
75+
auto textConstraints = getTextConstraints(layoutConstraints);
76+
7577
TextLayoutContext textLayoutContext{
7678
.pointScaleFactor = layoutContext.pointScaleFactor};
77-
return textLayoutManager_
78-
->measure(
79-
attributedStringBoxToMeasure(layoutContext),
80-
props.getEffectiveParagraphAttributes(),
81-
textLayoutContext,
82-
layoutConstraints)
83-
.size;
79+
auto textSize = textLayoutManager_
80+
->measure(
81+
attributedStringBoxToMeasure(layoutContext),
82+
props.paragraphAttributes,
83+
textLayoutContext,
84+
textConstraints)
85+
.size;
86+
87+
return layoutConstraints.clamp(textSize);
8488
}
8589

8690
void layout(LayoutContext layoutContext) override {
@@ -112,9 +116,7 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<
112116

113117
AttributedStringBox attributedStringBox{attributedString};
114118
return textLayoutManager_->baseline(
115-
attributedStringBox,
116-
props.getEffectiveParagraphAttributes(),
117-
size) +
119+
attributedStringBox, props.paragraphAttributes, size) +
118120
top;
119121
}
120122

@@ -166,6 +168,30 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<
166168
return attributedString;
167169
}
168170

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+
169195
/*
170196
* Returns an `AttributedStringBox` which represents text content that should
171197
* be used for measuring purposes. It might contain actual text value,

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

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,27 @@ AttributedString AndroidTextInputShadowNode::getMostRecentAttributedString()
108108
: reactTreeAttributedString);
109109
}
110110

111+
LayoutConstraints AndroidTextInputShadowNode::getTextConstraints(
112+
const LayoutConstraints& layoutConstraints) const {
113+
if (getConcreteProps().multiline) {
114+
return layoutConstraints;
115+
} else {
116+
// A single line TextInput acts as a horizontal scroller of infinitely
117+
// expandable text, so we want to measure the text as if it is allowed to
118+
// infinitely expand horizontally, and later clamp to the constraints of the
119+
// input.
120+
return LayoutConstraints{
121+
.minimumSize = layoutConstraints.minimumSize,
122+
.maximumSize =
123+
Size{
124+
.width = std::numeric_limits<Float>::infinity(),
125+
.height = layoutConstraints.maximumSize.height,
126+
},
127+
.layoutDirection = layoutConstraints.layoutDirection,
128+
};
129+
}
130+
}
131+
111132
void AndroidTextInputShadowNode::updateStateIfNeeded() {
112133
ensureUnsealed();
113134

@@ -149,20 +170,24 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() {
149170
Size AndroidTextInputShadowNode::measureContent(
150171
const LayoutContext& layoutContext,
151172
const LayoutConstraints& layoutConstraints) const {
173+
auto textConstraints = getTextConstraints(layoutConstraints);
174+
152175
if (getStateData().cachedAttributedStringId != 0) {
153-
return textLayoutManager_
154-
->measureCachedSpannableById(
155-
getStateData().cachedAttributedStringId,
156-
getConcreteProps().paragraphAttributes,
157-
layoutConstraints)
158-
.size;
176+
auto textSize = textLayoutManager_
177+
->measureCachedSpannableById(
178+
getStateData().cachedAttributedStringId,
179+
getConcreteProps().paragraphAttributes,
180+
textConstraints)
181+
.size;
182+
return layoutConstraints.clamp(textSize);
159183
}
160184

161185
// Layout is called right after measure.
162-
// Measure is marked as `const`, and `layout` is not; so State can be updated
163-
// during layout, but not during `measure`. If State is out-of-date in layout,
164-
// it's too late: measure will have already operated on old State. Thus, we
165-
// use the same value here that we *will* use in layout to update the state.
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.
166191
AttributedString attributedString = getMostRecentAttributedString();
167192

168193
if (attributedString.isEmpty()) {
@@ -175,13 +200,14 @@ Size AndroidTextInputShadowNode::measureContent(
175200

176201
TextLayoutContext textLayoutContext;
177202
textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor;
178-
return textLayoutManager_
179-
->measure(
180-
AttributedStringBox{attributedString},
181-
getConcreteProps().paragraphAttributes,
182-
textLayoutContext,
183-
layoutConstraints)
184-
.size;
203+
auto textSize = textLayoutManager_
204+
->measure(
205+
AttributedStringBox{attributedString},
206+
getConcreteProps().paragraphAttributes,
207+
textLayoutContext,
208+
textConstraints)
209+
.size;
210+
return layoutConstraints.clamp(textSize);
185211
}
186212

187213
Float AndroidTextInputShadowNode::baseline(

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ class AndroidTextInputShadowNode final
6969
*/
7070
AttributedString getMostRecentAttributedString() const;
7171

72+
/*
73+
* Determines the constraints to use while measure the underlying text
74+
*/
75+
LayoutConstraints getTextConstraints(
76+
const LayoutConstraints& layoutConstraints) const;
77+
7278
/*
7379
* Creates a `State` object (with `AttributedText` and
7480
* `TextLayoutManager`) if needed.

0 commit comments

Comments
 (0)