/**
* Copyright (c) 2017-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.litho.widget;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
class CardShadowDrawable extends Drawable {
static final float SHADOW_MULTIPLIER = 1.5f;
private int mShadowStartColor;
private int mShadowEndColor;
private final Paint mEdgeShadowPaint;
private final Path mCornerShadowTopPath = new Path();
private final Paint mCornerShadowTopPaint;
private final Path mCornerShadowBottomPath = new Path();
private final Paint mCornerShadowBottomPaint;
private float mCornerRadius;
private float mShadowSize;
private float mRawShadowSize;
private boolean mDirty = true;
CardShadowDrawable() {
mCornerShadowTopPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mCornerShadowTopPaint.setStyle(Paint.Style.FILL);
mCornerShadowBottomPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mCornerShadowBottomPaint.setStyle(Paint.Style.FILL);
mEdgeShadowPaint = new Paint(mCornerShadowTopPaint);
mEdgeShadowPaint.setAntiAlias(false);
}
@Override
public void setAlpha(int alpha) {
mCornerShadowTopPaint.setAlpha(alpha);
mCornerShadowBottomPaint.setAlpha(alpha);
mEdgeShadowPaint.setAlpha(alpha);
}
static int getShadowHorizontal(float shadowSize) {
return (int) Math.ceil(shadowSize);
}
static int getShadowTop(float shadowSize) {
return (int) Math.ceil(shadowSize / 2);
}
static int getShadowRight(float shadowSize) {
return (int) Math.ceil(shadowSize);
}
static int getShadowBottom(float shadowSize) {
return (int) Math.ceil(shadowSize * SHADOW_MULTIPLIER);
}
@Override
public void setColorFilter(ColorFilter cf) {
mCornerShadowTopPaint.setColorFilter(cf);
mCornerShadowBottomPaint.setColorFilter(cf);
mEdgeShadowPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public void draw(Canvas canvas) {
if (mDirty) {
buildShadow();
mDirty = false;
}
final Rect bounds = getBounds();
drawShadowCorners(canvas, bounds);
drawShadowEdges(canvas, bounds);
}
void setShadowStartColor(int shadowStartColor) {
if (mShadowStartColor == shadowStartColor) {
return;
}
mShadowStartColor = shadowStartColor;
mDirty = true;
invalidateSelf();
}
void setShadowEndColor(int shadowEndColor) {
if (mShadowEndColor == shadowEndColor) {
return;
}
mShadowEndColor = shadowEndColor;
mDirty = true;
invalidateSelf();
}
void setCornerRadius(float radius) {
radius = (int) (radius + .5f);
if (mCornerRadius == radius) {
return;
}
mCornerRadius = radius;
mDirty = true;
invalidateSelf();
}
void setShadowSize(float shadowSize) {
if (shadowSize < 0) {
throw new IllegalArgumentException("invalid shadow size");
}
shadowSize = toEven(shadowSize);
if (mRawShadowSize == shadowSize) {
return;
}
mRawShadowSize = shadowSize;
mShadowSize = (int) (shadowSize * SHADOW_MULTIPLIER + .5f);
mDirty = true;
invalidateSelf();
}
private void buildShadow() {
final int shadowHorizontal = getShadowHorizontal(mRawShadowSize);
final int shadowTop = getShadowTop(mRawShadowSize);
final int shadowBottom = getShadowBottom(mRawShadowSize);
final RectF topInnerBounds = new RectF(
getShadowHorizontal(mRawShadowSize),
getShadowTop(mRawShadowSize),
getShadowHorizontal(mRawShadowSize) + 2 * mCornerRadius,
getShadowTop(mRawShadowSize) + 2 * mCornerRadius);
final RectF topOuterBounds = new RectF(0, 0, 2 * mCornerRadius, 2 * mCornerRadius);
mCornerShadowTopPath.reset();
mCornerShadowTopPath.setFillType(Path.FillType.EVEN_ODD);
mCornerShadowTopPath.moveTo(shadowHorizontal + mCornerRadius, shadowTop);
mCornerShadowTopPath.arcTo(topInnerBounds, 270f, -90f, true);
mCornerShadowTopPath.rLineTo(-shadowHorizontal, 0);
mCornerShadowTopPath.lineTo(0, mCornerRadius);
mCornerShadowTopPath.arcTo(topOuterBounds, 180f, 90f, true);
mCornerShadowTopPath.lineTo(shadowHorizontal + mCornerRadius, 0);
mCornerShadowTopPath.rLineTo(0, shadowTop);
mCornerShadowTopPath.close();
mCornerShadowTopPaint.setShader(
new RadialGradient(
shadowHorizontal + mCornerRadius,
shadowTop + mCornerRadius + mRawShadowSize / 2,
mShadowSize,
new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
new float[]{0f, .2f, 1f},
Shader.TileMode.CLAMP));
final RectF bottomInnerBounds = new RectF(
getShadowHorizontal(mRawShadowSize),
getShadowBottom(mRawShadowSize),
getShadowHorizontal(mRawShadowSize) + 2 * mCornerRadius,
getShadowBottom(mRawShadowSize) + 2 * mCornerRadius);
final RectF bottomOuterBounds = new RectF(0, 0, 2 * mCornerRadius, 2 * mCornerRadius);
mCornerShadowBottomPath.reset();
mCornerShadowBottomPath.setFillType(Path.FillType.EVEN_ODD);
mCornerShadowBottomPath.moveTo(shadowHorizontal + mCornerRadius, shadowBottom);
mCornerShadowBottomPath.arcTo(bottomInnerBounds, 270f, -90f, true);
mCornerShadowBottomPath.rLineTo(-shadowHorizontal, 0);
mCornerShadowBottomPath.lineTo(0, mCornerRadius);
mCornerShadowBottomPath.arcTo(bottomOuterBounds, 180f, 90f, true);
mCornerShadowBottomPath.lineTo(shadowHorizontal + mCornerRadius, 0);
mCornerShadowBottomPath.rLineTo(0, shadowBottom);
mCornerShadowBottomPath.close();
mCornerShadowBottomPaint.setShader(
new RadialGradient(
shadowHorizontal + mShadowSize / 2,
shadowBottom + (mShadowSize / 2),
mShadowSize,
new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
new float[]{0f, .2f, 1f},
Shader.TileMode.CLAMP));
// We offset the content (shadowSize / 2) pixels up to make it more realistic.
// This is why edge shadow shader has some extra space. When drawing bottom edge
// shadow, we use that extra space.
mEdgeShadowPaint.setShader(
new LinearGradient(
0,
mCornerRadius + mShadowSize,
0,
0,
new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
new float[]{0f, .2f, 1f},
Shader.TileMode.CLAMP));
mEdgeShadowPaint.setAntiAlias(false);
}
private void drawShadowCorners(Canvas canvas, Rect bounds) {
// left-top
int saved = canvas.save();
canvas.translate(bounds.left, bounds.top);
canvas.drawPath(mCornerShadowTopPath, mCornerShadowTopPaint);
canvas.restoreToCount(saved);
// right-bottom
saved = canvas.save();
canvas.translate(bounds.right, bounds.bottom);
canvas.scale(-1f, -1f);
canvas.drawPath(mCornerShadowBottomPath, mCornerShadowBottomPaint);
canvas.restoreToCount(saved);
// left-bottom
saved = canvas.save();
canvas.translate(bounds.left, bounds.bottom);
canvas.scale(1f, -1f);
canvas.drawPath(mCornerShadowBottomPath, mCornerShadowBottomPaint);
canvas.restoreToCount(saved);
// right-top
saved = canvas.save();
canvas.translate(bounds.right, bounds.top);
canvas.scale(-1f, 1f);
canvas.drawPath(mCornerShadowTopPath, mCornerShadowTopPaint);
canvas.restoreToCount(saved);
}
private void drawShadowEdges(Canvas canvas, Rect bounds) {
final int paddingLeft = getShadowHorizontal(mRawShadowSize);
final int paddingTop = getShadowTop(mRawShadowSize);
final int paddingRight = getShadowRight(mRawShadowSize);
final int paddingBottom = getShadowBottom(mRawShadowSize);
// top
int saved = canvas.save();
canvas.translate(bounds.left, bounds.top);
canvas.drawRect(
paddingLeft + mCornerRadius,
0,
bounds.width() - mCornerRadius - paddingRight,
paddingTop,
mEdgeShadowPaint);
canvas.restoreToCount(saved);
// bottom
saved = canvas.save();
canvas.translate(bounds.right, bounds.bottom);
canvas.rotate(180f);
canvas.drawRect(
paddingRight + mCornerRadius,
0,
bounds.width() - mCornerRadius - paddingLeft,
paddingBottom,
mEdgeShadowPaint);
canvas.restoreToCount(saved);
// left
saved = canvas.save();
canvas.translate(bounds.left, bounds.bottom);
canvas.rotate(270f);
canvas.drawRect(
paddingBottom + mCornerRadius,
0,
bounds.height() - mCornerRadius - paddingTop,
paddingLeft,
mEdgeShadowPaint);
canvas.restoreToCount(saved);
// right
saved = canvas.save();
canvas.translate(bounds.right, bounds.top);
canvas.rotate(90f);
canvas.drawRect(
paddingTop + mCornerRadius,
0,
bounds.height() - mCornerRadius - paddingBottom,
paddingRight,
mEdgeShadowPaint);
canvas.restoreToCount(saved);
}
private static int toEven(float value) {
final int i = (int) (value + .5f);
if (i % 2 == 1) {
return i - 1;
}
return i;
}
}