blob: e4c0dfba7495ec9d728f7516c1f810c6d8fb3f36 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.taobao.weex.ui.view.border;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.SparseIntArray;
import com.taobao.weex.dom.CSSShorthand;
import com.taobao.weex.dom.CSSShorthand.CORNER;
import com.taobao.weex.dom.CSSShorthand.EDGE;
import com.taobao.weex.utils.WXLogUtils;
import com.taobao.weex.utils.WXViewUtils;
import static com.taobao.weex.dom.CSSShorthand.CORNER.ALL;
import static com.taobao.weex.dom.CSSShorthand.CORNER.BORDER_BOTTOM_LEFT;
import static com.taobao.weex.dom.CSSShorthand.CORNER.BORDER_BOTTOM_RIGHT;
import static com.taobao.weex.dom.CSSShorthand.CORNER.BORDER_TOP_LEFT;
import static com.taobao.weex.dom.CSSShorthand.CORNER.BORDER_TOP_RIGHT;
/**
* A subclass of
* {@link Drawable} used for background of {@link com.taobao.weex.ui.component.WXComponent}.
* 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 mPathForBorderDrawn} 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.
*/
public class BorderDrawable extends Drawable {
public static final int BORDER_TOP_LEFT_RADIUS = 0;
public static final int BORDER_TOP_RIGHT_RADIUS = 1;
public static final int BORDER_BOTTOM_RIGHT_RADIUS = 2;
public static final int BORDER_BOTTOM_LEFT_RADIUS = 3;
public static final int BORDER_RADIUS_ALL = 5;
static final int DEFAULT_BORDER_COLOR = Color.BLACK;
static final float DEFAULT_BORDER_WIDTH = 0;
private static final BorderStyle DEFAULT_BORDER_STYLE = BorderStyle.SOLID;
private static final String TAG = "Border";
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private static BorderStyle[] sBorderStyle = BorderStyle.values();
private
@Nullable
CSSShorthand<EDGE> mBorderWidth;
private
@Nullable
CSSShorthand<CORNER> mBorderRadius;
private
@Nullable
CSSShorthand<CORNER> mOverlappingBorderRadius;
private
@Nullable
SparseIntArray mBorderColor;
private
@Nullable
SparseIntArray mBorderStyle;
private
@Nullable
Path mPathForBorderOutline;
private boolean mNeedUpdatePath = false;
private int mColor = Color.TRANSPARENT;
/**
* set background-image linear-gradient
*/
private Shader mShader = null;
private int mAlpha = 255;
private TopLeftCorner mTopLeftCorner;
private TopRightCorner mTopRightCorner;
private BottomRightCorner mBottomRightCorner;
private BottomLeftCorner mBottomLeftCorner;
private final BorderEdge mBorderEdge = new BorderEdge();
private RectF mRectBounds;
public BorderDrawable() {
}
@Override
public void draw(@NonNull Canvas canvas) {
canvas.save();
updateBorderOutline();
//Shader uses alpha as well.
mPaint.setAlpha(255);
if (mPathForBorderOutline != null) {
int useColor = WXViewUtils.multiplyColorAlpha(mColor, mAlpha);
if (mShader != null) {
mPaint.setShader(mShader);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(mPathForBorderOutline, mPaint);
mPaint.setShader(null);
} else if ((useColor >>> 24) != 0) {
mPaint.setColor(useColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(mPathForBorderOutline, mPaint);
mPaint.setShader(null);
}
}
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
drawBorders(canvas);
mPaint.setShader(null);
canvas.restore();
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mNeedUpdatePath = true;
}
@Override
public void setAlpha(int alpha) {
if (alpha != mAlpha) {
mAlpha = alpha;
invalidateSelf();
}
}
@Override
public int getAlpha() {
return mAlpha;
}
/**
* Do not support Color Filter
*/
@Override
public void setColorFilter(ColorFilter cf) {
}
@Override
public int getOpacity() {
return mShader != null ? PixelFormat.OPAQUE :
WXViewUtils.getOpacityFromColor(WXViewUtils.multiplyColorAlpha(mColor, mAlpha));
}
/* Android's elevation implementation requires this to be implemented to know where to draw the
shadow. */
@Override
public void getOutline(@NonNull Outline outline) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (mPathForBorderOutline == null) {
mNeedUpdatePath = true;
}
updateBorderOutline();
outline.setConvexPath(mPathForBorderOutline);
}
}
public void setBorderWidth(CSSShorthand.EDGE edge, float width) {
if (mBorderWidth == null) {
mBorderWidth = new CSSShorthand<>();
}
if (mBorderWidth.get(edge) != width) {
mBorderWidth.set(edge, width);
mNeedUpdatePath = true;
invalidateSelf();
}
}
float getBorderWidth(CSSShorthand.EDGE edge) {
return mBorderWidth.get(edge);
}
public void setBorderRadius(CORNER position, float radius) {
if (mBorderRadius == null) {
mBorderRadius = new CSSShorthand<>();
}
if (mBorderRadius.get(position) != radius ||
(position == ALL &&
(radius != mBorderRadius.get(BORDER_TOP_LEFT) ||
radius != mBorderRadius.get(BORDER_TOP_RIGHT) ||
radius != mBorderRadius.get(BORDER_BOTTOM_RIGHT) ||
radius != mBorderRadius.get(BORDER_BOTTOM_LEFT)))) {
mBorderRadius.set(position, radius);
mNeedUpdatePath = true;
invalidateSelf();
}
}
public
@NonNull
float[] getBorderRadius(RectF borderBox) {
prepareBorderRadius(borderBox);
if (mOverlappingBorderRadius == null) {
mOverlappingBorderRadius = new CSSShorthand<>();
}
float topLeftRadius = mOverlappingBorderRadius.get(BORDER_TOP_LEFT);
float topRightRadius = mOverlappingBorderRadius.get(BORDER_TOP_RIGHT);
float bottomRightRadius = mOverlappingBorderRadius.get(BORDER_BOTTOM_RIGHT);
float bottomLeftRadius = mOverlappingBorderRadius.get(BORDER_BOTTOM_LEFT);
return new float[]{topLeftRadius, topLeftRadius,
topRightRadius, topRightRadius,
bottomRightRadius, bottomRightRadius,
bottomLeftRadius, bottomLeftRadius};
}
public
@NonNull
float[] getBorderInnerRadius(RectF borderBox) {
prepareBorderRadius(borderBox);
if (mOverlappingBorderRadius == null) {
mOverlappingBorderRadius = new CSSShorthand<>();
}
float topLeftRadius = mOverlappingBorderRadius.get(BORDER_TOP_LEFT);
float topRightRadius = mOverlappingBorderRadius.get(BORDER_TOP_RIGHT);
float bottomRightRadius = mOverlappingBorderRadius.get(BORDER_BOTTOM_RIGHT);
float bottomLeftRadius = mOverlappingBorderRadius.get(BORDER_BOTTOM_LEFT);
if (null != mBorderWidth) {
topLeftRadius = Math.max(topLeftRadius - mBorderWidth.get(EDGE.TOP), 0);
topRightRadius = Math.max(topRightRadius - mBorderWidth.get(EDGE.TOP), 0);
bottomRightRadius = Math.max(bottomRightRadius - mBorderWidth.get(EDGE.BOTTOM), 0);
bottomLeftRadius = Math.max(bottomLeftRadius - mBorderWidth.get(EDGE.BOTTOM), 0);
}
return new float[] {topLeftRadius, topLeftRadius,
topRightRadius, topRightRadius,
bottomRightRadius, bottomRightRadius,
bottomLeftRadius, bottomLeftRadius};
}
public void setBorderColor(CSSShorthand.EDGE edge, int color) {
if (mBorderColor == null) {
mBorderColor = new SparseIntArray(5);
mBorderColor.put(CSSShorthand.EDGE.ALL.ordinal(), DEFAULT_BORDER_COLOR);
}
if (getBorderColor(edge) != color) {
BorderUtil.updateSparseArray(mBorderColor, edge.ordinal(), color);
invalidateSelf();
}
}
int getBorderColor(CSSShorthand.EDGE edge) {
return BorderUtil.fetchFromSparseArray(mBorderColor, edge.ordinal(), DEFAULT_BORDER_COLOR);
}
public void setBorderStyle(CSSShorthand.EDGE edge, @NonNull String style) {
if (mBorderStyle == null) {
mBorderStyle = new SparseIntArray(5);
mBorderStyle.put(CSSShorthand.EDGE.ALL.ordinal(), DEFAULT_BORDER_STYLE.ordinal());
}
try {
int borderStyle = BorderStyle.valueOf(style.toUpperCase(Locale.US)).ordinal();
if (getBorderStyle(edge) != borderStyle) {
BorderUtil.updateSparseArray(mBorderStyle, edge.ordinal(), borderStyle);
invalidateSelf();
}
} catch (IllegalArgumentException e) {
WXLogUtils.e(TAG, WXLogUtils.getStackTrace(e));
}
}
int getBorderStyle(CSSShorthand.EDGE edge) {
return BorderUtil.fetchFromSparseArray(mBorderStyle, edge.ordinal(), BorderStyle.SOLID.ordinal());
}
public int getColor() {
return mColor;
}
public void setColor(int color) {
mColor = color;
invalidateSelf();
}
public void setImage(Shader shader) {
mShader = shader;
invalidateSelf();
}
public boolean hasImage() {
return mShader != null;
}
public boolean isRounded() {
return mBorderRadius != null &&
(mBorderRadius.get(BORDER_TOP_LEFT) != 0 ||
mBorderRadius.get(BORDER_TOP_RIGHT) != 0 ||
mBorderRadius.get(BORDER_BOTTOM_RIGHT) != 0 ||
mBorderRadius.get(BORDER_BOTTOM_LEFT) != 0);
}
public
@NonNull
Path getContentPath(@NonNull RectF borderBox) {
Path contentClip = new Path();
prepareBorderPath(0, 0, 0, 0, borderBox, contentClip);
return contentClip;
}
private void updateBorderOutline() {
if (mNeedUpdatePath) {
mNeedUpdatePath = false;
if (mPathForBorderOutline == null) {
mPathForBorderOutline = new Path();
}
mPathForBorderOutline.reset();
prepareBorderPath(0, 0, 0, 0, new RectF(getBounds()), mPathForBorderOutline);
}
}
private void prepareBorderPath(int topPadding,
int rightPadding,
int bottomPadding,
int leftPadding,
@NonNull RectF rectF,
@NonNull Path path) {
if (mBorderRadius != null) {
prepareBorderRadius(rectF);
if (mOverlappingBorderRadius == null) {
mOverlappingBorderRadius = new CSSShorthand<>();
}
float topLeftRadius = mOverlappingBorderRadius.get(BORDER_TOP_LEFT);
float topRightRadius = mOverlappingBorderRadius.get(BORDER_TOP_RIGHT);
float bottomRightRadius = mOverlappingBorderRadius.get(BORDER_BOTTOM_RIGHT);
float bottomLeftRadius = mOverlappingBorderRadius.get(BORDER_BOTTOM_LEFT);
path.addRoundRect(
rectF,
new float[]{
topLeftRadius - leftPadding,
topLeftRadius - topPadding,
topRightRadius - rightPadding,
topRightRadius - topPadding,
bottomRightRadius - rightPadding,
bottomRightRadius - bottomPadding,
bottomLeftRadius - leftPadding,
bottomLeftRadius - bottomPadding
},
Path.Direction.CW);
} else {
path.addRect(rectF, Path.Direction.CW);
}
}
/**
* Process overlapping curve according to https://www.w3.org/TR/css3-background/#corner-overlap .
*/
private void prepareBorderRadius(@NonNull RectF borderBox) {
if (mBorderRadius != null) {
float factor = getScaleFactor(borderBox);
if (mOverlappingBorderRadius == null) {
mOverlappingBorderRadius = new CSSShorthand<>();
}
if (!Float.isNaN(factor) && factor < 1) {
mOverlappingBorderRadius.set(BORDER_TOP_LEFT,
mBorderRadius.get(BORDER_TOP_LEFT) * factor);
mOverlappingBorderRadius.set(BORDER_TOP_RIGHT,
mBorderRadius.get(BORDER_TOP_RIGHT) * factor);
mOverlappingBorderRadius.set(BORDER_BOTTOM_RIGHT,
mBorderRadius.get(BORDER_BOTTOM_RIGHT) * factor);
mOverlappingBorderRadius.set(BORDER_BOTTOM_LEFT,
mBorderRadius.get(BORDER_BOTTOM_LEFT) * factor);
} else {
mOverlappingBorderRadius.set(BORDER_TOP_LEFT,
mBorderRadius.get(BORDER_TOP_LEFT));
mOverlappingBorderRadius.set(BORDER_TOP_RIGHT,
mBorderRadius.get(BORDER_TOP_RIGHT));
mOverlappingBorderRadius.set(BORDER_BOTTOM_RIGHT,
mBorderRadius.get(BORDER_BOTTOM_RIGHT));
mOverlappingBorderRadius.set(BORDER_BOTTOM_LEFT,
mBorderRadius.get(BORDER_BOTTOM_LEFT));
}
}
}
private float getScaleFactor(@NonNull RectF borderBox) {
final float topRadius = mBorderRadius.get(BORDER_TOP_LEFT)
+ mBorderRadius.get(BORDER_TOP_RIGHT);
final float rightRadius = mBorderRadius.get(BORDER_TOP_RIGHT)
+ mBorderRadius.get(BORDER_BOTTOM_RIGHT);
final float bottomRadius = mBorderRadius.get(BORDER_BOTTOM_RIGHT)
+ mBorderRadius.get(BORDER_BOTTOM_LEFT);
final float leftRadius = mBorderRadius.get(BORDER_BOTTOM_LEFT)
+ mBorderRadius.get(BORDER_TOP_LEFT);
List<Float> factors = new ArrayList<>(4);
updateFactor(factors, borderBox.width(), topRadius);
updateFactor(factors, borderBox.height(), rightRadius);
updateFactor(factors, borderBox.width(), bottomRadius);
updateFactor(factors, borderBox.height(), leftRadius);
float factor;
if (factors.isEmpty()) {
factor = Float.NaN;
} else {
factor = Collections.min(factors);
}
return factor;
}
private void updateFactor(@NonNull List<Float> list, float numerator, float denominator) {
if (denominator != 0) {
list.add(numerator / denominator);
}
}
private void drawBorders(Canvas canvas) {
if (mRectBounds == null) {
mRectBounds = new RectF(getBounds());
} else {
mRectBounds.set(getBounds());
}
if (mBorderWidth == null)
return;
final float leftBorderWidth = mBorderWidth.get(EDGE.LEFT);
final float topBorderWidth = mBorderWidth.get(EDGE.TOP);
final float bottomBorderWidth = mBorderWidth.get(EDGE.BOTTOM);
final float rightBorderWidth = mBorderWidth.get(EDGE.RIGHT);
if (mTopLeftCorner == null) {
mTopLeftCorner = new TopLeftCorner();
}
mTopLeftCorner.set(getBorderRadius(BORDER_TOP_LEFT), leftBorderWidth, topBorderWidth, mRectBounds);
if (mTopRightCorner == null) {
mTopRightCorner = new TopRightCorner();
}
mTopRightCorner.set(getBorderRadius(BORDER_TOP_RIGHT), topBorderWidth, rightBorderWidth, mRectBounds);
if (mBottomRightCorner == null) {
mBottomRightCorner = new BottomRightCorner();
}
mBottomRightCorner.set(getBorderRadius(BORDER_BOTTOM_RIGHT), rightBorderWidth, bottomBorderWidth, mRectBounds);
if (mBottomLeftCorner == null) {
mBottomLeftCorner = new BottomLeftCorner();
}
mBottomLeftCorner.set(getBorderRadius(BORDER_BOTTOM_LEFT), bottomBorderWidth, leftBorderWidth, mRectBounds);
drawOneSide(canvas, mBorderEdge.set(mTopLeftCorner, mTopRightCorner, topBorderWidth, EDGE.TOP));
drawOneSide(canvas, mBorderEdge.set(mTopRightCorner, mBottomRightCorner, rightBorderWidth, EDGE.RIGHT));
drawOneSide(canvas, mBorderEdge.set(mBottomRightCorner, mBottomLeftCorner, bottomBorderWidth, EDGE.BOTTOM));
drawOneSide(canvas, mBorderEdge.set(mBottomLeftCorner, mTopLeftCorner, leftBorderWidth, EDGE.LEFT));
}
private float getBorderRadius(CORNER position) {
if (null != mOverlappingBorderRadius) {
return mOverlappingBorderRadius.get(position);
} else {
return 0.0f;
}
}
private void drawOneSide(Canvas canvas, @NonNull BorderEdge borderEdge) {
if (0 != borderEdge.getBorderWidth()) {
preparePaint(borderEdge.getEdge());
borderEdge.drawEdge(canvas, mPaint);
}
}
private void preparePaint(CSSShorthand.EDGE edge) {
final float borderWidth = mBorderWidth.get(edge);
final int color = WXViewUtils.multiplyColorAlpha(getBorderColor(edge), mAlpha);
final BorderStyle borderStyle = sBorderStyle[getBorderStyle(edge)];
final Shader shader = borderStyle.getLineShader(borderWidth, color, edge);
mPaint.setShader(shader);
mPaint.setColor(color);
mPaint.setStrokeCap(Paint.Cap.ROUND);
}
}