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  */
16 package com.android.systemui.recents.views;
17 
18 import android.content.res.Resources;
19 import android.graphics.Canvas;
20 import android.graphics.ColorFilter;
21 import android.graphics.LinearGradient;
22 import android.graphics.Paint;
23 import android.graphics.Path;
24 import android.graphics.PixelFormat;
25 import android.graphics.RadialGradient;
26 import android.graphics.Rect;
27 import android.graphics.RectF;
28 import android.graphics.Shader;
29 import android.graphics.drawable.Drawable;
30 import android.util.Log;
31 
32 import com.android.systemui.R;
33 import com.android.systemui.recents.LegacyRecentsImpl;
34 import 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  */
42 class 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 
FakeShadowDrawable(Resources resources, RecentsConfiguration config)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 = LegacyRecentsImpl.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
setAlpha(int alpha)103     public void setAlpha(int alpha) {
104         mCornerShadowPaint.setAlpha(alpha);
105         mEdgeShadowPaint.setAlpha(alpha);
106     }
107 
108     @Override
onBoundsChange(Rect bounds)109     protected void onBoundsChange(Rect bounds) {
110         super.onBoundsChange(bounds);
111         mDirty = true;
112     }
113 
setShadowSize(float shadowSize, float maxShadowSize)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
getPadding(Rect padding)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 
calculateVerticalPadding(float maxShadowSize, float cornerRadius, boolean addPaddingForCorners)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 
calculateHorizontalPadding(float maxShadowSize, float cornerRadius, boolean addPaddingForCorners)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
setColorFilter(ColorFilter colorFilter)166     public void setColorFilter(ColorFilter colorFilter) {
167         mCornerShadowPaint.setColorFilter(colorFilter);
168         mEdgeShadowPaint.setColorFilter(colorFilter);
169     }
170 
171     @Override
getOpacity()172     public int getOpacity() {
173         return PixelFormat.OPAQUE;
174     }
175 
176     @Override
draw(Canvas canvas)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 
drawShadow(Canvas canvas)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 
buildShadowCorners()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 
buildComponents(Rect bounds)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 
getMinWidth()279     float getMinWidth() {
280         final float content = 2 *
281                 Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow + mRawMaxShadowSize / 2);
282         return content + (mRawMaxShadowSize + mInsetShadow) * 2;
283     }
284 
getMinHeight()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 }