Files
react-native/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp
T
Joshua Gross e689462782 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
2020-01-23 19:27:57 -08:00

182 lines
6.7 KiB
C++

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "AndroidTextInputShadowNode.h"
#include <fbjni/fbjni.h>
#include <react/attributedstring/AttributedStringBox.h>
#include <react/attributedstring/TextAttributes.h>
#include <react/components/text/BaseTextShadowNode.h>
#include <react/core/LayoutConstraints.h>
#include <react/core/LayoutContext.h>
#include <react/core/conversions.h>
#include <react/jni/ReadableNativeMap.h>
using namespace facebook::jni;
namespace facebook {
namespace react {
extern const char AndroidTextInputComponentName[] = "AndroidTextInput";
void AndroidTextInputShadowNode::setContextContainer(
ContextContainer *contextContainer) {
ensureUnsealed();
contextContainer_ = contextContainer;
}
AttributedString AndroidTextInputShadowNode::getAttributedString() const {
// Use BaseTextShadowNode to get attributed string from children
auto childTextAttributes = TextAttributes::defaultTextAttributes();
childTextAttributes.apply(getProps()->textAttributes);
auto attributedString =
BaseTextShadowNode::getAttributedString(childTextAttributes, *this);
// BaseTextShadowNode only gets children. We must detect and prepend text
// value attributes manually.
if (!getProps()->text.empty()) {
auto textAttributes = TextAttributes::defaultTextAttributes();
textAttributes.apply(getProps()->textAttributes);
auto fragment = AttributedString::Fragment{};
fragment.string = getProps()->text;
fragment.textAttributes = textAttributes;
// If the TextInput opacity is 0 < n < 1, the opacity of the TextInput and
// text value's background will stack. This is a hack/workaround to prevent
// that effect.
fragment.textAttributes.backgroundColor = clearColor();
fragment.parentShadowView = ShadowView(*this);
attributedString.prependFragment(fragment);
}
return attributedString;
}
// 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()
const {
// Return placeholder text, since text and children are empty.
auto textAttributedString = AttributedString{};
auto fragment = AttributedString::Fragment{};
fragment.string = getProps()->placeholder;
if (fragment.string.empty()) {
fragment.string = " ";
}
auto textAttributes = TextAttributes::defaultTextAttributes();
textAttributes.apply(getProps()->textAttributes);
// If there's no text, it's possible that this Fragment isn't actually
// appended to the AttributedString (see implementation of appendFragment)
fragment.textAttributes = textAttributes;
fragment.parentShadowView = ShadowView(*this);
textAttributedString.appendFragment(fragment);
return textAttributedString;
}
void AndroidTextInputShadowNode::setTextLayoutManager(
SharedTextLayoutManager textLayoutManager) {
ensureUnsealed();
textLayoutManager_ = textLayoutManager;
}
AttributedString AndroidTextInputShadowNode::getMostRecentAttributedString()
const {
auto const &state = getStateData();
auto reactTreeAttributedString = getAttributedString();
return (
state.reactTreeAttributedString == reactTreeAttributedString
? state.attributedString
: reactTreeAttributedString);
}
void AndroidTextInputShadowNode::updateStateIfNeeded() {
ensureUnsealed();
auto reactTreeAttributedString = getAttributedString();
auto const &state = getStateData();
assert(textLayoutManager_);
assert(
(!state.layoutManager || state.layoutManager == textLayoutManager_) &&
"`StateData` refers to a different `TextLayoutManager`");
// 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;
}
// Store default TextAttributes in state.
// In the case where the TextInput is completely empty (no value, no
// defaultValue, no placeholder, no children) there are therefore no fragments
// in the AttributedString, and when State is updated, it needs some way to
// reconstruct a Fragment with default TextAttributes.
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{newEventCount,
newAttributedString,
reactTreeAttributedString,
getProps()->paragraphAttributes,
defaultTextAttributes,
ShadowView(*this),
textLayoutManager_});
}
#pragma mark - LayoutableShadowNode
Size AndroidTextInputShadowNode::measure(
LayoutConstraints layoutConstraints) const {
// 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();
}
if (attributedString.isEmpty()) {
return {0, 0};
}
return textLayoutManager_->measure(
AttributedStringBox{attributedString},
getProps()->paragraphAttributes,
layoutConstraints);
}
void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) {
updateStateIfNeeded();
ConcreteViewShadowNode::layout(layoutContext);
}
} // namespace react
} // namespace facebook