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