Skip to content

Commit

Permalink
Android: Add support for having borders on <Text> & <TextInput> compo…
Browse files Browse the repository at this point in the history
…nents

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 #9658

Differential Revision: D3819993

Pulled By: lexs

fbshipit-source-id: 183b0aa95369dd781f03b5a1f0f409ab47284e39
  • Loading branch information
Adam Comella authored and Facebook Github Bot 4 committed Sep 8, 2016
1 parent afde9da commit 28ba749
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 2 deletions.
Expand Up @@ -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',
Expand Down
Expand Up @@ -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;
Expand All @@ -21,6 +23,7 @@
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;

Expand All @@ -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 =
Expand Down Expand Up @@ -204,6 +209,15 @@ public void onFinishTemporaryDetach() {
}
}

@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;
Expand Down Expand Up @@ -233,4 +247,41 @@ public void updateView() {
@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;
}
}
Expand Up @@ -16,13 +16,17 @@
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}.
Expand All @@ -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;
Expand Down Expand Up @@ -85,7 +93,54 @@ public void setTextAlignVertical(ReactTextView view, @Nullable String textAlignV
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;
Expand Down
Expand Up @@ -14,9 +14,11 @@
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;
Expand All @@ -39,6 +41,7 @@
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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -478,6 +483,52 @@ public void onFinishTemporaryDetach() {
}
}

@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.
Expand Down
Expand Up @@ -28,6 +28,8 @@
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;
Expand All @@ -42,10 +44,12 @@
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;

/**
Expand All @@ -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;

Expand Down Expand Up @@ -497,6 +505,53 @@ public void setReturnKeyLabel(ReactEditText view, String returnKeyLabel) {
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);
Expand Down
Expand Up @@ -43,7 +43,7 @@
* {@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;
Expand Down
Expand Up @@ -38,6 +38,7 @@
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;
Expand Down Expand Up @@ -343,7 +344,7 @@ public void testBackgroundColorStyleApplied() {
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
Expand Down

0 comments on commit 28ba749

Please sign in to comment.