Skip to content

Commit

Permalink
Implement partial rounded borders
Browse files Browse the repository at this point in the history
Reviewed By: achen1

Differential Revision: D5982241

fbshipit-source-id: 2f694daca7e1b16b5ff65f07c7d15dd558a4b7e8
  • Loading branch information
RSNara authored and facebook-github-bot committed Oct 19, 2017
1 parent de313f6 commit 4994d6a
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;
Expand All @@ -30,15 +31,15 @@
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 {

Expand Down Expand Up @@ -83,10 +84,12 @@ private static enum BorderStyle {

/* 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;
Expand Down Expand Up @@ -169,8 +172,13 @@ public void setBorderWidth(int position, float width) {
}
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();
}
Expand Down Expand Up @@ -266,44 +274,87 @@ public int getColor() {

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 =
Expand All @@ -315,8 +366,22 @@ private void updatePath() {
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,
Expand All @@ -329,6 +394,7 @@ private void updatePath() {
},
Path.Direction.CW);


float extraRadiusForOutline = 0;

if (mBorderWidth != null) {
Expand All @@ -350,6 +416,20 @@ private void updatePath() {
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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
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;

Expand Down Expand Up @@ -624,13 +625,25 @@ protected void dispatchDraw(Canvas canvas) {
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();
Expand Down Expand Up @@ -659,14 +672,14 @@ protected void dispatchDraw(Canvas canvas) {
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);
Expand Down

10 comments on commit 4994d6a

@uribro
Copy link

@uribro uribro commented on 4994d6a Jan 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This CL breaks the borderRadius inside animated view, after this CL borderRadius inside animating view isn't translating together with the view causing the border to stay in the same place while the rest of the view translating. and it looks like the border is being detached.

This is the issue:
#17224

@brunolemos
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RSNara I noticed the same problem: #18266

@allengleyzer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dashed and dotted borderStyles also broke as a result of this commit: #17251, #18285

@abarisic86
Copy link

@abarisic86 abarisic86 commented on 4994d6a Mar 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More issues related to this: #17267 & #18000. Can we revert those changes? @achen1

@hramos
Copy link
Contributor

@hramos hramos commented on 4994d6a Mar 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverting 5 months after the commit landed is unlikely to happen - this will need to be fixed forward.

@mannol
Copy link
Contributor

@mannol mannol commented on 4994d6a Mar 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just say that reverting this commit (and other commits related to the border work) did indeed fix it for me.

@brunolemos
Copy link
Contributor

@brunolemos brunolemos commented on 4994d6a Mar 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mannol which others? It would be nice to know the exact changes that caused the issues so we can work on a fix

@mannol
Copy link
Contributor

@mannol mannol commented on 4994d6a Mar 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brunolemos I had to revert following commits:

5aa1fb3 7170543 efa4d3c 00c9c1a 875f273 38c2c26 1a7abcf 0f467a2 7ed7593 f788831
4994d6a de313f6

And also, I had to add a new method in ReactViewBackgroundDrawable:

 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java 
index 496007c8b..414fa26d0 100644
@@ -111,6 +111,11 @@ public class ReactViewBackgroundDrawable extends Drawable {
     }
   }
 
+  public boolean hasRoundedBorders() {
+    return mBorderCornerRadii != null ||
+        (!YogaConstants.isUndefined(mBorderRadius) && mBorderRadius > 0);
+  }
+
   @Override
   protected void onBoundsChange(Rect bounds) {
     super.onBoundsChange(bounds);

Really hope this gets fixed soon; the change is too big for someone to just hop into it without any previous knowledge on the topic.

@alpamys-qanybet
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mannol, can you please just provide your working java classes.

@alpamys-qanybet
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey guys, I just copied the ReactViewBackgroundDrawable.java, ReactViewBackgroundManager.java, ReactViewGroup.java from RN0.50.1 and IT WORKS.

Please sign in to comment.