[go: nahoru, domu]

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