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 com.android.systemui.recents.views;
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.util.Log;
31
32import com.android.systemui.R;
33import com.android.systemui.recents.RecentsConfiguration;
34
35/**
36 * A rounded rectangle drawable which also includes a shadow around. This is mostly copied from
37 * frameworks/support/v7/cardview/eclair-mr1/android/support/v7/widget/
38 * RoundRectDrawableWithShadow.java revision c42ba8c000d1e6ce85e152dfc17089a0a69e739f with a few
39 * modifications to suit our needs in SystemUI.
40 */
41class FakeShadowDrawable extends Drawable {
42    // used to calculate content padding
43    final static double COS_45 = Math.cos(Math.toRadians(45));
44
45    final static float SHADOW_MULTIPLIER = 1.5f;
46
47    final float mInsetShadow; // extra shadow to avoid gaps between card and shadow
48
49    Paint mCornerShadowPaint;
50
51    Paint mEdgeShadowPaint;
52
53    final RectF mCardBounds;
54
55    float mCornerRadius;
56
57    Path mCornerShadowPath;
58
59    // updated value with inset
60    float mMaxShadowSize;
61
62    // actual value set by developer
63    float mRawMaxShadowSize;
64
65    // multiplied value to account for shadow offset
66    float mShadowSize;
67
68    // actual value set by developer
69    float mRawShadowSize;
70
71    private boolean mDirty = true;
72
73    private final int mShadowStartColor;
74
75    private final int mShadowEndColor;
76
77    private boolean mAddPaddingForCorners = true;
78
79    /**
80     * If shadow size is set to a value above max shadow, we print a warning
81     */
82    private boolean mPrintedShadowClipWarning = false;
83
84    public FakeShadowDrawable(Resources resources, RecentsConfiguration config) {
85        mShadowStartColor = resources.getColor(R.color.fake_shadow_start_color);
86        mShadowEndColor = resources.getColor(R.color.fake_shadow_end_color);
87        mInsetShadow = resources.getDimension(R.dimen.fake_shadow_inset);
88        setShadowSize(resources.getDimensionPixelSize(R.dimen.fake_shadow_size),
89                resources.getDimensionPixelSize(R.dimen.fake_shadow_size));
90        mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
91        mCornerShadowPaint.setStyle(Paint.Style.FILL);
92        mCornerShadowPaint.setDither(true);
93        mCornerRadius = resources.getDimensionPixelSize(
94                R.dimen.recents_task_view_rounded_corners_radius);
95        mCardBounds = new RectF();
96        mEdgeShadowPaint = new Paint(mCornerShadowPaint);
97    }
98
99    @Override
100    public void setAlpha(int alpha) {
101        mCornerShadowPaint.setAlpha(alpha);
102        mEdgeShadowPaint.setAlpha(alpha);
103    }
104
105    @Override
106    protected void onBoundsChange(Rect bounds) {
107        super.onBoundsChange(bounds);
108        mDirty = true;
109    }
110
111    void setShadowSize(float shadowSize, float maxShadowSize) {
112        if (shadowSize < 0 || maxShadowSize < 0) {
113            throw new IllegalArgumentException("invalid shadow size");
114        }
115        if (shadowSize > maxShadowSize) {
116            shadowSize = maxShadowSize;
117            if (!mPrintedShadowClipWarning) {
118                Log.w("CardView", "Shadow size is being clipped by the max shadow size. See "
119                        + "{CardView#setMaxCardElevation}.");
120                mPrintedShadowClipWarning = true;
121            }
122        }
123        if (mRawShadowSize == shadowSize && mRawMaxShadowSize == maxShadowSize) {
124            return;
125        }
126        mRawShadowSize = shadowSize;
127        mRawMaxShadowSize = maxShadowSize;
128        mShadowSize = shadowSize * SHADOW_MULTIPLIER + mInsetShadow;
129        mMaxShadowSize = maxShadowSize + mInsetShadow;
130        mDirty = true;
131        invalidateSelf();
132    }
133
134    @Override
135    public boolean getPadding(Rect padding) {
136        int vOffset = (int) Math.ceil(calculateVerticalPadding(mRawMaxShadowSize, mCornerRadius,
137                mAddPaddingForCorners));
138        int hOffset = (int) Math.ceil(calculateHorizontalPadding(mRawMaxShadowSize, mCornerRadius,
139                mAddPaddingForCorners));
140        padding.set(hOffset, vOffset, hOffset, vOffset);
141        return true;
142    }
143
144    static float calculateVerticalPadding(float maxShadowSize, float cornerRadius,
145            boolean addPaddingForCorners) {
146        if (addPaddingForCorners) {
147            return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius);
148        } else {
149            return maxShadowSize * SHADOW_MULTIPLIER;
150        }
151    }
152
153    static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius,
154            boolean addPaddingForCorners) {
155        if (addPaddingForCorners) {
156            return (float) (maxShadowSize + (1 - COS_45) * cornerRadius);
157        } else {
158            return maxShadowSize;
159        }
160    }
161
162    @Override
163    public void setColorFilter(ColorFilter colorFilter) {
164        mCornerShadowPaint.setColorFilter(colorFilter);
165        mEdgeShadowPaint.setColorFilter(colorFilter);
166    }
167
168    @Override
169    public int getOpacity() {
170        return PixelFormat.OPAQUE;
171    }
172
173    @Override
174    public void draw(Canvas canvas) {
175        if (mDirty) {
176            buildComponents(getBounds());
177            mDirty = false;
178        }
179        canvas.translate(0, mRawShadowSize / 4);
180        drawShadow(canvas);
181        canvas.translate(0, -mRawShadowSize / 4);
182    }
183
184    private void drawShadow(Canvas canvas) {
185        final float edgeShadowTop = -mCornerRadius - mShadowSize;
186        final float inset = mCornerRadius + mInsetShadow + mRawShadowSize / 2;
187        final boolean drawHorizontalEdges = mCardBounds.width() - 2 * inset > 0;
188        final boolean drawVerticalEdges = mCardBounds.height() - 2 * inset > 0;
189        // LT
190        int saved = canvas.save();
191        canvas.translate(mCardBounds.left + inset, mCardBounds.top + inset);
192        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
193        if (drawHorizontalEdges) {
194            canvas.drawRect(0, edgeShadowTop,
195                    mCardBounds.width() - 2 * inset, -mCornerRadius,
196                    mEdgeShadowPaint);
197        }
198        canvas.restoreToCount(saved);
199        // RB
200        saved = canvas.save();
201        canvas.translate(mCardBounds.right - inset, mCardBounds.bottom - inset);
202        canvas.rotate(180f);
203        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
204        if (drawHorizontalEdges) {
205            canvas.drawRect(0, edgeShadowTop,
206                    mCardBounds.width() - 2 * inset, -mCornerRadius + mShadowSize,
207                    mEdgeShadowPaint);
208        }
209        canvas.restoreToCount(saved);
210        // LB
211        saved = canvas.save();
212        canvas.translate(mCardBounds.left + inset, mCardBounds.bottom - inset);
213        canvas.rotate(270f);
214        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
215        if (drawVerticalEdges) {
216            canvas.drawRect(0, edgeShadowTop,
217                    mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint);
218        }
219        canvas.restoreToCount(saved);
220        // RT
221        saved = canvas.save();
222        canvas.translate(mCardBounds.right - inset, mCardBounds.top + inset);
223        canvas.rotate(90f);
224        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
225        if (drawVerticalEdges) {
226            canvas.drawRect(0, edgeShadowTop,
227                    mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint);
228        }
229        canvas.restoreToCount(saved);
230    }
231
232    private void buildShadowCorners() {
233        RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
234        RectF outerBounds = new RectF(innerBounds);
235        outerBounds.inset(-mShadowSize, -mShadowSize);
236
237        if (mCornerShadowPath == null) {
238            mCornerShadowPath = new Path();
239        } else {
240            mCornerShadowPath.reset();
241        }
242        mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
243        mCornerShadowPath.moveTo(-mCornerRadius, 0);
244        mCornerShadowPath.rLineTo(-mShadowSize, 0);
245        // outer arc
246        mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
247        // inner arc
248        mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
249        mCornerShadowPath.close();
250
251        float startRatio = mCornerRadius / (mCornerRadius + mShadowSize);
252        mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize,
253                new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
254                new float[]{0f, startRatio, 1f}
255                , Shader.TileMode.CLAMP));
256
257        // we offset the content shadowSize/2 pixels up to make it more realistic.
258        // this is why edge shadow shader has some extra space
259        // When drawing bottom edge shadow, we use that extra space.
260        mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0,
261                -mCornerRadius - mShadowSize,
262                new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
263                new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
264    }
265
266    private void buildComponents(Rect bounds) {
267        // Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift.
268        // We could have different top-bottom offsets to avoid extra gap above but in that case
269        // center aligning Views inside the CardView would be problematic.
270        final float verticalOffset = mMaxShadowSize * SHADOW_MULTIPLIER;
271        mCardBounds.set(bounds.left + mMaxShadowSize, bounds.top + verticalOffset,
272                bounds.right - mMaxShadowSize, bounds.bottom - verticalOffset);
273        buildShadowCorners();
274    }
275
276    float getMinWidth() {
277        final float content = 2 *
278                Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow + mRawMaxShadowSize / 2);
279        return content + (mRawMaxShadowSize + mInsetShadow) * 2;
280    }
281
282    float getMinHeight() {
283        final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow
284                        + mRawMaxShadowSize * SHADOW_MULTIPLIER / 2);
285        return content + (mRawMaxShadowSize * SHADOW_MULTIPLIER + mInsetShadow) * 2;
286    }
287}