| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed 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 android.support.design.widget; |
| |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| 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; |
| import android.support.design.R; |
| import android.support.v7.graphics.drawable.DrawableWrapper; |
| |
| /** |
| * A {@link android.graphics.drawable.Drawable} which wraps another drawable and |
| * draws a shadow around it. |
| */ |
| class ShadowDrawableWrapper extends DrawableWrapper { |
| // used to calculate content padding |
| static final double COS_45 = Math.cos(Math.toRadians(45)); |
| |
| static final float SHADOW_MULTIPLIER = 1.5f; |
| |
| static final float SHADOW_TOP_SCALE = 0.25f; |
| static final float SHADOW_HORIZ_SCALE = 0.5f; |
| static final float SHADOW_BOTTOM_SCALE = 1f; |
| |
| final Paint mCornerShadowPaint; |
| final Paint mEdgeShadowPaint; |
| |
| final RectF mContentBounds; |
| |
| float mCornerRadius; |
| |
| Path mCornerShadowPath; |
| |
| // updated value with inset |
| float mMaxShadowSize; |
| // actual value set by developer |
| float mRawMaxShadowSize; |
| |
| // multiplied value to account for shadow offset |
| float mShadowSize; |
| // actual value set by developer |
| float mRawShadowSize; |
| |
| private boolean mDirty = true; |
| |
| private final int mShadowStartColor; |
| private final int mShadowMiddleColor; |
| private final int mShadowEndColor; |
| |
| private boolean mAddPaddingForCorners = true; |
| |
| private float mRotation; |
| |
| /** |
| * If shadow size is set to a value above max shadow, we print a warning |
| */ |
| private boolean mPrintedShadowClipWarning = false; |
| |
| public ShadowDrawableWrapper(Resources resources, Drawable content, float radius, |
| float shadowSize, float maxShadowSize) { |
| super(content); |
| |
| mShadowStartColor = resources.getColor(R.color.design_fab_shadow_start_color); |
| mShadowMiddleColor = resources.getColor(R.color.design_fab_shadow_mid_color); |
| mShadowEndColor = resources.getColor(R.color.design_fab_shadow_end_color); |
| |
| mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); |
| mCornerShadowPaint.setStyle(Paint.Style.FILL); |
| mCornerRadius = Math.round(radius); |
| mContentBounds = new RectF(); |
| mEdgeShadowPaint = new Paint(mCornerShadowPaint); |
| mEdgeShadowPaint.setAntiAlias(false); |
| setShadowSize(shadowSize, maxShadowSize); |
| } |
| |
| /** |
| * Casts the value to an even integer. |
| */ |
| private static int toEven(float value) { |
| int i = Math.round(value); |
| return (i % 2 == 1) ? i - 1 : i; |
| } |
| |
| public void setAddPaddingForCorners(boolean addPaddingForCorners) { |
| mAddPaddingForCorners = addPaddingForCorners; |
| invalidateSelf(); |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| super.setAlpha(alpha); |
| mCornerShadowPaint.setAlpha(alpha); |
| mEdgeShadowPaint.setAlpha(alpha); |
| } |
| |
| @Override |
| protected void onBoundsChange(Rect bounds) { |
| mDirty = true; |
| } |
| |
| void setShadowSize(float shadowSize, float maxShadowSize) { |
| if (shadowSize < 0 || maxShadowSize < 0) { |
| throw new IllegalArgumentException("invalid shadow size"); |
| } |
| shadowSize = toEven(shadowSize); |
| maxShadowSize = toEven(maxShadowSize); |
| if (shadowSize > maxShadowSize) { |
| shadowSize = maxShadowSize; |
| if (!mPrintedShadowClipWarning) { |
| mPrintedShadowClipWarning = true; |
| } |
| } |
| if (mRawShadowSize == shadowSize && mRawMaxShadowSize == maxShadowSize) { |
| return; |
| } |
| mRawShadowSize = shadowSize; |
| mRawMaxShadowSize = maxShadowSize; |
| mShadowSize = Math.round(shadowSize * SHADOW_MULTIPLIER); |
| mMaxShadowSize = maxShadowSize; |
| mDirty = true; |
| invalidateSelf(); |
| } |
| |
| @Override |
| public boolean getPadding(Rect padding) { |
| int vOffset = (int) Math.ceil(calculateVerticalPadding(mRawMaxShadowSize, mCornerRadius, |
| mAddPaddingForCorners)); |
| int hOffset = (int) Math.ceil(calculateHorizontalPadding(mRawMaxShadowSize, mCornerRadius, |
| mAddPaddingForCorners)); |
| padding.set(hOffset, vOffset, hOffset, vOffset); |
| return true; |
| } |
| |
| public static float calculateVerticalPadding(float maxShadowSize, float cornerRadius, |
| boolean addPaddingForCorners) { |
| if (addPaddingForCorners) { |
| return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius); |
| } else { |
| return maxShadowSize * SHADOW_MULTIPLIER; |
| } |
| } |
| |
| public static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius, |
| boolean addPaddingForCorners) { |
| if (addPaddingForCorners) { |
| return (float) (maxShadowSize + (1 - COS_45) * cornerRadius); |
| } else { |
| return maxShadowSize; |
| } |
| } |
| |
| @Override |
| public int getOpacity() { |
| return PixelFormat.TRANSLUCENT; |
| } |
| |
| public void setCornerRadius(float radius) { |
| radius = Math.round(radius); |
| if (mCornerRadius == radius) { |
| return; |
| } |
| mCornerRadius = radius; |
| mDirty = true; |
| invalidateSelf(); |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| if (mDirty) { |
| buildComponents(getBounds()); |
| mDirty = false; |
| } |
| drawShadow(canvas); |
| |
| super.draw(canvas); |
| } |
| |
| final void setRotation(float rotation) { |
| if (mRotation != rotation) { |
| mRotation = rotation; |
| invalidateSelf(); |
| } |
| } |
| |
| private void drawShadow(Canvas canvas) { |
| final int rotateSaved = canvas.save(); |
| canvas.rotate(mRotation, mContentBounds.centerX(), mContentBounds.centerY()); |
| |
| final float edgeShadowTop = -mCornerRadius - mShadowSize; |
| final float shadowOffset = mCornerRadius; |
| final boolean drawHorizontalEdges = mContentBounds.width() - 2 * shadowOffset > 0; |
| final boolean drawVerticalEdges = mContentBounds.height() - 2 * shadowOffset > 0; |
| |
| final float shadowOffsetTop = mRawShadowSize - (mRawShadowSize * SHADOW_TOP_SCALE); |
| final float shadowOffsetHorizontal = mRawShadowSize - (mRawShadowSize * SHADOW_HORIZ_SCALE); |
| final float shadowOffsetBottom = mRawShadowSize - (mRawShadowSize * SHADOW_BOTTOM_SCALE); |
| |
| final float shadowScaleHorizontal = shadowOffset / (shadowOffset + shadowOffsetHorizontal); |
| final float shadowScaleTop = shadowOffset / (shadowOffset + shadowOffsetTop); |
| final float shadowScaleBottom = shadowOffset / (shadowOffset + shadowOffsetBottom); |
| |
| // LT |
| int saved = canvas.save(); |
| canvas.translate(mContentBounds.left + shadowOffset, mContentBounds.top + shadowOffset); |
| canvas.scale(shadowScaleHorizontal, shadowScaleTop); |
| canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); |
| if (drawHorizontalEdges) { |
| // TE |
| canvas.scale(1f / shadowScaleHorizontal, 1f); |
| canvas.drawRect(0, edgeShadowTop, |
| mContentBounds.width() - 2 * shadowOffset, -mCornerRadius, |
| mEdgeShadowPaint); |
| } |
| canvas.restoreToCount(saved); |
| // RB |
| saved = canvas.save(); |
| canvas.translate(mContentBounds.right - shadowOffset, mContentBounds.bottom - shadowOffset); |
| canvas.scale(shadowScaleHorizontal, shadowScaleBottom); |
| canvas.rotate(180f); |
| canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); |
| if (drawHorizontalEdges) { |
| // BE |
| canvas.scale(1f / shadowScaleHorizontal, 1f); |
| canvas.drawRect(0, edgeShadowTop, |
| mContentBounds.width() - 2 * shadowOffset, -mCornerRadius + mShadowSize, |
| mEdgeShadowPaint); |
| } |
| canvas.restoreToCount(saved); |
| // LB |
| saved = canvas.save(); |
| canvas.translate(mContentBounds.left + shadowOffset, mContentBounds.bottom - shadowOffset); |
| canvas.scale(shadowScaleHorizontal, shadowScaleBottom); |
| canvas.rotate(270f); |
| canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); |
| if (drawVerticalEdges) { |
| // LE |
| canvas.scale(1f / shadowScaleBottom, 1f); |
| canvas.drawRect(0, edgeShadowTop, |
| mContentBounds.height() - 2 * shadowOffset, -mCornerRadius, mEdgeShadowPaint); |
| } |
| canvas.restoreToCount(saved); |
| // RT |
| saved = canvas.save(); |
| canvas.translate(mContentBounds.right - shadowOffset, mContentBounds.top + shadowOffset); |
| canvas.scale(shadowScaleHorizontal, shadowScaleTop); |
| canvas.rotate(90f); |
| canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); |
| if (drawVerticalEdges) { |
| // RE |
| canvas.scale(1f / shadowScaleTop, 1f); |
| canvas.drawRect(0, edgeShadowTop, |
| mContentBounds.height() - 2 * shadowOffset, -mCornerRadius, mEdgeShadowPaint); |
| } |
| canvas.restoreToCount(saved); |
| |
| canvas.restoreToCount(rotateSaved); |
| } |
| |
| private void buildShadowCorners() { |
| RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius); |
| RectF outerBounds = new RectF(innerBounds); |
| outerBounds.inset(-mShadowSize, -mShadowSize); |
| |
| if (mCornerShadowPath == null) { |
| mCornerShadowPath = new Path(); |
| } else { |
| mCornerShadowPath.reset(); |
| } |
| mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD); |
| mCornerShadowPath.moveTo(-mCornerRadius, 0); |
| mCornerShadowPath.rLineTo(-mShadowSize, 0); |
| // outer arc |
| mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false); |
| // inner arc |
| mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false); |
| mCornerShadowPath.close(); |
| |
| float shadowRadius = -outerBounds.top; |
| if (shadowRadius > 0f) { |
| float startRatio = mCornerRadius / shadowRadius; |
| float midRatio = startRatio + ((1f - startRatio) / 2f); |
| mCornerShadowPaint.setShader(new RadialGradient(0, 0, shadowRadius, |
| new int[]{0, mShadowStartColor, mShadowMiddleColor, mShadowEndColor}, |
| new float[]{0f, startRatio, midRatio, 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, innerBounds.top, 0, outerBounds.top, |
| new int[]{mShadowStartColor, mShadowMiddleColor, mShadowEndColor}, |
| new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP)); |
| mEdgeShadowPaint.setAntiAlias(false); |
| } |
| |
| private void buildComponents(Rect bounds) { |
| // Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift. |
| // We could have different top-bottom offsets to avoid extra gap above but in that case |
| // center aligning Views inside the CardView would be problematic. |
| final float verticalOffset = mRawMaxShadowSize * SHADOW_MULTIPLIER; |
| mContentBounds.set(bounds.left + mRawMaxShadowSize, bounds.top + verticalOffset, |
| bounds.right - mRawMaxShadowSize, bounds.bottom - verticalOffset); |
| |
| getWrappedDrawable().setBounds((int) mContentBounds.left, (int) mContentBounds.top, |
| (int) mContentBounds.right, (int) mContentBounds.bottom); |
| |
| buildShadowCorners(); |
| } |
| |
| public float getCornerRadius() { |
| return mCornerRadius; |
| } |
| |
| public void setShadowSize(float size) { |
| setShadowSize(size, mRawMaxShadowSize); |
| } |
| |
| public void setMaxShadowSize(float size) { |
| setShadowSize(mRawShadowSize, size); |
| } |
| |
| public float getShadowSize() { |
| return mRawShadowSize; |
| } |
| |
| public float getMaxShadowSize() { |
| return mRawMaxShadowSize; |
| } |
| |
| public float getMinWidth() { |
| final float content = 2 * |
| Math.max(mRawMaxShadowSize, mCornerRadius + mRawMaxShadowSize / 2); |
| return content + mRawMaxShadowSize * 2; |
| } |
| |
| public float getMinHeight() { |
| final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius |
| + mRawMaxShadowSize * SHADOW_MULTIPLIER / 2); |
| return content + (mRawMaxShadowSize * SHADOW_MULTIPLIER) * 2; |
| } |
| } |