blob: 3f522ddba606bf67a4937580646dc9b3201caabe [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.utils;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.taobao.weex.WXEnvironment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Created by moxun on 2017/9/4.
* Utils for create shadow layer on view
* Requires Android 4.3 and higher
*
* @see <a href="https://www.w3schools.com/cssref/css3_pr_box-shadow.asp">CSS3 box-shadow Property</>
*/
public class BoxShadowUtil {
private static final String TAG = "BoxShadowUtil";
public static void setBoxShadow(final View target, String style, float[] radii, int viewPort) {
final BoxShadowOptions options = parseBoxShadow(style, viewPort);
if (options == null) {
WXLogUtils.w(TAG, "Failed to parse box-shadow: " + style);
return;
}
if (target == null) {
WXLogUtils.w(TAG, "Target view is null!");
return;
}
if (options.isClear && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
target.getOverlay().clear();
WXLogUtils.d(TAG, "Remove box-shadow");
return;
}
if (radii != null) {
if (radii.length != 8) {
WXLogUtils.w(TAG, "Length of radii must be 8");
} else {
for (int i = 0; i < radii.length; i++) {
float realRadius = WXViewUtils.getRealSubPxByWidth(radii[i], viewPort);
radii[i] = realRadius;
}
options.radii = radii;
}
}
WXLogUtils.d(TAG, "Set box-shadow: " + options.toString());
target.post(new Runnable() {
@Override
public void run() {
if (options.isInset) {
setInsetBoxShadow(target, options);
} else {
setNormalBoxShadow(target, options);
}
}
});
}
private static Bitmap createShadowBitmap(int viewWidth, int viewHeight,
float[] radii, float shadowRadius,
float shadowSpread,
float dx, float dy, int shadowColor) {
int canvasWidth = viewWidth + 2 * (int) (shadowRadius + shadowSpread + Math.abs(dx));
int canvasHeight = viewHeight + 2 * (int) (shadowRadius + shadowSpread + Math.abs(dy));
Bitmap output = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
if (false && WXEnvironment.isApkDebugable()) {
// Using for debug
Paint strokePaint = new Paint();
strokePaint.setColor(Color.BLACK);
strokePaint.setStrokeWidth(2);
strokePaint.setStyle(Paint.Style.STROKE);
canvas.drawRect(canvas.getClipBounds(), strokePaint);
}
float offsetX = shadowRadius + shadowSpread + Math.abs(dx);
float offsetY = shadowRadius + shadowSpread + Math.abs(dy);
RectF selfRect = new RectF(
offsetX,
offsetY,
(float) Math.floor(viewWidth + offsetX),
(float) Math.floor(viewHeight + offsetY));
Path contentPath = new Path();
contentPath.addRoundRect(selfRect, radii, Path.Direction.CCW);
// can not antialias
canvas.clipPath(contentPath, Region.Op.DIFFERENCE);
RectF shadowRect = new RectF(
0f, 0f,
viewWidth + 2f * shadowSpread, viewHeight + 2f * shadowSpread
);
float shadowDx = shadowRadius;
float shadowDy = shadowRadius;
if (dx > 0) {
shadowDx = shadowDx + 2f * dx;
}
if (dy > 0) {
shadowDy = shadowDy + 2f * dy;
}
shadowRect.offset(shadowDx, shadowDy);
Paint shadowPaint = new Paint();
shadowPaint.setAntiAlias(true);
shadowPaint.setColor(shadowColor);
shadowPaint.setStyle(Paint.Style.FILL);
if (shadowRadius > 0) {
shadowPaint.setMaskFilter(new BlurMaskFilter(shadowRadius, BlurMaskFilter.Blur.NORMAL));
}
Path shadowPath = new Path();
float[] shadowRadii = new float[8];
for (int i = 0; i < radii.length; i++) {
float contentRadius = radii[i];
if (contentRadius == 0f) {
shadowRadii[i] = 0f;
} else {
shadowRadii[i] = radii[i] + shadowSpread;
}
}
shadowPath.addRoundRect(shadowRect, shadowRadii, Path.Direction.CCW);
canvas.drawPath(shadowPath, shadowPaint);
return output;
}
private static void setNormalBoxShadow(View target, BoxShadowOptions options) {
int h = target.getHeight();
int w = target.getWidth();
if (h == 0 || w == 0) {
Log.w(TAG, "Target view is invisible, ignore set shadow.");
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
Bitmap shadowBitmap = createShadowBitmap(w, h, options.radii, options.blur, options.spread, options.hShadow, options.vShadow, options.color);
int overflowX = (int) (options.blur + Math.abs(options.hShadow) + options.spread);
int overflowY = (int) (options.blur + Math.abs(options.vShadow) + options.spread);
//Drawable's bounds must match the bitmap size, otherwise the shadows will be scaled
OverflowBitmapDrawable shadowDrawable = new OverflowBitmapDrawable(target.getResources(), shadowBitmap, overflowX, overflowY);
shadowDrawable.setBounds(-overflowX, -overflowY, w + overflowX, h + overflowY);
target.getOverlay().clear();
target.getOverlay().add(shadowDrawable);
//Relayout to ensure the shadows are fully drawn
ViewParent parent = target.getParent();
if (parent != null) {
parent.requestLayout();
if (parent instanceof ViewGroup) {
((ViewGroup) parent).invalidate(shadowDrawable.getBounds());
}
}
} else {
// I have a dream that one day our minSdkVersion will equals or higher than 21
Log.w("BoxShadowUtil", "Call setNormalBoxShadow() requires API level 18 or higher.");
}
}
private static void setInsetBoxShadow(View target, BoxShadowOptions options) {
if (target == null || options == null) {
WXLogUtils.w(TAG, "Illegal arguments");
return;
}
if (target.getWidth() == 0 || target.getHeight() == 0) {
WXLogUtils.w(TAG, "Target view is invisible, ignore set shadow.");
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
Drawable shadow = new InsetShadowDrawable(target.getWidth(), target.getHeight(),
options.hShadow, options.vShadow,
options.blur, options.spread,
options.color, options.radii);
target.getOverlay().clear();
target.getOverlay().add(shadow);
target.invalidate();
} else {
Log.w(TAG, "Call setInsetBoxShadow() requires API level 18 or higher.");
}
}
public static BoxShadowOptions parseBoxShadow(String boxShadow, int viewport) {
BoxShadowOptions result = new BoxShadowOptions(viewport);
if (TextUtils.isEmpty(boxShadow)) {
result.isClear = true;
return result;
}
String boxShadowCopy = boxShadow;
// trim rgb() & rgba()
boxShadowCopy = boxShadowCopy.replaceAll("\\s*,\\s+", ",");
// match inset first
if (boxShadowCopy.contains("inset")) {
result.isInset = true;
boxShadowCopy = boxShadowCopy.replace("inset", "").trim();
}
List<String> params = new ArrayList<>(Arrays.asList(boxShadowCopy.split("\\s+")));
// match color
String maybeColor = params.get(params.size() - 1);
if (!TextUtils.isEmpty(maybeColor)) {
if (maybeColor.startsWith("#") || maybeColor.startsWith("rgb") || WXResourceUtils.isNamedColor(maybeColor)) {
result.color = WXResourceUtils.getColor(maybeColor, Color.BLACK);
params.remove(params.size() - 1);
}
}
try {
if (params.size() < 2) {
// Missing required param
return null;
} else {
if (!TextUtils.isEmpty(params.get(0))) {
float px = WXUtils.getFloat(params.get(0).trim(), 0f);
result.hShadow = WXViewUtils.getRealSubPxByWidth(px, viewport);
}
if (!TextUtils.isEmpty(params.get(1))) {
float px = WXUtils.getFloat(params.get(1).trim(), 0f);
result.vShadow = WXViewUtils.getRealPxByWidth(px, viewport);
}
for (int i = 2; i < params.size(); i++) {
int parserIndex = i - 2;
BoxShadowOptions.IParser parser = result.optionParamParsers.get(parserIndex);
parser.parse(params.get(i));
}
}
} catch (Throwable t) {
t.printStackTrace();
}
return result;
}
private static class OverflowBitmapDrawable extends BitmapDrawable {
int paddingX;
int paddingY;
private OverflowBitmapDrawable(Resources resources, Bitmap bitmap, int paddingX, int paddingY) {
super(resources, bitmap);
this.paddingX = paddingX;
this.paddingY = paddingY;
}
@Override
public void draw(Canvas canvas) {
Rect newRect = canvas.getClipBounds();
// Make the Canvas Rect bigger according to the padding.
newRect.inset(-paddingX * 2, -paddingY * 2);
canvas.clipRect(newRect, Region.Op.REPLACE);
super.draw(canvas);
}
}
private static class InsetShadowDrawable extends Drawable {
private static final int LEFT_TO_RIGHT = 0;
private static final int TOP_TO_BOTTOM = 1;
private static final int RIGHT_TO_LEFT = 2;
private static final int BOTTOM_TO_TOP = 3;
private float blurRadius;
private int shadowColor;
private float[] radii;
private float width, height;
private float shadowXSize, shadowYSize;
private Shader[] shades = new Shader[4];
private Path[] paths = new Path[4];
private Paint paint;
private InsetShadowDrawable(int viewWidth, int viewHeight, float dx, float dy, float blurRadius, float spread, int shadowColor, float[] radii) {
this.blurRadius = blurRadius;
this.shadowColor = shadowColor;
this.width = viewWidth + 2 * dx;
this.height = viewHeight + 2 * dy;
this.shadowXSize = dx + spread;
this.shadowYSize = dy + spread;
this.radii = radii;
setBounds(0, 0, viewWidth, viewHeight);
prepare();
}
private void prepare() {
/*
* A +++++++++++++++++++++++++ B
* + E ------------------- F +
* + | | +
* + | | +
* + | | +
* + H ------------------- G +
* D +++++++++++++++++++++++++ C
*/
PointF a = new PointF(0, 0);
PointF b = new PointF(width, 0);
PointF c = new PointF(b.x, height);
PointF d = new PointF(a.x, c.y);
PointF e = new PointF(shadowXSize, shadowYSize);
PointF f = new PointF(b.x - shadowXSize, e.y);
PointF g = new PointF(f.x, c.y - shadowYSize);
PointF h = new PointF(e.x, g.y);
Shader ltr = new LinearGradient(e.x - blurRadius, e.y, e.x, e.y, shadowColor, Color.TRANSPARENT, Shader.TileMode.CLAMP);
Shader ttb = new LinearGradient(e.x, e.y - blurRadius, e.x, e.y, shadowColor, Color.TRANSPARENT, Shader.TileMode.CLAMP);
Shader rtl = new LinearGradient(g.x + blurRadius, g.y, g.x, g.y, shadowColor, Color.TRANSPARENT, Shader.TileMode.CLAMP);
Shader btt = new LinearGradient(g.x, g.y + blurRadius, g.x, g.y, shadowColor, Color.TRANSPARENT, Shader.TileMode.CLAMP);
shades[LEFT_TO_RIGHT] = ltr;
shades[TOP_TO_BOTTOM] = ttb;
shades[RIGHT_TO_LEFT] = rtl;
shades[BOTTOM_TO_TOP] = btt;
Path ltrPath = new Path();
ltrPath.moveTo(a.x, a.y);
ltrPath.lineTo(e.x, e.y);
ltrPath.lineTo(h.x, h.y);
ltrPath.lineTo(d.x, d.y);
ltrPath.close();
Path ttbPath = new Path();
ttbPath.moveTo(a.x, a.y);
ttbPath.lineTo(b.x, b.y);
ttbPath.lineTo(f.x, f.y);
ttbPath.lineTo(e.x, e.y);
ttbPath.close();
Path rtlPath = new Path();
rtlPath.moveTo(b.x, b.y);
rtlPath.lineTo(c.x, c.y);
rtlPath.lineTo(g.x, g.y);
rtlPath.lineTo(f.x, f.y);
rtlPath.close();
Path bttPath = new Path();
bttPath.moveTo(d.x, d.y);
bttPath.lineTo(c.x, c.y);
bttPath.lineTo(g.x, g.y);
bttPath.lineTo(h.x, h.y);
bttPath.close();
paths[LEFT_TO_RIGHT] = ltrPath;
paths[TOP_TO_BOTTOM] = ttbPath;
paths[RIGHT_TO_LEFT] = rtlPath;
paths[BOTTOM_TO_TOP] = bttPath;
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(shadowColor);
}
@Override
public void draw(Canvas canvas) {
Path border = new Path();
RectF rectF = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
border.addRoundRect(rectF, radii, Path.Direction.CCW);
canvas.clipPath(border);
//TODO: we need clip border-width too
for (int i = 0; i < 4; i++) {
Shader shader = shades[i];
Path path = paths[i];
paint.setShader(shader);
canvas.drawPath(path, paint);
}
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(ColorFilter cf) {
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
}
public static class BoxShadowOptions {
private List<IParser> optionParamParsers;
private int viewport = 750;
public float hShadow;
public float vShadow;
public float blur = 0f;
public float spread = 0f;
public float[] radii = new float[]{0, 0, 0, 0, 0, 0, 0, 0};
public int color = Color.BLACK;
public boolean isInset = false;
public boolean isClear = false;
private BoxShadowOptions(int vp) {
if (viewport != 0) {
this.viewport = vp;
}
optionParamParsers = new ArrayList<>();
IParser spreadParser = new IParser() {
@Override
public void parse(String param) {
if (!TextUtils.isEmpty(param)) {
float px = WXUtils.getFloat(param, 0f);
spread = WXViewUtils.getRealSubPxByWidth(px, viewport);
WXLogUtils.w(TAG, "Experimental box-shadow attribute: spread");
}
}
};
IParser blurParser = new IParser() {
@Override
public void parse(String param) {
if (!TextUtils.isEmpty(param)) {
float px = WXUtils.getFloat(param, 0f);
blur = WXViewUtils.getRealSubPxByWidth(px, viewport);
}
}
};
optionParamParsers.add(blurParser);
optionParamParsers.add(spreadParser);
}
@Override
public String toString() {
String r = "[" + radii[0] + "," + radii[2] + "," + radii[4] + "," + radii[6] + "]";
final StringBuffer sb = new StringBuffer("BoxShadowOptions{");
sb.append("h-shadow=").append(hShadow);
sb.append(", v-shadow=").append(vShadow);
sb.append(", blur=").append(blur);
sb.append(", spread=").append(spread);
sb.append(", corner-radius=").append(r);
sb.append(", color=#").append(Integer.toHexString(color));
sb.append(", inset=").append(isInset);
sb.append('}');
return sb.toString();
}
private interface IParser {
void parse(String param);
}
}
}