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 */
16package android.support.v7.widget;
17
18import android.content.res.Resources;
19import android.graphics.Canvas;
20import android.graphics.ColorFilter;
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.v7.cardview.R;
31
32/**
33 * A rounded rectangle drawable which also includes a shadow around.
34 */
35class RoundRectDrawableWithShadow extends Drawable {
36    // used to calculate content padding
37    final static double COS_45 = Math.cos(Math.toRadians(45));
38
39    final static float SHADOW_MULTIPLIER = 1.5f;
40
41    final int mInsetShadow; // extra shadow to avoid gaps between card and shadow
42
43    /*
44    * This helper is set by CardView implementations.
45    * <p>
46    * Prior to API 17, canvas.drawRoundRect is expensive; which is why we need this interface
47    * to draw efficient rounded rectangles before 17.
48    * */
49    static RoundRectHelper sRoundRectHelper;
50
51    Paint mPaint;
52
53    Paint mCornerShadowPaint;
54
55    Paint mEdgeShadowPaint;
56
57    final RectF mCardBounds;
58
59    float mCornerRadius;
60
61    Path mCornerShadowPath;
62
63    // updated value with inset
64    float mMaxShadowSize;
65
66    // actual value set by developer
67    float mRawMaxShadowSize;
68
69    // multiplied value to account for shadow offset
70    float mShadowSize;
71
72    // actual value set by developer
73    float mRawShadowSize;
74
75    private boolean mDirty = true;
76
77    private final int mShadowStartColor;
78
79    private final int mShadowEndColor;
80
81    private boolean mAddPaddingForCorners = true;
82
83    /**
84     * If shadow size is set to a value above max shadow, we print a warning
85     */
86    private boolean mPrintedShadowClipWarning = false;
87
88    RoundRectDrawableWithShadow(Resources resources, int backgroundColor, float radius,
89            float shadowSize, float maxShadowSize) {
90        mShadowStartColor = resources.getColor(R.color.cardview_shadow_start_color);
91        mShadowEndColor = resources.getColor(R.color.cardview_shadow_end_color);
92        mInsetShadow = resources.getDimensionPixelSize(R.dimen.cardview_compat_inset_shadow);
93        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
94        mPaint.setColor(backgroundColor);
95        mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
96        mCornerShadowPaint.setStyle(Paint.Style.FILL);
97        mCornerRadius = (int) (radius + .5f);
98        mCardBounds = new RectF();
99        mEdgeShadowPaint = new Paint(mCornerShadowPaint);
100        mEdgeShadowPaint.setAntiAlias(false);
101        setShadowSize(shadowSize, maxShadowSize);
102    }
103
104    /**
105     * Casts the value to an even integer.
106     */
107    private int toEven(float value) {
108        int i = (int) (value + .5f);
109        if (i % 2 == 1) {
110            return i - 1;
111        }
112        return i;
113    }
114
115    public void setAddPaddingForCorners(boolean addPaddingForCorners) {
116        mAddPaddingForCorners = addPaddingForCorners;
117        invalidateSelf();
118    }
119
120    @Override
121    public void setAlpha(int alpha) {
122        mPaint.setAlpha(alpha);
123        mCornerShadowPaint.setAlpha(alpha);
124        mEdgeShadowPaint.setAlpha(alpha);
125    }
126
127    @Override
128    protected void onBoundsChange(Rect bounds) {
129        super.onBoundsChange(bounds);
130        mDirty = true;
131    }
132
133    void setShadowSize(float shadowSize, float maxShadowSize) {
134        if (shadowSize < 0 || maxShadowSize < 0) {
135            throw new IllegalArgumentException("invalid shadow size");
136        }
137        shadowSize = toEven(shadowSize);
138        maxShadowSize = toEven(maxShadowSize);
139        if (shadowSize > maxShadowSize) {
140            shadowSize = maxShadowSize;
141            if (!mPrintedShadowClipWarning) {
142                mPrintedShadowClipWarning = true;
143            }
144        }
145        if (mRawShadowSize == shadowSize && mRawMaxShadowSize == maxShadowSize) {
146            return;
147        }
148        mRawShadowSize = shadowSize;
149        mRawMaxShadowSize = maxShadowSize;
150        mShadowSize = (int)(shadowSize * SHADOW_MULTIPLIER + mInsetShadow + .5f);
151        mMaxShadowSize = maxShadowSize + mInsetShadow;
152        mDirty = true;
153        invalidateSelf();
154    }
155
156    @Override
157    public boolean getPadding(Rect padding) {
158        int vOffset = (int) Math.ceil(calculateVerticalPadding(mRawMaxShadowSize, mCornerRadius,
159                mAddPaddingForCorners));
160        int hOffset = (int) Math.ceil(calculateHorizontalPadding(mRawMaxShadowSize, mCornerRadius,
161                mAddPaddingForCorners));
162        padding.set(hOffset, vOffset, hOffset, vOffset);
163        return true;
164    }
165
166    static float calculateVerticalPadding(float maxShadowSize, float cornerRadius,
167            boolean addPaddingForCorners) {
168        if (addPaddingForCorners) {
169            return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius);
170        } else {
171            return maxShadowSize * SHADOW_MULTIPLIER;
172        }
173    }
174
175    static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius,
176            boolean addPaddingForCorners) {
177        if (addPaddingForCorners) {
178            return (float) (maxShadowSize + (1 - COS_45) * cornerRadius);
179        } else {
180            return maxShadowSize;
181        }
182    }
183
184    @Override
185    public void setColorFilter(ColorFilter cf) {
186        mPaint.setColorFilter(cf);
187        mCornerShadowPaint.setColorFilter(cf);
188        mEdgeShadowPaint.setColorFilter(cf);
189    }
190
191    @Override
192    public int getOpacity() {
193        return PixelFormat.TRANSLUCENT;
194    }
195
196    void setCornerRadius(float radius) {
197        radius = (int) (radius + .5f);
198        if (mCornerRadius == radius) {
199            return;
200        }
201        mCornerRadius = radius;
202        mDirty = true;
203        invalidateSelf();
204    }
205
206    @Override
207    public void draw(Canvas canvas) {
208        if (mDirty) {
209            buildComponents(getBounds());
210            mDirty = false;
211        }
212        canvas.translate(0, mRawShadowSize / 2);
213        drawShadow(canvas);
214        canvas.translate(0, -mRawShadowSize / 2);
215        sRoundRectHelper.drawRoundRect(canvas, mCardBounds, mCornerRadius, mPaint);
216    }
217
218    private void drawShadow(Canvas canvas) {
219        final float edgeShadowTop = -mCornerRadius - mShadowSize;
220        final float inset = mCornerRadius + mInsetShadow + mRawShadowSize / 2;
221        final boolean drawHorizontalEdges = mCardBounds.width() - 2 * inset > 0;
222        final boolean drawVerticalEdges = mCardBounds.height() - 2 * inset > 0;
223        // LT
224        int saved = canvas.save();
225        canvas.translate(mCardBounds.left + inset, mCardBounds.top + inset);
226        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
227        if (drawHorizontalEdges) {
228            canvas.drawRect(0, edgeShadowTop,
229                    mCardBounds.width() - 2 * inset, -mCornerRadius,
230                    mEdgeShadowPaint);
231        }
232        canvas.restoreToCount(saved);
233        // RB
234        saved = canvas.save();
235        canvas.translate(mCardBounds.right - inset, mCardBounds.bottom - inset);
236        canvas.rotate(180f);
237        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
238        if (drawHorizontalEdges) {
239            canvas.drawRect(0, edgeShadowTop,
240                    mCardBounds.width() - 2 * inset, -mCornerRadius + mShadowSize,
241                    mEdgeShadowPaint);
242        }
243        canvas.restoreToCount(saved);
244        // LB
245        saved = canvas.save();
246        canvas.translate(mCardBounds.left + inset, mCardBounds.bottom - inset);
247        canvas.rotate(270f);
248        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
249        if (drawVerticalEdges) {
250            canvas.drawRect(0, edgeShadowTop,
251                    mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint);
252        }
253        canvas.restoreToCount(saved);
254        // RT
255        saved = canvas.save();
256        canvas.translate(mCardBounds.right - inset, mCardBounds.top + inset);
257        canvas.rotate(90f);
258        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
259        if (drawVerticalEdges) {
260            canvas.drawRect(0, edgeShadowTop,
261                    mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint);
262        }
263        canvas.restoreToCount(saved);
264    }
265
266    private void buildShadowCorners() {
267        RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
268        RectF outerBounds = new RectF(innerBounds);
269        outerBounds.inset(-mShadowSize, -mShadowSize);
270
271        if (mCornerShadowPath == null) {
272            mCornerShadowPath = new Path();
273        } else {
274            mCornerShadowPath.reset();
275        }
276        mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
277        mCornerShadowPath.moveTo(-mCornerRadius, 0);
278        mCornerShadowPath.rLineTo(-mShadowSize, 0);
279        // outer arc
280        mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
281        // inner arc
282        mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
283        mCornerShadowPath.close();
284        float startRatio = mCornerRadius / (mCornerRadius + mShadowSize);
285        mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize,
286                new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
287                new float[]{0f, startRatio, 1f}
288                , Shader.TileMode.CLAMP));
289
290        // we offset the content shadowSize/2 pixels up to make it more realistic.
291        // this is why edge shadow shader has some extra space
292        // When drawing bottom edge shadow, we use that extra space.
293        mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0,
294                -mCornerRadius - mShadowSize,
295                new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
296                new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
297        mEdgeShadowPaint.setAntiAlias(false);
298    }
299
300    private void buildComponents(Rect bounds) {
301        // Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift.
302        // We could have different top-bottom offsets to avoid extra gap above but in that case
303        // center aligning Views inside the CardView would be problematic.
304        final float verticalOffset = mRawMaxShadowSize * SHADOW_MULTIPLIER;
305        mCardBounds.set(bounds.left + mRawMaxShadowSize, bounds.top + verticalOffset,
306                bounds.right - mRawMaxShadowSize, bounds.bottom - verticalOffset);
307        buildShadowCorners();
308    }
309
310    float getCornerRadius() {
311        return mCornerRadius;
312    }
313
314    void getMaxShadowAndCornerPadding(Rect into) {
315        getPadding(into);
316    }
317
318    void setShadowSize(float size) {
319        setShadowSize(size, mRawMaxShadowSize);
320    }
321
322    void setMaxShadowSize(float size) {
323        setShadowSize(mRawShadowSize, size);
324    }
325
326    float getShadowSize() {
327        return mRawShadowSize;
328    }
329
330    float getMaxShadowSize() {
331        return mRawMaxShadowSize;
332    }
333
334    float getMinWidth() {
335        final float content = 2 *
336                Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow + mRawMaxShadowSize / 2);
337        return content + (mRawMaxShadowSize + mInsetShadow) * 2;
338    }
339
340    float getMinHeight() {
341        final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow
342                        + mRawMaxShadowSize * SHADOW_MULTIPLIER / 2);
343        return content + (mRawMaxShadowSize * SHADOW_MULTIPLIER + mInsetShadow) * 2;
344    }
345
346    public void setColor(int color) {
347        mPaint.setColor(color);
348        invalidateSelf();
349    }
350
351    static interface RoundRectHelper {
352        void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius, Paint paint);
353    }
354}
355