/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.views.view;
import javax.annotation.Nullable;
import java.util.Locale;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.csslayout.CSSConstants;
import com.facebook.csslayout.FloatUtil;
import com.facebook.csslayout.Spacing;
/**
* 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.
*/
/* package */ class ReactViewBackgroundDrawable extends Drawable {
private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
private static enum BorderStyle {
SOLID,
DASHED,
DOTTED;
public @Nullable PathEffect getPathEffect(float borderWidth) {
switch (this) {
case SOLID:
return null;
case DASHED:
return new DashPathEffect(
new float[] {borderWidth*3, borderWidth*3, borderWidth*3, borderWidth*3}, 0);
case DOTTED:
return new DashPathEffect(
new float[] {borderWidth, borderWidth, borderWidth, borderWidth}, 0);
default:
return null;
}
}
};
/* Value at Spacing.ALL index used for rounded borders, whole array used by rectangular borders */
private @Nullable Spacing mBorderWidth;
private @Nullable Spacing mBorderColor;
private @Nullable BorderStyle mBorderStyle;
/* Used for rounded border and rounded background */
private @Nullable PathEffect mPathEffectForBorderStyle;
private @Nullable Path mPathForBorderRadius;
private @Nullable RectF mTempRectForBorderRadius;
private boolean mNeedUpdatePathForBorderRadius = false;
private float mBorderRadius = CSSConstants.UNDEFINED;
/* Used by all types of background and for drawing borders */
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int mColor = Color.TRANSPARENT;
private int mAlpha = 255;
@Override
public void draw(Canvas canvas) {
if (!CSSConstants.isUndefined(mBorderRadius) && mBorderRadius > 0) {
drawRoundedBackgroundWithBorders(canvas);
} else {
drawRectangularBackgroundWithBorders(canvas);
}
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mNeedUpdatePathForBorderRadius = true;
}
@Override
public void setAlpha(int alpha) {
if (alpha != mAlpha) {
mAlpha = alpha;
invalidateSelf();
}
}
@Override
public int getAlpha() {
return mAlpha;
}
@Override
public void setColorFilter(ColorFilter cf) {
// do nothing
}
@Override
public int getOpacity() {
return ColorUtil.getOpacityFromColor(ColorUtil.multiplyColorAlpha(mColor, mAlpha));
}
public void setBorderWidth(int position, float width) {
if (mBorderWidth == null) {
mBorderWidth = new Spacing();
}
if (!FloatUtil.floatsEqual(mBorderWidth.getRaw(position), width)) {
mBorderWidth.set(position, width);
if (position == Spacing.ALL) {
mNeedUpdatePathForBorderRadius = true;
}
invalidateSelf();
}
}
public void setBorderColor(int position, float color) {
if (mBorderColor == null) {
mBorderColor = new Spacing();
mBorderColor.setDefault(Spacing.LEFT, DEFAULT_BORDER_COLOR);
mBorderColor.setDefault(Spacing.TOP, DEFAULT_BORDER_COLOR);
mBorderColor.setDefault(Spacing.RIGHT, DEFAULT_BORDER_COLOR);
mBorderColor.setDefault(Spacing.BOTTOM, DEFAULT_BORDER_COLOR);
}
if (!FloatUtil.floatsEqual(mBorderColor.getRaw(position), color)) {
mBorderColor.set(position, color);
invalidateSelf();
}
}
public void setBorderStyle(@Nullable String style) {
BorderStyle borderStyle = style == null
? null
: BorderStyle.valueOf(style.toUpperCase(Locale.US));
if (mBorderStyle != borderStyle) {
mBorderStyle = borderStyle;
mNeedUpdatePathForBorderRadius = true;
invalidateSelf();
}
}
public void setRadius(float radius) {
if (mBorderRadius != radius) {
mBorderRadius = radius;
invalidateSelf();
}
}
public void setColor(int color) {
mColor = color;
invalidateSelf();
}
@VisibleForTesting
public int getColor() {
return mColor;
}
private void drawRoundedBackgroundWithBorders(Canvas canvas) {
updatePath();
int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha);
if ((useColor >>> 24) != 0) { // color is not transparent
mPaint.setColor(useColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(mPathForBorderRadius, mPaint);
}
// maybe draw borders?
float fullBorderWidth = getFullBorderWidth();
if (fullBorderWidth > 0) {
int borderColor = getFullBorderColor();
mPaint.setColor(ColorUtil.multiplyColorAlpha(borderColor, mAlpha));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(fullBorderWidth);
mPaint.setPathEffect(mPathEffectForBorderStyle);
canvas.drawPath(mPathForBorderRadius, mPaint);
}
}
private void updatePath() {
if (!mNeedUpdatePathForBorderRadius) {
return;
}
mNeedUpdatePathForBorderRadius = false;
if (mPathForBorderRadius == null) {
mPathForBorderRadius = new Path();
mTempRectForBorderRadius = new RectF();
}
mPathForBorderRadius.reset();
mTempRectForBorderRadius.set(getBounds());
float fullBorderWidth = getFullBorderWidth();
if (fullBorderWidth > 0) {
mTempRectForBorderRadius.inset(fullBorderWidth * 0.5f, fullBorderWidth * 0.5f);
}
mPathForBorderRadius.addRoundRect(
mTempRectForBorderRadius,
mBorderRadius,
mBorderRadius,
Path.Direction.CW);
mPathEffectForBorderStyle = mBorderStyle != null
? mBorderStyle.getPathEffect(getFullBorderWidth())
: null;
}
/**
* For rounded borders we use default "borderWidth" property.
*/
private float getFullBorderWidth() {
return (mBorderWidth != null && !CSSConstants.isUndefined(mBorderWidth.getRaw(Spacing.ALL))) ?
mBorderWidth.getRaw(Spacing.ALL) : 0f;
}
/**
* We use this method for getting color for rounded borders only similarly as for
* {@link #getFullBorderWidth}.
*/
private int getFullBorderColor() {
return (mBorderColor != null && !CSSConstants.isUndefined(mBorderColor.getRaw(Spacing.ALL))) ?
(int) (long) mBorderColor.getRaw(Spacing.ALL) : DEFAULT_BORDER_COLOR;
}
private void drawRectangularBackgroundWithBorders(Canvas canvas) {
int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha);
if ((useColor >>> 24) != 0) { // color is not transparent
mPaint.setColor(useColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(getBounds(), mPaint);
}
// maybe draw borders?
if (getBorderWidth(Spacing.LEFT) > 0 || getBorderWidth(Spacing.TOP) > 0 ||
getBorderWidth(Spacing.RIGHT) > 0 || getBorderWidth(Spacing.BOTTOM) > 0) {
int borderLeft = getBorderWidth(Spacing.LEFT);
int borderTop = getBorderWidth(Spacing.TOP);
int borderRight = getBorderWidth(Spacing.RIGHT);
int borderBottom = getBorderWidth(Spacing.BOTTOM);
int colorLeft = getBorderColor(Spacing.LEFT);
int colorTop = getBorderColor(Spacing.TOP);
int colorRight = getBorderColor(Spacing.RIGHT);
int colorBottom = getBorderColor(Spacing.BOTTOM);
int width = getBounds().width();
int height = getBounds().height();
if (borderLeft > 0 && colorLeft != Color.TRANSPARENT) {
mPaint.setColor(colorLeft);
canvas.drawRect(0, borderTop, borderLeft, height - borderBottom, mPaint);
}
if (borderTop > 0 && colorTop != Color.TRANSPARENT) {
mPaint.setColor(colorTop);
canvas.drawRect(0, 0, width, borderTop, mPaint);
}
if (borderRight > 0 && colorRight != Color.TRANSPARENT) {
mPaint.setColor(colorRight);
canvas.drawRect(
width - borderRight,
borderTop,
width,
height - borderBottom,
mPaint);
}
if (borderBottom > 0 && colorBottom != Color.TRANSPARENT) {
mPaint.setColor(colorBottom);
canvas.drawRect(0, height - borderBottom, width, height, mPaint);
}
}
}
private int getBorderWidth(int position) {
return mBorderWidth != null ? Math.round(mBorderWidth.get(position)) : 0;
}
private int getBorderColor(int position) {
// Check CatalystStylesDiffMap#getColorInt() to see why this is needed
return mBorderColor != null ? (int) (long) mBorderColor.get(position) : DEFAULT_BORDER_COLOR;
}
}