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