AndroidTextInput: Fix stale measurements based on outdated State

Summary:
Fix initial width measurement of AndroidTextInput.

The lifecycle here between measure and layout is a little wacky. I put this in comments too, but:

1. Measure is called first. It's marked as const so it can't call updateStateIfNeeded.
2. Layout is called immediately after. It's not const so it calls updateStateIfNeeded.
3. The state is updated, but it's not part of a commit, it just mutates the node in-place.
4. If the node isn't dirtied again, measure won't be called again.

For completeness: I did try calling `dirtyLayout` in the `layout` method. That does not work.

Changelog: [Internal]

Reviewed By: mdvacca

Differential Revision: D19549803

fbshipit-source-id: f3798e10dca2edacb364cc5b53f58f091de5e4d0
This commit is contained in:
Joshua Gross
2020-01-23 19:27:57 -08:00
committed by Facebook Github Bot
parent 27d61388bd
commit e689462782
2 changed files with 36 additions and 15 deletions
@@ -88,6 +88,18 @@ void AndroidTextInputShadowNode::setTextLayoutManager(
textLayoutManager_ = textLayoutManager;
}
AttributedString AndroidTextInputShadowNode::getMostRecentAttributedString()
const {
auto const &state = getStateData();
auto reactTreeAttributedString = getAttributedString();
return (
state.reactTreeAttributedString == reactTreeAttributedString
? state.attributedString
: reactTreeAttributedString);
}
void AndroidTextInputShadowNode::updateStateIfNeeded() {
ensureUnsealed();
@@ -115,32 +127,36 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() {
auto defaultTextAttributes = TextAttributes::defaultTextAttributes();
defaultTextAttributes.apply(getProps()->textAttributes);
auto newEventCount =
(state.reactTreeAttributedString == reactTreeAttributedString
? 0
: getProps()->mostRecentEventCount);
auto newAttributedString = getMostRecentAttributedString();
// Even if we're here and updating state, it may be only to update the layout
// manager If that is the case, make sure we don't update text: pass in the
// current attributedString unchanged, and pass in zero for the "event count"
// so no changes are applied There's no way to prevent a state update from
// flowing to Java, so we just ensure it's a noop in those cases.
setStateData(AndroidTextInputState{
(state.reactTreeAttributedString == reactTreeAttributedString
? 0
: getProps()->mostRecentEventCount),
(state.reactTreeAttributedString == reactTreeAttributedString
? state.attributedString
: reactTreeAttributedString),
reactTreeAttributedString,
getProps()->paragraphAttributes,
defaultTextAttributes,
ShadowView(*this),
textLayoutManager_});
setStateData(AndroidTextInputState{newEventCount,
newAttributedString,
reactTreeAttributedString,
getProps()->paragraphAttributes,
defaultTextAttributes,
ShadowView(*this),
textLayoutManager_});
}
#pragma mark - LayoutableShadowNode
Size AndroidTextInputShadowNode::measure(
LayoutConstraints layoutConstraints) const {
auto const &state = getStateData();
AttributedString attributedString = state.attributedString;
// Layout is called right after measure.
// Measure is marked as `const`, and `layout` is not; so State can be updated
// during layout, but not during `measure`. If State is out-of-date in layout,
// it's too late: measure will have already operated on old State. Thus, we
// use the same value here that we *will* use in layout to update the state.
AttributedString attributedString = getMostRecentAttributedString();
if (attributedString.isEmpty()) {
attributedString = getPlaceholderAttributedString();