mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
0556e86d09
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
151 lines
5.0 KiB
C++
151 lines
5.0 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 <fb/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);
|
|
|
|
// We know this is not empty, because we at least have the `text` value
|
|
return attributedString;
|
|
}
|
|
|
|
// No need to use placeholder if we have text at this point.
|
|
if (!attributedString.isEmpty()) {
|
|
return attributedString;
|
|
}
|
|
|
|
return getPlaceholderAttributedString(false);
|
|
}
|
|
|
|
// 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(
|
|
bool ensureMinimumLength) 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() && ensureMinimumLength) {
|
|
fragment.string = " ";
|
|
}
|
|
|
|
auto textAttributes = TextAttributes::defaultTextAttributes();
|
|
textAttributes.apply(getProps()->textAttributes);
|
|
|
|
fragment.textAttributes = textAttributes;
|
|
fragment.parentShadowView = ShadowView(*this);
|
|
textAttributedString.appendFragment(fragment);
|
|
return textAttributedString;
|
|
}
|
|
|
|
void AndroidTextInputShadowNode::setTextLayoutManager(
|
|
SharedTextLayoutManager textLayoutManager) {
|
|
ensureUnsealed();
|
|
textLayoutManager_ = textLayoutManager;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
setStateData(AndroidTextInputState{state.mostRecentEventCount,
|
|
reactTreeAttributedString,
|
|
reactTreeAttributedString,
|
|
getProps()->paragraphAttributes,
|
|
textLayoutManager_});
|
|
}
|
|
|
|
#pragma mark - LayoutableShadowNode
|
|
|
|
Size AndroidTextInputShadowNode::measure(
|
|
LayoutConstraints layoutConstraints) const {
|
|
auto const &state = getStateData();
|
|
|
|
AttributedString attributedString = state.attributedString;
|
|
|
|
if (attributedString.isEmpty()) {
|
|
attributedString = getPlaceholderAttributedString(true);
|
|
}
|
|
|
|
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
|