1 /*
2  * Copyright (C) 2018 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 
17 package android.view.shadow;
18 
19 import android.graphics.Bitmap;
20 import android.graphics.Canvas;
21 import android.graphics.Outline;
22 import android.graphics.Paint;
23 import android.graphics.Rect;
24 import android.util.DisplayMetrics;
25 import android.view.ViewGroup;
26 
27 import static android.view.shadow.ShadowConstants.MIN_ALPHA;
28 import static android.view.shadow.ShadowConstants.SCALE_DOWN;
29 
30 public class HighQualityShadowPainter {
31 
HighQualityShadowPainter()32     private HighQualityShadowPainter() { }
33 
34     /**
35      * Draws simple Rect shadow
36      */
paintRectShadow(ViewGroup parent, Outline outline, float elevation, Canvas canvas, float alpha, float densityDpi)37     public static void paintRectShadow(ViewGroup parent, Outline outline, float elevation,
38             Canvas canvas, float alpha, float densityDpi) {
39 
40         if (!validate(elevation, densityDpi)) {
41             return;
42         }
43 
44         int width = parent.getWidth() / SCALE_DOWN;
45         int height = parent.getHeight() / SCALE_DOWN;
46 
47         Rect rectOriginal = new Rect();
48         Rect rectScaled = new Rect();
49         if (!outline.getRect(rectScaled) || alpha < MIN_ALPHA) {
50             // If alpha below MIN_ALPHA it's invisible (based on manual test). Save some perf.
51             return;
52         }
53 
54         outline.getRect(rectOriginal);
55 
56         rectScaled.left /= SCALE_DOWN;
57         rectScaled.right /= SCALE_DOWN;
58         rectScaled.top /= SCALE_DOWN;
59         rectScaled.bottom /= SCALE_DOWN;
60         float radius = outline.getRadius() / SCALE_DOWN;
61 
62         if (radius > rectScaled.width() || radius > rectScaled.height()) {
63             // Rounded edge generation fails if radius is bigger than drawing box.
64             return;
65         }
66 
67         // ensure alpha doesn't go over 1
68         alpha = (alpha > 1.0f) ? 1.0f : alpha;
69         float[] poly = getPoly(rectScaled, elevation / SCALE_DOWN, radius);
70 
71         paintAmbientShadow(poly, canvas, width, height, alpha, rectOriginal, radius);
72         paintSpotShadow(poly, rectScaled, elevation / SCALE_DOWN,
73                 canvas, densityDpi, width, height, alpha, rectOriginal, radius);
74     }
75 
76     /**
77      * High quality shadow does not work well with object that is too high in elevation. Check if
78      * the object elevation is reasonable and returns true if shadow will work well. False other
79      * wise.
80      */
validate(float elevation, float densityDpi)81     private static boolean validate(float elevation, float densityDpi) {
82         float scaledElevationPx = elevation / SCALE_DOWN;
83         float scaledSpotLightHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP *
84                 (densityDpi / DisplayMetrics.DENSITY_DEFAULT);
85         if (scaledElevationPx > scaledSpotLightHeightPx) {
86             return false;
87         }
88 
89         return true;
90     }
91 
92     /**
93      * @param polygon - polygon of the shadow caster
94      * @param canvas - canvas to draw
95      * @param width - scaled canvas (parent) width
96      * @param height - scaled canvas (parent) height
97      * @param alpha - 0-1 scale
98      * @param shadowCasterOutline - unscaled original shadow caster outline.
99      * @param radius
100      */
paintAmbientShadow(float[] polygon, Canvas canvas, int width, int height, float alpha, Rect shadowCasterOutline, float radius)101     private static void paintAmbientShadow(float[] polygon, Canvas canvas, int width, int height,
102             float alpha, Rect shadowCasterOutline, float radius) {
103         // TODO: Consider re-using the triangle buffer here since the world stays consistent.
104         // TODO: Reduce the buffer size based on shadow bounds.
105 
106         AmbientShadowConfig config = new AmbientShadowConfig.Builder()
107                 .setSize(width, height)
108                 .setPolygon(polygon)
109                 .setEdgeScale(ShadowConstants.AMBIENT_SHADOW_EDGE_SCALE)
110                 .setShadowBoundRatio(ShadowConstants.AMBIENT_SHADOW_SHADOW_BOUND)
111                 .setShadowStrength(ShadowConstants.AMBIENT_SHADOW_STRENGTH * alpha)
112                 .setRays(ShadowConstants.AMBIENT_SHADOW_RAYS)
113                 .setLayers(ShadowConstants.AMBIENT_SHADOW_LAYERS)
114                 .build();
115 
116         AmbientShadowBitmapGenerator generator = new AmbientShadowBitmapGenerator(config);
117         generator.populateShadow();
118 
119         if (!generator.isValid()) {
120             return;
121         }
122 
123         drawScaled(
124                 canvas, generator.getBitmap(), (int) generator.getTranslateX(),
125                 (int) generator.getTranslateY(), width, height,
126                 shadowCasterOutline, radius);
127     }
128 
129     /**
130      * @param poly - polygon of the shadow caster
131      * @param rectBound - scaled bounds of shadow caster.
132      * @param canvas - canvas to draw
133      * @param width - scaled canvas (parent) width
134      * @param height - scaled canvas (parent) height
135      * @param alpha - 0-1 scale
136      * @param shadowCasterOutline - unscaled original shadow caster outline.
137      * @param radius
138      */
paintSpotShadow(float[] poly, Rect rectBound, float elevation, Canvas canvas, float densityDpi, int width, int height, float alpha, Rect shadowCasterOutline, float radius)139     private static void paintSpotShadow(float[] poly, Rect rectBound, float elevation, Canvas canvas,
140             float densityDpi, int width, int height, float alpha, Rect shadowCasterOutline,
141             float radius) {
142 
143         // TODO: Use alpha later
144         float lightZHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP * (densityDpi / DisplayMetrics.DENSITY_DEFAULT);
145         if (lightZHeightPx - elevation < ShadowConstants.SPOT_SHADOW_LIGHT_Z_EPSILON) {
146             // If the view is above or too close to the light source then return.
147             // This is done to somewhat simulate android behaviour.
148             return;
149         }
150 
151         float lightX = (rectBound.left + rectBound.right) / 2;
152         float lightY = rectBound.top;
153         // Light shouldn't be bigger than the object by too much.
154         int dynamicLightRadius = Math.min(rectBound.width(), rectBound.height());
155 
156         SpotShadowConfig config = new SpotShadowConfig.Builder()
157                 .setSize(width, height)
158                 .setLayers(ShadowConstants.SPOT_SHADOW_LAYERS)
159                 .setRays(ShadowConstants.SPOT_SHADOW_RAYS)
160                 .setLightCoord(lightX, lightY, lightZHeightPx)
161                 .setLightRadius(dynamicLightRadius)
162                 .setLightSourcePoints(ShadowConstants.SPOT_SHADOW_LIGHT_SOURCE_POINTS)
163                 .setShadowStrength(ShadowConstants.SPOT_SHADOW_STRENGTH * alpha)
164                 .setPolygon(poly, poly.length / ShadowConstants.COORDINATE_SIZE)
165                 .build();
166 
167         SpotShadowBitmapGenerator generator = new SpotShadowBitmapGenerator(config);
168         generator.populateShadow();
169 
170         if (!generator.validate()) {
171             return;
172         }
173 
174         drawScaled(canvas, generator.getBitmap(), (int) generator.getTranslateX(),
175                 (int) generator.getTranslateY(), width, height, shadowCasterOutline, radius);
176     }
177 
178     /**
179      * Draw the bitmap scaled up.
180      * @param translateX - offset in x axis by which the bitmap is shifted.
181      * @param translateY - offset in y axis by which the bitmap is shifted.
182      * @param width  - scaled width of canvas (parent)
183      * @param height - scaled height of canvas (parent)
184      * @param shadowCaster - unscaled outline of shadow caster
185      * @param radius
186      */
drawScaled(Canvas canvas, Bitmap bitmap, int translateX, int translateY, int width, int height, Rect shadowCaster, float radius)187     private static void drawScaled(Canvas canvas, Bitmap bitmap, int translateX, int translateY,
188             int width, int height, Rect shadowCaster, float radius) {
189         int unscaledTranslateX = translateX * SCALE_DOWN;
190         int unscaledTranslateY = translateY * SCALE_DOWN;
191 
192         // To the canvas
193         Rect dest = new Rect(
194                 -unscaledTranslateX,
195                 -unscaledTranslateY,
196                 (width * SCALE_DOWN) - unscaledTranslateX,
197                 (height * SCALE_DOWN) - unscaledTranslateY);
198         Rect destSrc = new Rect(0, 0, width, height);
199 
200         if (radius > 0) {
201             // Rounded edge.
202             int save = canvas.save();
203             canvas.drawBitmap(bitmap, destSrc, dest, null);
204             canvas.restoreToCount(save);
205             return;
206         }
207 
208         /**
209          * ----------------------------------
210          * |                                |
211          * |              top               |
212          * |                                |
213          * ----------------------------------
214          * |      |                 |       |
215          * | left |  shadow caster  | right |
216          * |      |                 |       |
217          * ----------------------------------
218          * |                                |
219          * |            bottom              |
220          * |                                |
221          * ----------------------------------
222          *
223          * dest == top + left + shadow caster + right + bottom
224          * Visually, canvas.drawBitmap(bitmap, destSrc, dest, paint) would achieve the same result.
225          */
226         Rect left = new Rect(dest.left, shadowCaster.top, shadowCaster.left, shadowCaster.bottom);
227         int leftScaled = left.width() / SCALE_DOWN + destSrc.left;
228 
229         Rect top = new Rect(dest.left, dest.top, dest.right, shadowCaster.top);
230         int topScaled = top.height() / SCALE_DOWN + destSrc.top;
231 
232         Rect right = new Rect(shadowCaster.right, shadowCaster.top, dest.right,
233                 shadowCaster.bottom);
234         int rightScaled = (shadowCaster.right + unscaledTranslateX) / SCALE_DOWN + destSrc.left;
235 
236         Rect bottom = new Rect(dest.left, shadowCaster.bottom, dest.right, dest.bottom);
237         int bottomScaled = (bottom.bottom - bottom.height()) / SCALE_DOWN + destSrc.top;
238 
239         // calculate parts of the middle ground that can be ignored.
240         Rect leftSrc = new Rect(destSrc.left, topScaled, leftScaled, bottomScaled);
241         Rect topSrc = new Rect(destSrc.left, destSrc.top, destSrc.right, topScaled);
242         Rect rightSrc = new Rect(rightScaled, topScaled, destSrc.right, bottomScaled);
243         Rect bottomSrc = new Rect(destSrc.left, bottomScaled, destSrc.right, destSrc.bottom);
244 
245         int save = canvas.save();
246         Paint paint = new Paint();
247         canvas.drawBitmap(bitmap, leftSrc, left, paint);
248         canvas.drawBitmap(bitmap, topSrc, top, paint);
249         canvas.drawBitmap(bitmap, rightSrc, right, paint);
250         canvas.drawBitmap(bitmap, bottomSrc, bottom, paint);
251         canvas.restoreToCount(save);
252     }
253 
getPoly(Rect rect, float elevation, float radius)254     private static float[] getPoly(Rect rect, float elevation, float radius) {
255         if (radius <= 0) {
256             float[] poly = new float[ShadowConstants.RECT_VERTICES_SIZE * ShadowConstants.COORDINATE_SIZE];
257 
258             poly[0] = poly[9] = rect.left;
259             poly[1] = poly[4] = rect.top;
260             poly[3] = poly[6] = rect.right;
261             poly[7] = poly[10] = rect.bottom;
262             poly[2] = poly[5] = poly[8] = poly[11] = elevation;
263 
264             return poly;
265         }
266 
267         return buildRoundedEdges(rect, elevation, radius);
268     }
269 
buildRoundedEdges( Rect rect, float elevation, float radius)270     private static float[] buildRoundedEdges(
271             Rect rect, float elevation, float radius) {
272 
273         float[] roundedEdgeVertices = new float[(ShadowConstants.SPLICE_ROUNDED_EDGE + 1) * 4 * 3];
274         int index = 0;
275         // 1.0 LT. From theta 0 to pi/2 in K division.
276         for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) {
277             double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
278             float x = (float) (rect.left + (radius - radius * Math.cos(theta)));
279             float y = (float) (rect.top + (radius - radius * Math.sin(theta)));
280             roundedEdgeVertices[index++] = x;
281             roundedEdgeVertices[index++] = y;
282             roundedEdgeVertices[index++] = elevation;
283         }
284 
285         // 2.0 RT
286         for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) {
287             double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
288             float x = (float) (rect.right - (radius - radius * Math.cos(theta)));
289             float y = (float) (rect.top + (radius - radius * Math.sin(theta)));
290             roundedEdgeVertices[index++] = x;
291             roundedEdgeVertices[index++] = y;
292             roundedEdgeVertices[index++] = elevation;
293         }
294 
295         // 3.0 RB
296         for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) {
297             double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
298             float x = (float) (rect.right - (radius - radius * Math.cos(theta)));
299             float y = (float) (rect.bottom - (radius - radius * Math.sin(theta)));
300             roundedEdgeVertices[index++] = x;
301             roundedEdgeVertices[index++] = y;
302             roundedEdgeVertices[index++] = elevation;
303         }
304 
305         // 4.0 LB
306         for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) {
307             double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
308             float x = (float) (rect.left + (radius - radius * Math.cos(theta)));
309             float y = (float) (rect.bottom - (radius - radius * Math.sin(theta)));
310             roundedEdgeVertices[index++] = x;
311             roundedEdgeVertices[index++] = y;
312             roundedEdgeVertices[index++] = elevation;
313         }
314 
315         return roundedEdgeVertices;
316     }
317 }
318