1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.design.widget;
18
19import android.content.res.Resources;
20import android.graphics.Canvas;
21import android.graphics.LinearGradient;
22import android.graphics.Paint;
23import android.graphics.Path;
24import android.graphics.PixelFormat;
25import android.graphics.RadialGradient;
26import android.graphics.Rect;
27import android.graphics.RectF;
28import android.graphics.Shader;
29import android.graphics.drawable.Drawable;
30import android.support.design.R;
31import android.support.v7.graphics.drawable.DrawableWrapper;
32
33/**
34 * A {@link android.graphics.drawable.Drawable} which wraps another drawable and
35 * draws a shadow around it.
36 */
37class ShadowDrawableWrapper extends DrawableWrapper {
38    // used to calculate content padding
39    static final double COS_45 = Math.cos(Math.toRadians(45));
40
41    static final float SHADOW_MULTIPLIER = 1.5f;
42
43    static final float SHADOW_TOP_SCALE = 0.25f;
44    static final float SHADOW_HORIZ_SCALE = 0.5f;
45    static final float SHADOW_BOTTOM_SCALE = 1f;
46
47    final Paint mCornerShadowPaint;
48    final Paint mEdgeShadowPaint;
49
50    final RectF mContentBounds;
51
52    float mCornerRadius;
53
54    Path mCornerShadowPath;
55
56    // updated value with inset
57    float mMaxShadowSize;
58    // actual value set by developer
59    float mRawMaxShadowSize;
60
61    // multiplied value to account for shadow offset
62    float mShadowSize;
63    // actual value set by developer
64    float mRawShadowSize;
65
66    private boolean mDirty = true;
67
68    private final int mShadowStartColor;
69    private final int mShadowMiddleColor;
70    private final int mShadowEndColor;
71
72    private boolean mAddPaddingForCorners = true;
73
74    private float mRotation;
75
76    /**
77     * If shadow size is set to a value above max shadow, we print a warning
78     */
79    private boolean mPrintedShadowClipWarning = false;
80
81    public ShadowDrawableWrapper(Resources resources, Drawable content, float radius,
82            float shadowSize, float maxShadowSize) {
83        super(content);
84
85        mShadowStartColor = resources.getColor(R.color.design_fab_shadow_start_color);
86        mShadowMiddleColor = resources.getColor(R.color.design_fab_shadow_mid_color);
87        mShadowEndColor = resources.getColor(R.color.design_fab_shadow_end_color);
88
89        mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
90        mCornerShadowPaint.setStyle(Paint.Style.FILL);
91        mCornerRadius = Math.round(radius);
92        mContentBounds = new RectF();
93        mEdgeShadowPaint = new Paint(mCornerShadowPaint);
94        mEdgeShadowPaint.setAntiAlias(false);
95        setShadowSize(shadowSize, maxShadowSize);
96    }
97
98    /**
99     * Casts the value to an even integer.
100     */
101    private static int toEven(float value) {
102        int i = Math.round(value);
103        return (i % 2 == 1) ? i - 1 : i;
104    }
105
106    public void setAddPaddingForCorners(boolean addPaddingForCorners) {
107        mAddPaddingForCorners = addPaddingForCorners;
108        invalidateSelf();
109    }
110
111    @Override
112    public void setAlpha(int alpha) {
113        super.setAlpha(alpha);
114        mCornerShadowPaint.setAlpha(alpha);
115        mEdgeShadowPaint.setAlpha(alpha);
116    }
117
118    @Override
119    protected void onBoundsChange(Rect bounds) {
120        mDirty = true;
121    }
122
123    void setShadowSize(float shadowSize, float maxShadowSize) {
124        if (shadowSize < 0 || maxShadowSize < 0) {
125            throw new IllegalArgumentException("invalid shadow size");
126        }
127        shadowSize = toEven(shadowSize);
128        maxShadowSize = toEven(maxShadowSize);
129        if (shadowSize > maxShadowSize) {
130            shadowSize = maxShadowSize;
131            if (!mPrintedShadowClipWarning) {
132                mPrintedShadowClipWarning = true;
133            }
134        }
135        if (mRawShadowSize == shadowSize && mRawMaxShadowSize == maxShadowSize) {
136            return;
137        }
138        mRawShadowSize = shadowSize;
139        mRawMaxShadowSize = maxShadowSize;
140        mShadowSize = Math.round(shadowSize * SHADOW_MULTIPLIER);
141        mMaxShadowSize = maxShadowSize;
142        mDirty = true;
143        invalidateSelf();
144    }
145
146    @Override
147    public boolean getPadding(Rect padding) {
148        int vOffset = (int) Math.ceil(calculateVerticalPadding(mRawMaxShadowSize, mCornerRadius,
149                mAddPaddingForCorners));
150        int hOffset = (int) Math.ceil(calculateHorizontalPadding(mRawMaxShadowSize, mCornerRadius,
151                mAddPaddingForCorners));
152        padding.set(hOffset, vOffset, hOffset, vOffset);
153        return true;
154    }
155
156    public static float calculateVerticalPadding(float maxShadowSize, float cornerRadius,
157            boolean addPaddingForCorners) {
158        if (addPaddingForCorners) {
159            return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius);
160        } else {
161            return maxShadowSize * SHADOW_MULTIPLIER;
162        }
163    }
164
165    public static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius,
166            boolean addPaddingForCorners) {
167        if (addPaddingForCorners) {
168            return (float) (maxShadowSize + (1 - COS_45) * cornerRadius);
169        } else {
170            return maxShadowSize;
171        }
172    }
173
174    @Override
175    public int getOpacity() {
176        return PixelFormat.TRANSLUCENT;
177    }
178
179    public void setCornerRadius(float radius) {
180        radius = Math.round(radius);
181        if (mCornerRadius == radius) {
182            return;
183        }
184        mCornerRadius = radius;
185        mDirty = true;
186        invalidateSelf();
187    }
188
189    @Override
190    public void draw(Canvas canvas) {
191        if (mDirty) {
192            buildComponents(getBounds());
193            mDirty = false;
194        }
195        drawShadow(canvas);
196
197        super.draw(canvas);
198    }
199
200    final void setRotation(float rotation) {
201        if (mRotation != rotation) {
202            mRotation = rotation;
203            invalidateSelf();
204        }
205    }
206
207    private void drawShadow(Canvas canvas) {
208        final int rotateSaved = canvas.save();
209        canvas.rotate(mRotation, mContentBounds.centerX(), mContentBounds.centerY());
210
211        final float edgeShadowTop = -mCornerRadius - mShadowSize;
212        final float shadowOffset = mCornerRadius;
213        final boolean drawHorizontalEdges = mContentBounds.width() - 2 * shadowOffset > 0;
214        final boolean drawVerticalEdges = mContentBounds.height() - 2 * shadowOffset > 0;
215
216        final float shadowOffsetTop = mRawShadowSize - (mRawShadowSize * SHADOW_TOP_SCALE);
217        final float shadowOffsetHorizontal = mRawShadowSize - (mRawShadowSize * SHADOW_HORIZ_SCALE);
218        final float shadowOffsetBottom = mRawShadowSize - (mRawShadowSize * SHADOW_BOTTOM_SCALE);
219
220        final float shadowScaleHorizontal = shadowOffset / (shadowOffset + shadowOffsetHorizontal);
221        final float shadowScaleTop = shadowOffset / (shadowOffset + shadowOffsetTop);
222        final float shadowScaleBottom = shadowOffset / (shadowOffset + shadowOffsetBottom);
223
224        // LT
225        int saved = canvas.save();
226        canvas.translate(mContentBounds.left + shadowOffset, mContentBounds.top + shadowOffset);
227        canvas.scale(shadowScaleHorizontal, shadowScaleTop);
228        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
229        if (drawHorizontalEdges) {
230            // TE
231            canvas.scale(1f / shadowScaleHorizontal, 1f);
232            canvas.drawRect(0, edgeShadowTop,
233                    mContentBounds.width() - 2 * shadowOffset, -mCornerRadius,
234                    mEdgeShadowPaint);
235        }
236        canvas.restoreToCount(saved);
237        // RB
238        saved = canvas.save();
239        canvas.translate(mContentBounds.right - shadowOffset, mContentBounds.bottom - shadowOffset);
240        canvas.scale(shadowScaleHorizontal, shadowScaleBottom);
241        canvas.rotate(180f);
242        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
243        if (drawHorizontalEdges) {
244            // BE
245            canvas.scale(1f / shadowScaleHorizontal, 1f);
246            canvas.drawRect(0, edgeShadowTop,
247                    mContentBounds.width() - 2 * shadowOffset, -mCornerRadius + mShadowSize,
248                    mEdgeShadowPaint);
249        }
250        canvas.restoreToCount(saved);
251        // LB
252        saved = canvas.save();
253        canvas.translate(mContentBounds.left + shadowOffset, mContentBounds.bottom - shadowOffset);
254        canvas.scale(shadowScaleHorizontal, shadowScaleBottom);
255        canvas.rotate(270f);
256        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
257        if (drawVerticalEdges) {
258            // LE
259            canvas.scale(1f / shadowScaleBottom, 1f);
260            canvas.drawRect(0, edgeShadowTop,
261                    mContentBounds.height() - 2 * shadowOffset, -mCornerRadius, mEdgeShadowPaint);
262        }
263        canvas.restoreToCount(saved);
264        // RT
265        saved = canvas.save();
266        canvas.translate(mContentBounds.right - shadowOffset, mContentBounds.top + shadowOffset);
267        canvas.scale(shadowScaleHorizontal, shadowScaleTop);
268        canvas.rotate(90f);
269        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
270        if (drawVerticalEdges) {
271            // RE
272            canvas.scale(1f / shadowScaleTop, 1f);
273            canvas.drawRect(0, edgeShadowTop,
274                    mContentBounds.height() - 2 * shadowOffset, -mCornerRadius, mEdgeShadowPaint);
275        }
276        canvas.restoreToCount(saved);
277
278        canvas.restoreToCount(rotateSaved);
279    }
280
281    private void buildShadowCorners() {
282        RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
283        RectF outerBounds = new RectF(innerBounds);
284        outerBounds.inset(-mShadowSize, -mShadowSize);
285
286        if (mCornerShadowPath == null) {
287            mCornerShadowPath = new Path();
288        } else {
289            mCornerShadowPath.reset();
290        }
291        mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
292        mCornerShadowPath.moveTo(-mCornerRadius, 0);
293        mCornerShadowPath.rLineTo(-mShadowSize, 0);
294        // outer arc
295        mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
296        // inner arc
297        mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
298        mCornerShadowPath.close();
299
300        float shadowRadius = -outerBounds.top;
301        if (shadowRadius > 0f) {
302            float startRatio = mCornerRadius / shadowRadius;
303            float midRatio = startRatio + ((1f - startRatio) / 2f);
304            mCornerShadowPaint.setShader(new RadialGradient(0, 0, shadowRadius,
305                    new int[]{0, mShadowStartColor, mShadowMiddleColor, mShadowEndColor},
306                    new float[]{0f, startRatio, midRatio, 1f},
307                    Shader.TileMode.CLAMP));
308        }
309
310        // we offset the content shadowSize/2 pixels up to make it more realistic.
311        // this is why edge shadow shader has some extra space
312        // When drawing bottom edge shadow, we use that extra space.
313        mEdgeShadowPaint.setShader(new LinearGradient(0, innerBounds.top, 0, outerBounds.top,
314                new int[]{mShadowStartColor, mShadowMiddleColor, mShadowEndColor},
315                new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
316        mEdgeShadowPaint.setAntiAlias(false);
317    }
318
319    private void buildComponents(Rect bounds) {
320        // Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift.
321        // We could have different top-bottom offsets to avoid extra gap above but in that case
322        // center aligning Views inside the CardView would be problematic.
323        final float verticalOffset = mRawMaxShadowSize * SHADOW_MULTIPLIER;
324        mContentBounds.set(bounds.left + mRawMaxShadowSize, bounds.top + verticalOffset,
325                bounds.right - mRawMaxShadowSize, bounds.bottom - verticalOffset);
326
327        getWrappedDrawable().setBounds((int) mContentBounds.left, (int) mContentBounds.top,
328                (int) mContentBounds.right, (int) mContentBounds.bottom);
329
330        buildShadowCorners();
331    }
332
333    public float getCornerRadius() {
334        return mCornerRadius;
335    }
336
337    public void setShadowSize(float size) {
338        setShadowSize(size, mRawMaxShadowSize);
339    }
340
341    public void setMaxShadowSize(float size) {
342        setShadowSize(mRawShadowSize, size);
343    }
344
345    public float getShadowSize() {
346        return mRawShadowSize;
347    }
348
349    public float getMaxShadowSize() {
350        return mRawMaxShadowSize;
351    }
352
353    public float getMinWidth() {
354        final float content = 2 *
355                Math.max(mRawMaxShadowSize, mCornerRadius + mRawMaxShadowSize / 2);
356        return content + mRawMaxShadowSize * 2;
357    }
358
359    public float getMinHeight() {
360        final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius
361                + mRawMaxShadowSize * SHADOW_MULTIPLIER / 2);
362        return content + (mRawMaxShadowSize * SHADOW_MULTIPLIER) * 2;
363    }
364}
365