mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
5be86695a3
Summary: For backwards-compatibility with Paper, we're implementing a feature in Fabric that will allow TextInputs to use the default padding from the theme in Android. Note that this uses some pretty ugly hacks that probably shouldn't be used inside of components at all: looking directly at rawProps, overriding props/Yoga styles in the component descriptor, etc. I would (personally) really like to kill this feature entirely unless and until we can find a more elegant solution. Changelog: [Internal] TextInputs are still not pixel-perfect with Paper, but they're much closer, and the underline visual glitchiness is no longer an issue. Reviewed By: mdvacca Differential Revision: D20109605 fbshipit-source-id: 543282843e0a9f03a504d72d7a014431099bd64c
186 lines
7.0 KiB
C++
186 lines
7.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 <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(getConcreteProps().textAttributes);
|
|
auto attributedString =
|
|
BaseTextShadowNode::getAttributedString(childTextAttributes, *this);
|
|
|
|
// BaseTextShadowNode only gets children. We must detect and prepend text
|
|
// value attributes manually.
|
|
if (!getConcreteProps().text.empty()) {
|
|
auto textAttributes = TextAttributes::defaultTextAttributes();
|
|
textAttributes.apply(getConcreteProps().textAttributes);
|
|
auto fragment = AttributedString::Fragment{};
|
|
fragment.string = getConcreteProps().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 = getConcreteProps().placeholder;
|
|
|
|
if (fragment.string.empty()) {
|
|
fragment.string = " ";
|
|
}
|
|
|
|
auto textAttributes = TextAttributes::defaultTextAttributes();
|
|
textAttributes.apply(getConcreteProps().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(getConcreteProps().textAttributes);
|
|
|
|
auto newEventCount =
|
|
(state.reactTreeAttributedString == reactTreeAttributedString
|
|
? 0
|
|
: getConcreteProps().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,
|
|
getConcreteProps().paragraphAttributes,
|
|
defaultTextAttributes,
|
|
ShadowView(*this),
|
|
textLayoutManager_,
|
|
state.defaultThemePaddingStart,
|
|
state.defaultThemePaddingEnd,
|
|
state.defaultThemePaddingTop,
|
|
state.defaultThemePaddingBottom});
|
|
}
|
|
|
|
#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},
|
|
getConcreteProps().paragraphAttributes,
|
|
layoutConstraints);
|
|
}
|
|
|
|
void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) {
|
|
updateStateIfNeeded();
|
|
ConcreteViewShadowNode::layout(layoutContext);
|
|
}
|
|
|
|
} // namespace react
|
|
} // namespace facebook
|