Intrinsic content size for ReactTextInput (aka autoexpandable <TextInput> on Android)
Summary: After this diff the intrinsic content size of <TextInput> reflects the size of text inside EditText, it means that if there is no additional style constraints, <TextInput> will grow with containing text. If you want to constraint minimum or maximum height, just do it via Yoga styling. Reviewed By: achen1 Differential Revision: D5828366 fbshipit-source-id: eccd0cb4ccf724c7096c947332a64a0a1e402673
This commit is contained in:
parent
d0790fea39
commit
c550f27a4e
|
@ -18,6 +18,8 @@ var {
|
|||
TextInput,
|
||||
View,
|
||||
StyleSheet,
|
||||
Slider,
|
||||
Switch,
|
||||
} = ReactNative;
|
||||
|
||||
class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
|
||||
|
@ -70,27 +72,6 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
|
|||
}
|
||||
}
|
||||
|
||||
class AutoExpandingTextInput extends React.Component<$FlowFixMeProps, $FlowFixMeState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
height: 0,
|
||||
};
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<TextInput
|
||||
{...this.props}
|
||||
multiline={true}
|
||||
onContentSizeChange={(event) => {
|
||||
this.setState({height: event.nativeEvent.contentSize.height});
|
||||
}}
|
||||
style={[styles.default, {height: Math.min(200, Math.max(35, this.state.height))}]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RewriteExample extends React.Component<$FlowFixMeProps, $FlowFixMeState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -330,12 +311,59 @@ class SelectionExample extends React.Component<$FlowFixMeProps, SelectionExample
|
|||
}
|
||||
}
|
||||
|
||||
class AutogrowingTextInputExample extends React.Component<{}> {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
width: 100,
|
||||
multiline: true,
|
||||
text: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidReceiveProps(props) {
|
||||
this.setState({
|
||||
multiline: props.multiline,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
var {style, multiline, ...props} = this.props;
|
||||
return (
|
||||
<View>
|
||||
<Text>Width:</Text>
|
||||
<Slider
|
||||
value={100}
|
||||
minimumValue={0}
|
||||
maximumValue={100}
|
||||
step={10}
|
||||
onValueChange={(value) => this.setState({width: value})}
|
||||
/>
|
||||
<Text>Multiline:</Text>
|
||||
<Switch
|
||||
value={this.state.multiline}
|
||||
onValueChange={(value) => this.setState({multiline: value})}
|
||||
/>
|
||||
<Text>TextInput:</Text>
|
||||
<TextInput
|
||||
multiline={this.state.multiline}
|
||||
style={[style, {width: this.state.width + '%'}]}
|
||||
onChangeText={(value) => this.setState({text: value})}
|
||||
{...props}
|
||||
/>
|
||||
<Text>Plain text value representation:</Text>
|
||||
<Text>{this.state.text}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
multiline: {
|
||||
height: 60,
|
||||
fontSize: 16,
|
||||
padding: 4,
|
||||
marginBottom: 10,
|
||||
},
|
||||
eventLabel: {
|
||||
margin: 3,
|
||||
|
@ -343,7 +371,6 @@ var styles = StyleSheet.create({
|
|||
},
|
||||
singleLine: {
|
||||
fontSize: 16,
|
||||
padding: 4,
|
||||
},
|
||||
singleLineWithHeightTextInput: {
|
||||
height: 30,
|
||||
|
@ -363,7 +390,8 @@ exports.examples = [
|
|||
return (
|
||||
<TextInput
|
||||
autoFocus={true}
|
||||
style={styles.singleLine}
|
||||
multiline={true}
|
||||
style={styles.input}
|
||||
accessibilityLabel="I am the accessibility label for text input"
|
||||
/>
|
||||
);
|
||||
|
@ -613,12 +641,22 @@ exports.examples = [
|
|||
render: function() {
|
||||
return (
|
||||
<View>
|
||||
<AutoExpandingTextInput
|
||||
placeholder="height increases with content"
|
||||
defaultValue="React Native enables you to build world-class application experiences on native platforms using a consistent developer experience based on JavaScript and React. The focus of React Native is on developer efficiency across all the platforms you care about — learn once, write anywhere. Facebook uses React Native in multiple production apps and will continue investing in React Native."
|
||||
<AutogrowingTextInputExample
|
||||
enablesReturnKeyAutomatically={true}
|
||||
returnKeyType="done"
|
||||
/>
|
||||
multiline={true}
|
||||
style={{maxHeight: 400, minHeight: 20, backgroundColor: '#eeeeee'}}
|
||||
>
|
||||
generic generic generic
|
||||
<Text style={{fontSize: 6, color: 'red'}}>
|
||||
small small small small small small
|
||||
</Text>
|
||||
<Text>regular regular</Text>
|
||||
<Text style={{fontSize: 30, color: 'green'}}>
|
||||
huge huge huge huge huge
|
||||
</Text>
|
||||
generic generic generic
|
||||
</AutogrowingTextInputExample>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,8 @@ import android.view.inputmethod.InputConnection;
|
|||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
import com.facebook.react.views.text.CustomStyleSpan;
|
||||
import com.facebook.react.views.text.ReactTagSpan;
|
||||
import com.facebook.react.views.text.ReactTextUpdate;
|
||||
|
@ -129,9 +131,7 @@ public class ReactEditText extends EditText {
|
|||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
if (mContentSizeWatcher != null) {
|
||||
mContentSizeWatcher.onLayout();
|
||||
}
|
||||
onContentSizeChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -366,7 +366,9 @@ public class ReactEditText extends EditText {
|
|||
manageSpans(spannableStringBuilder);
|
||||
mContainsImages = reactTextUpdate.containsImages();
|
||||
mIsSettingTextFromJS = true;
|
||||
|
||||
getText().replace(0, length(), spannableStringBuilder);
|
||||
|
||||
mIsSettingTextFromJS = false;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (getBreakStrategy() != reactTextUpdate.getTextBreakStrategy()) {
|
||||
|
@ -446,6 +448,21 @@ public class ReactEditText extends EditText {
|
|||
return (getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
|
||||
}
|
||||
|
||||
private void onContentSizeChange() {
|
||||
if (mContentSizeWatcher != null) {
|
||||
mContentSizeWatcher.onLayout();
|
||||
}
|
||||
|
||||
setIntrinsicContentSize();
|
||||
}
|
||||
|
||||
private void setIntrinsicContentSize() {
|
||||
ReactContext reactContext = (ReactContext) getContext();
|
||||
UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
|
||||
final ReactTextInputLocalData localData = new ReactTextInputLocalData(this);
|
||||
uiManager.setViewLocalData(getId(), localData);
|
||||
}
|
||||
|
||||
/* package */ void setGravityHorizontal(int gravityHorizontal) {
|
||||
if (gravityHorizontal == 0) {
|
||||
gravityHorizontal = mDefaultGravityHorizontal;
|
||||
|
@ -621,9 +638,7 @@ public class ReactEditText extends EditText {
|
|||
}
|
||||
}
|
||||
|
||||
if (mContentSizeWatcher != null) {
|
||||
mContentSizeWatcher.onLayout();
|
||||
}
|
||||
onContentSizeChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.views.textinput;
|
||||
|
||||
import android.os.Build;
|
||||
import android.text.SpannableString;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.EditText;
|
||||
|
||||
/** Local state bearer for EditText instance. */
|
||||
public final class ReactTextInputLocalData {
|
||||
|
||||
private final SpannableString mText;
|
||||
private final float mTextSize;
|
||||
private final int mMinLines;
|
||||
private final int mMaxLines;
|
||||
private final int mInputType;
|
||||
private final int mBreakStrategy;
|
||||
|
||||
public ReactTextInputLocalData(EditText editText) {
|
||||
mText = new SpannableString(editText.getText());
|
||||
mTextSize = editText.getTextSize();
|
||||
mMinLines = editText.getMinLines();
|
||||
mMaxLines = editText.getMaxLines();
|
||||
mInputType = editText.getInputType();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mBreakStrategy = editText.getBreakStrategy();
|
||||
} else {
|
||||
mBreakStrategy = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void apply(EditText editText) {
|
||||
editText.setText(mText);
|
||||
editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
|
||||
editText.setMinLines(mMinLines);
|
||||
editText.setMaxLines(mMaxLines);
|
||||
editText.setInputType(mInputType);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
editText.setBreakStrategy(mBreakStrategy);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,19 +11,16 @@ package com.facebook.react.views.textinput;
|
|||
|
||||
import android.os.Build;
|
||||
import android.text.Layout;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.Spacing;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.UIViewOperationQueue;
|
||||
import com.facebook.react.uimanager.ViewDefaults;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.react.views.text.ReactTextShadowNode;
|
||||
import com.facebook.react.views.text.ReactBaseTextShadowNode;
|
||||
import com.facebook.react.views.text.ReactTextUpdate;
|
||||
import com.facebook.react.views.view.MeasureUtil;
|
||||
import com.facebook.yoga.YogaMeasureFunction;
|
||||
|
@ -33,19 +30,22 @@ import com.facebook.yoga.YogaNode;
|
|||
import javax.annotation.Nullable;
|
||||
|
||||
@VisibleForTesting
|
||||
public class ReactTextInputShadowNode extends ReactTextShadowNode implements
|
||||
YogaMeasureFunction {
|
||||
public class ReactTextInputShadowNode extends ReactBaseTextShadowNode
|
||||
implements YogaMeasureFunction {
|
||||
|
||||
private @Nullable EditText mEditText;
|
||||
private int mMostRecentEventCount = UNSET;
|
||||
private @Nullable EditText mDummyEditText;
|
||||
private @Nullable ReactTextInputLocalData mLocalData;
|
||||
|
||||
@VisibleForTesting public static final String PROP_TEXT = "text";
|
||||
|
||||
// Represents the {@code text} property only, not possible nested content.
|
||||
private @Nullable String mText = null;
|
||||
|
||||
public ReactTextInputShadowNode() {
|
||||
mTextBreakStrategy = (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ?
|
||||
0 : Layout.BREAK_STRATEGY_SIMPLE;
|
||||
|
||||
setMeasureFunction(this);
|
||||
}
|
||||
|
||||
|
@ -53,20 +53,30 @@ public class ReactTextInputShadowNode extends ReactTextShadowNode implements
|
|||
public void setThemedContext(ThemedReactContext themedContext) {
|
||||
super.setThemedContext(themedContext);
|
||||
|
||||
// TODO #7120264: cache this stuff better
|
||||
mEditText = new EditText(getThemedContext());
|
||||
// {@code EditText} has by default a border at the bottom of its view
|
||||
// called "underline". To have a native look and feel of the TextEdit
|
||||
// we have to preserve it at least by default.
|
||||
// The border (underline) has its padding set by the background image
|
||||
// provided by the system (which vary a lot among versions and vendors
|
||||
// of Android), and it cannot be changed.
|
||||
// So, we have to enforce it as a default padding.
|
||||
// TODO #7120264: Cache this stuff better.
|
||||
EditText editText = new EditText(getThemedContext());
|
||||
setDefaultPadding(Spacing.START, editText.getPaddingStart());
|
||||
setDefaultPadding(Spacing.TOP, editText.getPaddingTop());
|
||||
setDefaultPadding(Spacing.END, editText.getPaddingEnd());
|
||||
setDefaultPadding(Spacing.BOTTOM, editText.getPaddingBottom());
|
||||
|
||||
mDummyEditText = editText;
|
||||
|
||||
// We must measure the EditText without paddings, so we have to reset them.
|
||||
mDummyEditText.setPadding(0, 0, 0, 0);
|
||||
|
||||
// This is needed to fix an android bug since 4.4.3 which will throw an NPE in measure,
|
||||
// setting the layoutParams fixes it: https://code.google.com/p/android/issues/detail?id=75877
|
||||
mEditText.setLayoutParams(
|
||||
mDummyEditText.setLayoutParams(
|
||||
new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
setDefaultPadding(Spacing.START, mEditText.getPaddingStart());
|
||||
setDefaultPadding(Spacing.TOP, mEditText.getPaddingTop());
|
||||
setDefaultPadding(Spacing.END, mEditText.getPaddingEnd());
|
||||
setDefaultPadding(Spacing.BOTTOM, mEditText.getPaddingBottom());
|
||||
mEditText.setPadding(0, 0, 0, 0);
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -77,22 +87,14 @@ public class ReactTextInputShadowNode extends ReactTextShadowNode implements
|
|||
float height,
|
||||
YogaMeasureMode heightMode) {
|
||||
// measure() should never be called before setThemedContext()
|
||||
EditText editText = Assertions.assertNotNull(mEditText);
|
||||
EditText editText = Assertions.assertNotNull(mDummyEditText);
|
||||
|
||||
editText.setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX,
|
||||
mFontSize == UNSET ?
|
||||
(int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP)) : mFontSize);
|
||||
|
||||
if (mNumberOfLines != UNSET) {
|
||||
editText.setLines(mNumberOfLines);
|
||||
if (mLocalData == null) {
|
||||
// No local data, no intrinsic size.
|
||||
return YogaMeasureOutput.make(0, 0);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (editText.getBreakStrategy() != mTextBreakStrategy) {
|
||||
editText.setBreakStrategy(mTextBreakStrategy);
|
||||
}
|
||||
}
|
||||
mLocalData.apply(editText);
|
||||
|
||||
editText.measure(
|
||||
MeasureUtil.getMeasureSpec(width, widthMode),
|
||||
|
@ -102,9 +104,25 @@ public class ReactTextInputShadowNode extends ReactTextShadowNode implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeLayout() {
|
||||
// We don't have to measure the text within the text input.
|
||||
return;
|
||||
public boolean isVirtualAnchor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isYogaLeafNode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocalData(Object data) {
|
||||
Assertions.assertCondition(data instanceof ReactTextInputLocalData);
|
||||
mLocalData = (ReactTextInputLocalData) data;
|
||||
|
||||
// Telling to Yoga that the node should be remeasured on next layout pass.
|
||||
dirty();
|
||||
|
||||
// Note: We should NOT mark the node updated (by calling {@code markUpdated}) here
|
||||
// because the state remains the same.
|
||||
}
|
||||
|
||||
@ReactProp(name = "mostRecentEventCount")
|
||||
|
|
Loading…
Reference in New Issue