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