Android: Add support for having borders on <Text> & <TextInput> components

Summary:
Currently, `<Text>` and `<TextInput>` components on Android do not support borders.
This change adds support for the borderRadius, borderColor, and
borderWidth props on the `<Text>` and `<TextInput>` components on Android.

ReactViewGroup already implements this functionality so
we copied its implementation over into the ReactTextView
and ReactEditText classes.

**Test plan (required)**

Verified that the various border props work on Text and TextInput components in a test app.

Adam Comella
Microsoft Corp.
Closes https://github.com/facebook/react-native/pull/9658

Differential Revision: D3819993

Pulled By: lexs

fbshipit-source-id: 183b0aa95369dd781f03b5a1f0f409ab47284e39
This commit is contained in:
Adam Comella 2016-09-08 02:55:42 -07:00 committed by Facebook Github Bot 4
parent afde9da93d
commit 28ba749ba0
7 changed files with 216 additions and 2 deletions

View File

@ -11,6 +11,7 @@ android_library(
react_native_target('java/com/facebook/react/common:common'),
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),
react_native_target('java/com/facebook/react/views/view:view'),
],
visibility = [
'PUBLIC',

View File

@ -10,7 +10,9 @@
package com.facebook.react.views.text;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextUtils;
@ -21,6 +23,7 @@ import android.widget.TextView;
import com.facebook.csslayout.FloatUtil;
import com.facebook.react.uimanager.ReactCompoundView;
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
import javax.annotation.Nullable;
@ -38,6 +41,8 @@ public class ReactTextView extends TextView implements ReactCompoundView {
private int mNumberOfLines = ViewDefaults.NUMBER_OF_LINES;
private TextUtils.TruncateAt mEllipsizeLocation = TextUtils.TruncateAt.END;
private ReactViewBackgroundDrawable mReactBackgroundDrawable;
public ReactTextView(Context context) {
super(context);
mDefaultGravityHorizontal =
@ -204,6 +209,15 @@ public class ReactTextView extends TextView implements ReactCompoundView {
}
}
@Override
public void setBackgroundColor(int color) {
if (color == Color.TRANSPARENT && mReactBackgroundDrawable == null) {
// don't do anything, no need to allocate ReactBackgroundDrawable for transparent background
} else {
getOrCreateReactViewBackground().setColor(color);
}
}
/* package */ void setGravityHorizontal(int gravityHorizontal) {
if (gravityHorizontal == 0) {
gravityHorizontal = mDefaultGravityHorizontal;
@ -233,4 +247,41 @@ public class ReactTextView extends TextView implements ReactCompoundView {
@Nullable TextUtils.TruncateAt ellipsizeLocation = mNumberOfLines == ViewDefaults.NUMBER_OF_LINES ? null : mEllipsizeLocation;
setEllipsize(ellipsizeLocation);
}
public void setBorderWidth(int position, float width) {
getOrCreateReactViewBackground().setBorderWidth(position, width);
}
public void setBorderColor(int position, float color, float alpha) {
getOrCreateReactViewBackground().setBorderColor(position, color, alpha);
}
public void setBorderRadius(float borderRadius) {
getOrCreateReactViewBackground().setRadius(borderRadius);
}
public void setBorderRadius(float borderRadius, int position) {
getOrCreateReactViewBackground().setRadius(borderRadius, position);
}
public void setBorderStyle(@Nullable String style) {
getOrCreateReactViewBackground().setBorderStyle(style);
}
private ReactViewBackgroundDrawable getOrCreateReactViewBackground() {
if (mReactBackgroundDrawable == null) {
mReactBackgroundDrawable = new ReactViewBackgroundDrawable();
Drawable backgroundDrawable = getBackground();
super.setBackground(null); // required so that drawable callback is cleared before we add the
// drawable back as a part of LayerDrawable
if (backgroundDrawable == null) {
super.setBackground(mReactBackgroundDrawable);
} else {
LayerDrawable layerDrawable =
new LayerDrawable(new Drawable[]{mReactBackgroundDrawable, backgroundDrawable});
super.setBackground(layerDrawable);
}
}
return mReactBackgroundDrawable;
}
}

View File

@ -16,13 +16,17 @@ import android.text.TextUtils;
import android.view.Gravity;
import android.widget.TextView;
import com.facebook.csslayout.CSSConstants;
import com.facebook.csslayout.Spacing;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.uimanager.BaseViewManager;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
/**
* Manages instances of spannable {@link TextView}.
@ -37,6 +41,10 @@ public class ReactTextViewManager extends BaseViewManager<ReactTextView, ReactTe
@VisibleForTesting
public static final String REACT_CLASS = "RCTText";
private static final int[] SPACING_TYPES = {
Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM,
};
@Override
public String getName() {
return REACT_CLASS;
@ -85,7 +93,54 @@ public class ReactTextViewManager extends BaseViewManager<ReactTextView, ReactTe
public void setSelectable(ReactTextView view, boolean isSelectable) {
view.setTextIsSelectable(isSelectable);
}
@ReactPropGroup(names = {
ViewProps.BORDER_RADIUS,
ViewProps.BORDER_TOP_LEFT_RADIUS,
ViewProps.BORDER_TOP_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_LEFT_RADIUS
}, defaultFloat = CSSConstants.UNDEFINED)
public void setBorderRadius(ReactTextView view, int index, float borderRadius) {
if (!CSSConstants.isUndefined(borderRadius)) {
borderRadius = PixelUtil.toPixelFromDIP(borderRadius);
}
if (index == 0) {
view.setBorderRadius(borderRadius);
} else {
view.setBorderRadius(borderRadius, index - 1);
}
}
@ReactProp(name = "borderStyle")
public void setBorderStyle(ReactTextView view, @Nullable String borderStyle) {
view.setBorderStyle(borderStyle);
}
@ReactPropGroup(names = {
ViewProps.BORDER_WIDTH,
ViewProps.BORDER_LEFT_WIDTH,
ViewProps.BORDER_RIGHT_WIDTH,
ViewProps.BORDER_TOP_WIDTH,
ViewProps.BORDER_BOTTOM_WIDTH,
}, defaultFloat = CSSConstants.UNDEFINED)
public void setBorderWidth(ReactTextView view, int index, float width) {
if (!CSSConstants.isUndefined(width)) {
width = PixelUtil.toPixelFromDIP(width);
}
view.setBorderWidth(SPACING_TYPES[index], width);
}
@ReactPropGroup(names = {
"borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor"
}, customType = "Color")
public void setBorderColor(ReactTextView view, int index, Integer color) {
float rgbComponent = color == null ? CSSConstants.UNDEFINED : (float) ((int)color & 0x00FFFFFF);
float alphaComponent = color == null ? CSSConstants.UNDEFINED : (float) ((int)color >>> 24);
view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent);
}
@Override
public void updateExtraData(ReactTextView view, Object extraData) {
ReactTextUpdate update = (ReactTextUpdate) extraData;

View File

@ -14,9 +14,11 @@ import javax.annotation.Nullable;
import java.util.ArrayList;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableStringBuilder;
@ -39,6 +41,7 @@ import com.facebook.react.views.text.CustomStyleSpan;
import com.facebook.react.views.text.ReactTagSpan;
import com.facebook.react.views.text.ReactTextUpdate;
import com.facebook.react.views.text.TextInlineImageSpan;
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
/**
* A wrapper around the EditText that lets us better control what happens when an EditText gets
@ -76,6 +79,8 @@ public class ReactEditText extends EditText {
private final InternalKeyListener mKeyListener;
private boolean mDetectScrollMovement = false;
private ReactViewBackgroundDrawable mReactBackgroundDrawable;
private static final KeyListener sKeyListener = QwertyKeyListener.getInstanceForFullKeyboard();
public ReactEditText(Context context) {
@ -478,6 +483,52 @@ public class ReactEditText extends EditText {
}
}
@Override
public void setBackgroundColor(int color) {
if (color == Color.TRANSPARENT && mReactBackgroundDrawable == null) {
// don't do anything, no need to allocate ReactBackgroundDrawable for transparent background
} else {
getOrCreateReactViewBackground().setColor(color);
}
}
public void setBorderWidth(int position, float width) {
getOrCreateReactViewBackground().setBorderWidth(position, width);
}
public void setBorderColor(int position, float color, float alpha) {
getOrCreateReactViewBackground().setBorderColor(position, color, alpha);
}
public void setBorderRadius(float borderRadius) {
getOrCreateReactViewBackground().setRadius(borderRadius);
}
public void setBorderRadius(float borderRadius, int position) {
getOrCreateReactViewBackground().setRadius(borderRadius, position);
}
public void setBorderStyle(@Nullable String style) {
getOrCreateReactViewBackground().setBorderStyle(style);
}
private ReactViewBackgroundDrawable getOrCreateReactViewBackground() {
if (mReactBackgroundDrawable == null) {
mReactBackgroundDrawable = new ReactViewBackgroundDrawable();
Drawable backgroundDrawable = getBackground();
super.setBackground(null); // required so that drawable callback is cleared before we add the
// drawable back as a part of LayerDrawable
if (backgroundDrawable == null) {
super.setBackground(mReactBackgroundDrawable);
} else {
LayerDrawable layerDrawable =
new LayerDrawable(new Drawable[]{mReactBackgroundDrawable, backgroundDrawable});
super.setBackground(layerDrawable);
}
}
return mReactBackgroundDrawable;
}
/**
* This class will redirect *TextChanged calls to the listeners only in the case where the text
* is changed by the user, and not explicitly set by JS.

View File

@ -28,6 +28,8 @@ import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.TextView;
import com.facebook.csslayout.CSSConstants;
import com.facebook.csslayout.Spacing;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactContext;
@ -42,10 +44,12 @@ import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
import com.facebook.react.views.text.DefaultStyleValuesUtil;
import com.facebook.react.views.text.ReactTextUpdate;
import com.facebook.react.views.text.ReactTextView;
import com.facebook.react.views.text.TextInlineImageSpan;
/**
@ -55,6 +59,10 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
/* package */ static final String REACT_CLASS = "AndroidTextInput";
private static final int[] SPACING_TYPES = {
Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM,
};
private static final int FOCUS_TEXT_INPUT = 1;
private static final int BLUR_TEXT_INPUT = 2;
@ -497,6 +505,53 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
view.setImeActionLabel(returnKeyLabel, IME_ACTION_ID);
}
@ReactPropGroup(names = {
ViewProps.BORDER_RADIUS,
ViewProps.BORDER_TOP_LEFT_RADIUS,
ViewProps.BORDER_TOP_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_LEFT_RADIUS
}, defaultFloat = CSSConstants.UNDEFINED)
public void setBorderRadius(ReactEditText view, int index, float borderRadius) {
if (!CSSConstants.isUndefined(borderRadius)) {
borderRadius = PixelUtil.toPixelFromDIP(borderRadius);
}
if (index == 0) {
view.setBorderRadius(borderRadius);
} else {
view.setBorderRadius(borderRadius, index - 1);
}
}
@ReactProp(name = "borderStyle")
public void setBorderStyle(ReactEditText view, @Nullable String borderStyle) {
view.setBorderStyle(borderStyle);
}
@ReactPropGroup(names = {
ViewProps.BORDER_WIDTH,
ViewProps.BORDER_LEFT_WIDTH,
ViewProps.BORDER_RIGHT_WIDTH,
ViewProps.BORDER_TOP_WIDTH,
ViewProps.BORDER_BOTTOM_WIDTH,
}, defaultFloat = CSSConstants.UNDEFINED)
public void setBorderWidth(ReactEditText view, int index, float width) {
if (!CSSConstants.isUndefined(width)) {
width = PixelUtil.toPixelFromDIP(width);
}
view.setBorderWidth(SPACING_TYPES[index], width);
}
@ReactPropGroup(names = {
"borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor"
}, customType = "Color")
public void setBorderColor(ReactEditText view, int index, Integer color) {
float rgbComponent = color == null ? CSSConstants.UNDEFINED : (float) ((int)color & 0x00FFFFFF);
float alphaComponent = color == null ? CSSConstants.UNDEFINED : (float) ((int)color >>> 24);
view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent);
}
@Override
protected void onAfterUpdateTransaction(ReactEditText view) {
super.onAfterUpdateTransaction(view);

View File

@ -43,7 +43,7 @@ import com.facebook.csslayout.Spacing;
* {@code mBorderWidthResult} and similar. When only background color is set we won't allocate any
* extra/unnecessary objects.
*/
/* package */ class ReactViewBackgroundDrawable extends Drawable {
public class ReactViewBackgroundDrawable extends Drawable {
private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
private static final int DEFAULT_BORDER_RGB = 0x00FFFFFF & DEFAULT_BORDER_COLOR;

View File

@ -38,6 +38,7 @@ import com.facebook.react.uimanager.UIImplementation;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
import org.junit.Before;
import org.junit.Rule;
@ -343,7 +344,7 @@ public class ReactTextTest {
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
Drawable backgroundDrawable = ((TextView) rootView.getChildAt(0)).getBackground();
assertThat(((ColorDrawable) backgroundDrawable).getColor()).isEqualTo(Color.BLUE);
assertThat(((ReactViewBackgroundDrawable) backgroundDrawable).getColor()).isEqualTo(Color.BLUE);
}
// JELLY_BEAN is needed for TextView#getMaxLines(), which is OK, because in the actual code we