diff --git a/RNTester/js/TextInputExample.android.js b/RNTester/js/TextInputExample.android.js index b90424dc378..bf3afaf39e6 100644 --- a/RNTester/js/TextInputExample.android.js +++ b/RNTester/js/TextInputExample.android.js @@ -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 ( - { - 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 ( + + Width: + this.setState({width: value})} + /> + Multiline: + this.setState({multiline: value})} + /> + TextInput: + this.setState({text: value})} + {...props} + /> + Plain text value representation: + {this.state.text} + + ); + } +} + 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 ( ); @@ -613,12 +641,22 @@ exports.examples = [ render: function() { return ( - + multiline={true} + style={{maxHeight: 400, minHeight: 20, backgroundColor: '#eeeeee'}} + > + generic generic generic + + small small small small small small + + regular regular + + huge huge huge huge huge + + generic generic generic + ); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 19aa1d589b6..7b06e8897d1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -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 diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java new file mode 100644 index 00000000000..1740a68004b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java @@ -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); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java index 9fb09471e1f..3fac6104ec0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java @@ -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")