| /* |
| * 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); |
| } |
| } |