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:
Valentin Shergin
2017-10-02 16:29:44 -07:00
committed by Facebook Github Bot
parent d0790fea39
commit c550f27a4e
4 changed files with 191 additions and 69 deletions

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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")