Implement partial rounded borders

Reviewed By: achen1

Differential Revision: D5982241

fbshipit-source-id: 2f694daca7e1b16b5ff65f07c7d15dd558a4b7e8
This commit is contained in:
Ramanpreet Nara 2017-10-18 19:29:31 -07:00 committed by Facebook Github Bot
parent de313f6fdd
commit 4994d6a389
2 changed files with 137 additions and 44 deletions

View File

@ -19,6 +19,7 @@ import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.os.Build;
import com.facebook.react.common.annotations.VisibleForTesting;
@ -30,15 +31,15 @@ import java.util.Locale;
import javax.annotation.Nullable;
/**
* A subclass of {@link Drawable} used for background of {@link ReactViewGroup}. It supports
* drawing background color and borders (including rounded borders) by providing a react friendly
* API (setter for each of those properties).
* A subclass of {@link Drawable} used for background of {@link ReactViewGroup}. It supports drawing
* background color and borders (including rounded borders) by providing a react friendly API
* (setter for each of those properties).
*
* The implementation tries to allocate as few objects as possible depending on which properties are
* set. E.g. for views with rounded background/borders we allocate {@code mPathForBorderRadius} and
* {@code mTempRectForBorderRadius}. In case when view have a rectangular borders we allocate
* {@code mBorderWidthResult} and similar. When only background color is set we won't allocate any
* extra/unnecessary objects.
* <p>The implementation tries to allocate as few objects as possible depending on which properties
* are set. E.g. for views with rounded background/borders we allocate {@code
* mInnerClipPathForBorderRadius} and {@code mInnerClipTempRectForBorderRadius}. In case when view
* have a rectangular borders we allocate {@code mBorderWidthResult} and similar. When only
* background color is set we won't allocate any extra/unnecessary objects.
*/
public class ReactViewBackgroundDrawable extends Drawable {
@ -83,10 +84,12 @@ public class ReactViewBackgroundDrawable extends Drawable {
/* Used for rounded border and rounded background */
private @Nullable PathEffect mPathEffectForBorderStyle;
private @Nullable Path mPathForBorderRadius;
private @Nullable Path mInnerClipPathForBorderRadius;
private @Nullable Path mOuterClipPathForBorderRadius;
private @Nullable Path mPathForBorderRadiusOutline;
private @Nullable Path mPathForBorder;
private @Nullable RectF mTempRectForBorderRadius;
private @Nullable RectF mInnerClipTempRectForBorderRadius;
private @Nullable RectF mOuterClipTempRectForBorderRadius;
private @Nullable RectF mTempRectForBorderRadiusOutline;
private boolean mNeedUpdatePathForBorderRadius = false;
private float mBorderRadius = YogaConstants.UNDEFINED;
@ -169,8 +172,13 @@ public class ReactViewBackgroundDrawable extends Drawable {
}
if (!FloatUtil.floatsEqual(mBorderWidth.getRaw(position), width)) {
mBorderWidth.set(position, width);
if (position == Spacing.ALL) {
mNeedUpdatePathForBorderRadius = true;
switch (position) {
case Spacing.ALL:
case Spacing.LEFT:
case Spacing.BOTTOM:
case Spacing.RIGHT:
case Spacing.TOP:
mNeedUpdatePathForBorderRadius = true;
}
invalidateSelf();
}
@ -266,44 +274,87 @@ public class ReactViewBackgroundDrawable extends Drawable {
private void drawRoundedBackgroundWithBorders(Canvas canvas) {
updatePath();
canvas.save();
int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha);
if (Color.alpha(useColor) != 0) { // color is not transparent
mPaint.setColor(useColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(mPathForBorderRadius, mPaint);
canvas.drawPath(mInnerClipPathForBorderRadius, mPaint);
}
// maybe draw borders?
float fullBorderWidth = getFullBorderWidth();
if (fullBorderWidth > 0) {
final float borderWidth = getBorderWidthOrDefaultTo(0, Spacing.ALL);
final float borderTopWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.TOP);
final float borderBottomWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.BOTTOM);
final float borderLeftWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.LEFT);
final float borderRightWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.RIGHT);
if (borderTopWidth > 0
|| borderBottomWidth > 0
|| borderLeftWidth > 0
|| borderRightWidth > 0) {
int borderColor = getFullBorderColor();
mPaint.setColor(ColorUtil.multiplyColorAlpha(borderColor, mAlpha));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(fullBorderWidth);
canvas.drawPath(mPathForBorderRadius, mPaint);
mPaint.setStyle(Paint.Style.FILL);
// Draw border
canvas.clipPath(mOuterClipPathForBorderRadius, Region.Op.INTERSECT);
canvas.clipPath(mInnerClipPathForBorderRadius, Region.Op.DIFFERENCE);
canvas.drawRect(getBounds(), mPaint);
}
canvas.restore();
}
private void updatePath() {
if (!mNeedUpdatePathForBorderRadius) {
return;
}
mNeedUpdatePathForBorderRadius = false;
if (mPathForBorderRadius == null) {
mPathForBorderRadius = new Path();
mTempRectForBorderRadius = new RectF();
if (mInnerClipPathForBorderRadius == null) {
mInnerClipPathForBorderRadius = new Path();
}
if (mOuterClipPathForBorderRadius == null) {
mOuterClipPathForBorderRadius = new Path();
}
if (mPathForBorderRadiusOutline == null) {
mPathForBorderRadiusOutline = new Path();
}
if (mInnerClipTempRectForBorderRadius == null) {
mInnerClipTempRectForBorderRadius = new RectF();
}
if (mOuterClipTempRectForBorderRadius == null) {
mOuterClipTempRectForBorderRadius = new RectF();
}
if (mTempRectForBorderRadiusOutline == null) {
mTempRectForBorderRadiusOutline = new RectF();
}
mPathForBorderRadius.reset();
mInnerClipPathForBorderRadius.reset();
mOuterClipPathForBorderRadius.reset();
mPathForBorderRadiusOutline.reset();
mTempRectForBorderRadius.set(getBounds());
mInnerClipTempRectForBorderRadius.set(getBounds());
mOuterClipTempRectForBorderRadius.set(getBounds());
mTempRectForBorderRadiusOutline.set(getBounds());
float fullBorderWidth = getFullBorderWidth();
if (fullBorderWidth > 0) {
mTempRectForBorderRadius.inset(fullBorderWidth * 0.5f, fullBorderWidth * 0.5f);
}
final float borderWidth = getBorderWidthOrDefaultTo(0, Spacing.ALL);
final float borderTopWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.TOP);
final float borderBottomWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.BOTTOM);
final float borderLeftWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.LEFT);
final float borderRightWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.RIGHT);
mInnerClipTempRectForBorderRadius.top += borderTopWidth;
mInnerClipTempRectForBorderRadius.bottom -= borderBottomWidth;
mInnerClipTempRectForBorderRadius.left += borderLeftWidth;
mInnerClipTempRectForBorderRadius.right -= borderRightWidth;
final float borderRadius = getFullBorderRadius();
final float topLeftRadius =
@ -315,8 +366,22 @@ public class ReactViewBackgroundDrawable extends Drawable {
final float bottomRightRadius =
getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.BOTTOM_RIGHT);
mPathForBorderRadius.addRoundRect(
mTempRectForBorderRadius,
mInnerClipPathForBorderRadius.addRoundRect(
mInnerClipTempRectForBorderRadius,
new float[] {
Math.max(topLeftRadius - borderLeftWidth, 0),
Math.max(topLeftRadius - borderTopWidth, 0),
Math.max(topRightRadius - borderRightWidth, 0),
Math.max(topRightRadius - borderTopWidth, 0),
Math.max(bottomRightRadius - borderRightWidth, 0),
Math.max(bottomRightRadius - borderBottomWidth, 0),
Math.max(bottomLeftRadius - borderLeftWidth, 0),
Math.max(bottomLeftRadius - borderBottomWidth, 0),
},
Path.Direction.CW);
mOuterClipPathForBorderRadius.addRoundRect(
mOuterClipTempRectForBorderRadius,
new float[] {
topLeftRadius,
topLeftRadius,
@ -329,6 +394,7 @@ public class ReactViewBackgroundDrawable extends Drawable {
},
Path.Direction.CW);
float extraRadiusForOutline = 0;
if (mBorderWidth != null) {
@ -350,6 +416,20 @@ public class ReactViewBackgroundDrawable extends Drawable {
Path.Direction.CW);
}
public float getBorderWidthOrDefaultTo(final float defaultValue, final int spacingType) {
if (mBorderWidth == null) {
return defaultValue;
}
final float width = mBorderWidth.getRaw(spacingType);
if (YogaConstants.isUndefined(width)) {
return defaultValue;
}
return width;
}
/**
* Set type of border
*/

View File

@ -33,6 +33,7 @@ import com.facebook.react.uimanager.ReactClippingViewGroup;
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
import com.facebook.react.uimanager.ReactPointerEventsView;
import com.facebook.react.uimanager.ReactZIndexedViewGroup;
import com.facebook.react.uimanager.Spacing;
import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper;
import javax.annotation.Nullable;
@ -624,13 +625,25 @@ public class ReactViewGroup extends ViewGroup implements
float top = 0f;
float right = getWidth();
float bottom = getHeight();
final float borderWidth = mReactBackgroundDrawable.getFullBorderWidth();
if (borderWidth != 0f) {
left += borderWidth;
top += borderWidth;
right -= borderWidth;
bottom -= borderWidth;
final float borderWidth = mReactBackgroundDrawable.getFullBorderWidth();
final float borderTopWidth =
mReactBackgroundDrawable.getBorderWidthOrDefaultTo(borderWidth, Spacing.TOP);
final float borderBottomWidth =
mReactBackgroundDrawable.getBorderWidthOrDefaultTo(borderWidth, Spacing.BOTTOM);
final float borderLeftWidth =
mReactBackgroundDrawable.getBorderWidthOrDefaultTo(borderWidth, Spacing.LEFT);
final float borderRightWidth =
mReactBackgroundDrawable.getBorderWidthOrDefaultTo(borderWidth, Spacing.RIGHT);
if (borderTopWidth > 0
|| borderLeftWidth > 0
|| borderBottomWidth > 0
|| borderRightWidth > 0) {
left += borderLeftWidth;
top += borderTopWidth;
right -= borderRightWidth;
bottom -= borderBottomWidth;
}
final float borderRadius = mReactBackgroundDrawable.getFullBorderRadius();
@ -659,14 +672,14 @@ public class ReactViewGroup extends ViewGroup implements
mPath.addRoundRect(
new RectF(left, top, right, bottom),
new float[] {
Math.max(topLeftBorderRadius - borderWidth, 0),
Math.max(topLeftBorderRadius - borderWidth, 0),
Math.max(topRightBorderRadius - borderWidth, 0),
Math.max(topRightBorderRadius - borderWidth, 0),
Math.max(bottomRightBorderRadius - borderWidth, 0),
Math.max(bottomRightBorderRadius - borderWidth, 0),
Math.max(bottomLeftBorderRadius - borderWidth, 0),
Math.max(bottomLeftBorderRadius - borderWidth, 0),
Math.max(topLeftBorderRadius - borderLeftWidth, 0),
Math.max(topLeftBorderRadius - borderTopWidth, 0),
Math.max(topRightBorderRadius - borderRightWidth, 0),
Math.max(topRightBorderRadius - borderTopWidth, 0),
Math.max(bottomRightBorderRadius - borderRightWidth, 0),
Math.max(bottomRightBorderRadius - borderBottomWidth, 0),
Math.max(bottomLeftBorderRadius - borderLeftWidth, 0),
Math.max(bottomLeftBorderRadius - borderBottomWidth, 0),
},
Path.Direction.CW);
canvas.clipPath(mPath);