blob: a05a964cee1d5271cc43125a98fe22444c6ec251 [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 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.SparseArray;
import android.util.SparseIntArray;
import com.taobao.weex.dom.flex.FloatUtil;
import com.taobao.weex.dom.flex.Spacing;
import com.taobao.weex.utils.WXLogUtils;
import com.taobao.weex.utils.WXViewUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* 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 = 8;
static final int DEFAULT_BORDER_COLOR = Color.BLACK;
static final float DEFAULT_BORDER_WIDTH = 0;
private static final float DEFAULT_BORDER_RADIUS = 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
@Nullable
SparseArray<Float> mBorderWidth;
private
@Nullable
SparseArray<Float> mBorderRadius;
private
@Nullable
SparseArray<Float> 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;
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(@BorderWidthStyleColorType int position, float width) {
if (mBorderWidth == null) {
mBorderWidth = new SparseArray<>(5);
mBorderWidth.put(Spacing.ALL, DEFAULT_BORDER_WIDTH);
}
if (!FloatUtil.floatsEqual(getBorderWidth(position), width)) {
BorderUtil.updateSparseArray(mBorderWidth, position, width);
mBorderWidth.put(position, width);
mNeedUpdatePath = true;
invalidateSelf();
}
}
float getBorderWidth(@BorderWidthStyleColorType int position) {
return BorderUtil.fetchFromSparseArray(mBorderWidth, position, DEFAULT_BORDER_WIDTH);
}
public void setBorderRadius(@BorderRadiusType int position, float radius) {
if (mBorderRadius == null) {
mBorderRadius = new SparseArray<>(5);
mBorderRadius.put(Spacing.ALL, DEFAULT_BORDER_RADIUS);
}
if (!FloatUtil.floatsEqual(getBorderRadius(mBorderRadius, position), radius)) {
BorderUtil.updateSparseArray(mBorderRadius, position, radius, true);
mNeedUpdatePath = true;
invalidateSelf();
}
}
@VisibleForTesting
float getBorderRadius(@BorderRadiusType int position) {
return getBorderRadius(mOverlappingBorderRadius, position);
}
public
@NonNull
float[] getBorderRadius(RectF borderBox) {
prepareBorderRadius(borderBox);
float topLeftRadius = getBorderRadius(mOverlappingBorderRadius, BORDER_TOP_LEFT_RADIUS);
float topRightRadius = getBorderRadius(mOverlappingBorderRadius, BORDER_TOP_RIGHT_RADIUS);
float bottomRightRadius = getBorderRadius(mOverlappingBorderRadius, BORDER_BOTTOM_RIGHT_RADIUS);
float bottomLeftRadius = getBorderRadius(mOverlappingBorderRadius, BORDER_BOTTOM_LEFT_RADIUS);
return new float[]{topLeftRadius,topLeftRadius,
topRightRadius,topRightRadius,
bottomRightRadius, bottomRightRadius,
bottomLeftRadius,bottomLeftRadius};
}
public void setBorderColor(@BorderWidthStyleColorType int position, int color) {
if (mBorderColor == null) {
mBorderColor = new SparseIntArray(5);
mBorderColor.put(Spacing.ALL, DEFAULT_BORDER_COLOR);
}
if (getBorderColor(position) != color) {
BorderUtil.updateSparseArray(mBorderColor, position, color);
invalidateSelf();
}
}
int getBorderColor(@BorderWidthStyleColorType int position) {
return BorderUtil.fetchFromSparseArray(mBorderColor, position, DEFAULT_BORDER_COLOR);
}
public void setBorderStyle(@BorderWidthStyleColorType int position, @NonNull String style) {
if (mBorderStyle == null) {
mBorderStyle = new SparseIntArray(5);
mBorderStyle.put(Spacing.ALL, DEFAULT_BORDER_STYLE.ordinal());
}
try {
int borderStyle = BorderStyle.valueOf(style.toUpperCase(Locale.US)).ordinal();
if (getBorderStyle(position) != borderStyle) {
BorderUtil.updateSparseArray(mBorderStyle, position, borderStyle);
invalidateSelf();
}
} catch (IllegalArgumentException e) {
WXLogUtils.e(TAG, WXLogUtils.getStackTrace(e));
}
}
int getBorderStyle(@BorderWidthStyleColorType int position) {
return BorderUtil.fetchFromSparseArray(mBorderStyle, position, 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 &&
(!FloatUtil.floatsEqual(getBorderRadius(mBorderRadius, BORDER_TOP_LEFT_RADIUS), 0) ||
!FloatUtil.floatsEqual(getBorderRadius(mBorderRadius, BORDER_TOP_RIGHT_RADIUS), 0) ||
!FloatUtil.floatsEqual(getBorderRadius(mBorderRadius, BORDER_BOTTOM_RIGHT_RADIUS), 0) ||
!FloatUtil.floatsEqual(getBorderRadius(mBorderRadius, BORDER_BOTTOM_LEFT_RADIUS), 0));
}
public
@NonNull
Path getContentPath(@NonNull RectF borderBox) {
Path contentClip = new Path();
prepareBorderPath(0, 0, 0, 0, borderBox, contentClip);
return contentClip;
}
private float getBorderRadius(@Nullable SparseArray<Float> borderRadius, @BorderRadiusType int position) {
return BorderUtil.fetchFromSparseArray(borderRadius, position, DEFAULT_BORDER_RADIUS);
}
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);
float topLeftRadius = getBorderRadius(mOverlappingBorderRadius, BORDER_TOP_LEFT_RADIUS);
float topRightRadius = getBorderRadius(mOverlappingBorderRadius, BORDER_TOP_RIGHT_RADIUS);
float bottomRightRadius = getBorderRadius(mOverlappingBorderRadius,
BORDER_BOTTOM_RIGHT_RADIUS);
float bottomLeftRadius = getBorderRadius(mOverlappingBorderRadius,
BORDER_BOTTOM_LEFT_RADIUS);
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 SparseArray<>(5);
mOverlappingBorderRadius.put(Spacing.ALL, 0f);
}
if (!Float.isNaN(factor) && factor < 1) {
mOverlappingBorderRadius.put(BORDER_TOP_LEFT_RADIUS,
getBorderRadius(mBorderRadius, BORDER_TOP_LEFT_RADIUS) *
factor);
mOverlappingBorderRadius.put(BORDER_TOP_RIGHT_RADIUS,
getBorderRadius(mBorderRadius, BORDER_TOP_RIGHT_RADIUS) *
factor);
mOverlappingBorderRadius.put(BORDER_BOTTOM_RIGHT_RADIUS,
getBorderRadius(mBorderRadius, BORDER_BOTTOM_RIGHT_RADIUS) *
factor);
mOverlappingBorderRadius.put(BORDER_BOTTOM_LEFT_RADIUS,
getBorderRadius(mBorderRadius, BORDER_BOTTOM_LEFT_RADIUS) *
factor);
} else {
mOverlappingBorderRadius.put(BORDER_TOP_LEFT_RADIUS,
getBorderRadius(mBorderRadius, BORDER_TOP_LEFT_RADIUS));
mOverlappingBorderRadius.put(BORDER_TOP_RIGHT_RADIUS,
getBorderRadius(mBorderRadius, BORDER_TOP_RIGHT_RADIUS));
mOverlappingBorderRadius.put(BORDER_BOTTOM_RIGHT_RADIUS,
getBorderRadius(mBorderRadius, BORDER_BOTTOM_RIGHT_RADIUS));
mOverlappingBorderRadius.put(BORDER_BOTTOM_LEFT_RADIUS,
getBorderRadius(mBorderRadius, BORDER_BOTTOM_LEFT_RADIUS));
}
}
}
private float getScaleFactor(@NonNull RectF borderBox) {
final float topRadius = getBorderRadius(mBorderRadius, BORDER_TOP_LEFT_RADIUS)
+ getBorderRadius(mBorderRadius, BORDER_TOP_RIGHT_RADIUS);
final float rightRadius = getBorderRadius(mBorderRadius, BORDER_TOP_RIGHT_RADIUS)
+ getBorderRadius(mBorderRadius, BORDER_BOTTOM_RIGHT_RADIUS);
final float bottomRadius = getBorderRadius(mBorderRadius, BORDER_BOTTOM_RIGHT_RADIUS)
+ getBorderRadius(mBorderRadius, BORDER_BOTTOM_LEFT_RADIUS);
final float leftRadius = getBorderRadius(mBorderRadius, BORDER_BOTTOM_LEFT_RADIUS)
+ getBorderRadius(mBorderRadius, BORDER_TOP_LEFT_RADIUS);
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 (!FloatUtil.floatsEqual(denominator, 0)) {
list.add(numerator / denominator);
}
}
private void drawBorders(Canvas canvas) {
RectF rectBounds = new RectF(getBounds());
BorderCorner topLeft = new TopLeftCorner(
getBorderRadius(mOverlappingBorderRadius, BORDER_TOP_LEFT_RADIUS),
getBorderWidth(Spacing.LEFT),
getBorderWidth(Spacing.TOP),
rectBounds);
BorderCorner topRight = new TopRightCorner(
getBorderRadius(mOverlappingBorderRadius, BORDER_TOP_RIGHT_RADIUS),
getBorderWidth(Spacing.TOP),
getBorderWidth(Spacing.RIGHT),
rectBounds);
BorderCorner bottomRight = new BottomRightCorner(
getBorderRadius(mOverlappingBorderRadius, BORDER_BOTTOM_RIGHT_RADIUS),
getBorderWidth(Spacing.RIGHT),
getBorderWidth(Spacing.BOTTOM),
rectBounds);
BorderCorner bottomLeft = new BottomLeftCorner(
getBorderRadius(mOverlappingBorderRadius, BORDER_BOTTOM_LEFT_RADIUS),
getBorderWidth(Spacing.BOTTOM),
getBorderWidth(Spacing.LEFT),
rectBounds);
drawOneSide(canvas, new BorderEdge(topLeft, topRight, Spacing.TOP,
getBorderWidth(Spacing.TOP)));
drawOneSide(canvas, new BorderEdge(topRight, bottomRight, Spacing.RIGHT,
getBorderWidth(Spacing.RIGHT)));
drawOneSide(canvas, new BorderEdge(bottomRight, bottomLeft, Spacing.BOTTOM,
getBorderWidth(Spacing.BOTTOM)));
drawOneSide(canvas, new BorderEdge(bottomLeft, topLeft, Spacing.LEFT,
getBorderWidth(Spacing.LEFT)));
}
private void drawOneSide(Canvas canvas, @NonNull BorderEdge borderEdge) {
if (!FloatUtil.floatsEqual(0, getBorderWidth(borderEdge.getEdge()))) {
preparePaint(borderEdge.getEdge());
borderEdge.drawEdge(canvas, mPaint);
}
}
private void preparePaint(@BorderWidthStyleColorType int side) {
float borderWidth = getBorderWidth(side);
int color = WXViewUtils.multiplyColorAlpha(getBorderColor(side), mAlpha);
BorderStyle borderStyle = BorderStyle.values()[getBorderStyle(side)];
Shader shader = borderStyle.getLineShader(borderWidth, color, side);
mPaint.setShader(shader);
mPaint.setColor(color);
mPaint.setStrokeCap(Paint.Cap.ROUND);
}
}