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 import android.view.math.Math3DHelper;
27 
28 import static android.view.shadow.ShadowConstants.MIN_ALPHA;
29 import static android.view.shadow.ShadowConstants.SCALE_DOWN;
30 
31 public class HighQualityShadowPainter {
32     private static final float sRoundedGap = (float) (1.0 - Math.sqrt(2.0) / 2.0);
33 
HighQualityShadowPainter()34     private HighQualityShadowPainter() { }
35 
36     /**
37      * Draws simple Rect shadow
38      */
paintRectShadow(ViewGroup parent, Outline outline, float elevation, Canvas canvas, float alpha, float densityDpi)39     public static void paintRectShadow(ViewGroup parent, Outline outline, float elevation,
40             Canvas canvas, float alpha, float densityDpi) {
41 
42         if (!validate(elevation, densityDpi)) {
43             return;
44         }
45 
46         int width = parent.getWidth() / SCALE_DOWN;
47         int height = parent.getHeight() / SCALE_DOWN;
48 
49         Rect rectOriginal = new Rect();
50         Rect rectScaled = new Rect();
51         if (!outline.getRect(rectScaled) || alpha < MIN_ALPHA) {
52             // If alpha below MIN_ALPHA it's invisible (based on manual test). Save some perf.
53             return;
54         }
55 
56         outline.getRect(rectOriginal);
57 
58         rectScaled.left /= SCALE_DOWN;
59         rectScaled.right /= SCALE_DOWN;
60         rectScaled.top /= SCALE_DOWN;
61         rectScaled.bottom /= SCALE_DOWN;
62         float radius = outline.getRadius() / SCALE_DOWN;
63 
64         if (radius > rectScaled.width() || radius > rectScaled.height()) {
65             // Rounded edge generation fails if radius is bigger than drawing box.
66             return;
67         }
68 
69         // ensure alpha doesn't go over 1
70         alpha = (alpha > 1.0f) ? 1.0f : alpha;
71         boolean isOpaque = outline.getAlpha() * alpha == 1.0f;
72         float[] poly = getPoly(rectScaled, elevation / SCALE_DOWN, radius);
73 
74         AmbientShadowConfig ambientConfig = new AmbientShadowConfig.Builder()
75                 .setPolygon(poly)
76                 .setLightSourcePosition(
77                         (rectScaled.left + rectScaled.right) / 2.0f,
78                         (rectScaled.top + rectScaled.bottom) / 2.0f)
79                 .setEdgeScale(ShadowConstants.AMBIENT_SHADOW_EDGE_SCALE)
80                 .setShadowBoundRatio(ShadowConstants.AMBIENT_SHADOW_SHADOW_BOUND)
81                 .setShadowStrength(ShadowConstants.AMBIENT_SHADOW_STRENGTH * alpha)
82                 .build();
83 
84         AmbientShadowTriangulator ambientTriangulator = new AmbientShadowTriangulator(ambientConfig);
85         ambientTriangulator.triangulate();
86 
87         SpotShadowTriangulator spotTriangulator = null;
88         float lightZHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP * (densityDpi / DisplayMetrics.DENSITY_DEFAULT);
89         if (lightZHeightPx - elevation / SCALE_DOWN >= ShadowConstants.SPOT_SHADOW_LIGHT_Z_EPSILON) {
90 
91             float lightX = (rectScaled.left + rectScaled.right) / 2;
92             float lightY = rectScaled.top;
93             // Light shouldn't be bigger than the object by too much.
94             int dynamicLightRadius = Math.min(rectScaled.width(), rectScaled.height());
95 
96             SpotShadowConfig spotConfig = new SpotShadowConfig.Builder()
97                     .setLightCoord(lightX, lightY, lightZHeightPx)
98                     .setLightRadius(dynamicLightRadius)
99                     .setShadowStrength(ShadowConstants.SPOT_SHADOW_STRENGTH * alpha)
100                     .setPolygon(poly, poly.length / ShadowConstants.COORDINATE_SIZE)
101                     .build();
102 
103             spotTriangulator = new SpotShadowTriangulator(spotConfig);
104             spotTriangulator.triangulate();
105         }
106 
107         int translateX = 0;
108         int translateY = 0;
109         int imgW = 0;
110         int imgH = 0;
111 
112         if (ambientTriangulator.isValid()) {
113             float[] shadowBounds = Math3DHelper.flatBound(ambientTriangulator.getVertices(), 2);
114             // Move the shadow to the left top corner to occupy the least possible bitmap
115 
116             translateX = -(int) Math.floor(shadowBounds[0]);
117             translateY = -(int) Math.floor(shadowBounds[1]);
118 
119             // create bitmap of the least possible size that covers the entire shadow
120             imgW = (int) Math.ceil(shadowBounds[2] + translateX);
121             imgH = (int) Math.ceil(shadowBounds[3] + translateY);
122         }
123 
124         if (spotTriangulator != null && spotTriangulator.validate()) {
125 
126             // Bit of a hack to re-adjust spot shadow to fit correctly within parent canvas.
127             // Problem is that outline passed is not a final position, which throws off our
128             // whereas our shadow rendering algorithm, which requires pre-set range for
129             // optimization purposes.
130             float[] shadowBounds = Math3DHelper.flatBound(spotTriangulator.getStrips()[0], 3);
131 
132             if ((shadowBounds[2] - shadowBounds[0]) > width ||
133                     (shadowBounds[3] - shadowBounds[1]) > height) {
134                 // Spot shadow to be casted is larger than the parent canvas,
135                 // We'll let ambient shadow do the trick and skip spot shadow here.
136                 spotTriangulator = null;
137             }
138 
139             translateX = Math.max(-(int) Math.floor(shadowBounds[0]), translateX);
140             translateY = Math.max(-(int) Math.floor(shadowBounds[1]), translateY);
141 
142             // create bitmap of the least possible size that covers the entire shadow
143             imgW = Math.max((int) Math.ceil(shadowBounds[2] + translateX), imgW);
144             imgH = Math.max((int) Math.ceil(shadowBounds[3] + translateY), imgH);
145         }
146 
147         TriangleBuffer renderer = new TriangleBuffer();
148         renderer.setSize(imgW, imgH, 0);
149 
150         if (ambientTriangulator.isValid()) {
151 
152             Math3DHelper.translate(ambientTriangulator.getVertices(), translateX, translateY, 2);
153             renderer.drawTriangles(ambientTriangulator.getIndices(), ambientTriangulator.getVertices(),
154                     ambientTriangulator.getColors(), ambientConfig.getShadowStrength());
155         }
156 
157         if (spotTriangulator != null && spotTriangulator.validate()) {
158             float[][] strips = spotTriangulator.getStrips();
159             for (int i = 0; i < strips.length; ++i) {
160                 Math3DHelper.translate(strips[i], translateX, translateY, 3);
161                 renderer.drawTriangles(strips[i], ShadowConstants.SPOT_SHADOW_STRENGTH * alpha);
162             }
163         }
164 
165         Bitmap img = renderer.createImage();
166 
167         drawScaled(canvas, img, translateX, translateY, rectOriginal, radius, isOpaque);
168     }
169 
170     /**
171      * High quality shadow does not work well with object that is too high in elevation. Check if
172      * the object elevation is reasonable and returns true if shadow will work well. False other
173      * wise.
174      */
validate(float elevation, float densityDpi)175     private static boolean validate(float elevation, float densityDpi) {
176         float scaledElevationPx = elevation / SCALE_DOWN;
177         float scaledSpotLightHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP *
178                 (densityDpi / DisplayMetrics.DENSITY_DEFAULT);
179         if (scaledElevationPx > scaledSpotLightHeightPx) {
180             return false;
181         }
182 
183         return true;
184     }
185 
186     /**
187      * Draw the bitmap scaled up.
188      * @param translateX - offset in x axis by which the bitmap is shifted.
189      * @param translateY - offset in y axis by which the bitmap is shifted.
190      * @param shadowCaster - unscaled outline of shadow caster
191      * @param radius
192      */
drawScaled(Canvas canvas, Bitmap bitmap, int translateX, int translateY, Rect shadowCaster, float radius, boolean isOpaque)193     private static void drawScaled(Canvas canvas, Bitmap bitmap, int translateX, int translateY,
194             Rect shadowCaster, float radius, boolean isOpaque) {
195         int unscaledTranslateX = translateX * SCALE_DOWN;
196         int unscaledTranslateY = translateY * SCALE_DOWN;
197 
198         // To the canvas
199         Rect dest = new Rect(
200                 -unscaledTranslateX,
201                 -unscaledTranslateY,
202                 (bitmap.getWidth() * SCALE_DOWN) - unscaledTranslateX,
203                 (bitmap.getHeight() * SCALE_DOWN) - unscaledTranslateY);
204         Rect destSrc = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
205         // We can skip drawing the shadows behind the caster if either
206         // 1) radius is 0, the shadow caster is rectangle and we can have a perfect cut
207         // 2) shadow caster is opaque and even if remove shadow only partially it won't affect
208         // the visual quality, otherwise we will observe shadow part through the translucent caster
209         // This can be improved by:
210         // TODO: do not draw the shadow behind the caster at all during the tesselation phase
211         if (radius > 0 && !isOpaque) {
212             // Rounded edge.
213             int save = canvas.save();
214             canvas.drawBitmap(bitmap, destSrc, dest, null);
215             canvas.restoreToCount(save);
216             return;
217         }
218 
219         /**
220          * ----------------------------------
221          * |                                |
222          * |              top               |
223          * |                                |
224          * ----------------------------------
225          * |      |                 |       |
226          * | left |  shadow caster  | right |
227          * |      |                 |       |
228          * ----------------------------------
229          * |                                |
230          * |            bottom              |
231          * |                                |
232          * ----------------------------------
233          *
234          * dest == top + left + shadow caster + right + bottom
235          * Visually, canvas.drawBitmap(bitmap, destSrc, dest, paint) would achieve the same result.
236          */
237         int gap = (int) Math.ceil(radius * SCALE_DOWN * sRoundedGap);
238         shadowCaster.bottom -= gap;
239         shadowCaster.top += gap;
240         shadowCaster.left += gap;
241         shadowCaster.right -= gap;
242         Rect left = new Rect(dest.left, shadowCaster.top, shadowCaster.left,
243                 shadowCaster.bottom);
244         int leftScaled = left.width() / SCALE_DOWN + destSrc.left;
245 
246         Rect top = new Rect(dest.left, dest.top, dest.right, shadowCaster.top);
247         int topScaled = top.height() / SCALE_DOWN + destSrc.top;
248 
249         Rect right = new Rect(shadowCaster.right, shadowCaster.top, dest.right,
250                 shadowCaster.bottom);
251         int rightScaled = (shadowCaster.right - dest.left) / SCALE_DOWN + destSrc.left;
252 
253         Rect bottom = new Rect(dest.left, shadowCaster.bottom, dest.right, dest.bottom);
254         int bottomScaled = (shadowCaster.bottom - dest.top) / SCALE_DOWN + destSrc.top;
255 
256         // calculate parts of the middle ground that can be ignored.
257         Rect leftSrc = new Rect(destSrc.left, topScaled, leftScaled, bottomScaled);
258         Rect topSrc = new Rect(destSrc.left, destSrc.top, destSrc.right, topScaled);
259         Rect rightSrc = new Rect(rightScaled, topScaled, destSrc.right, bottomScaled);
260         Rect bottomSrc = new Rect(destSrc.left, bottomScaled, destSrc.right, destSrc.bottom);
261 
262         int save = canvas.save();
263         Paint paint = new Paint();
264         canvas.drawBitmap(bitmap, leftSrc, left, paint);
265         canvas.drawBitmap(bitmap, topSrc, top, paint);
266         canvas.drawBitmap(bitmap, rightSrc, right, paint);
267         canvas.drawBitmap(bitmap, bottomSrc, bottom, paint);
268         canvas.restoreToCount(save);
269     }
270 
getPoly(Rect rect, float elevation, float radius)271     private static float[] getPoly(Rect rect, float elevation, float radius) {
272         if (radius <= 0) {
273             float[] poly = new float[ShadowConstants.RECT_VERTICES_SIZE * ShadowConstants.COORDINATE_SIZE];
274 
275             poly[0] = poly[9] = rect.left;
276             poly[1] = poly[4] = rect.top;
277             poly[3] = poly[6] = rect.right;
278             poly[7] = poly[10] = rect.bottom;
279             poly[2] = poly[5] = poly[8] = poly[11] = elevation;
280 
281             return poly;
282         }
283 
284         return buildRoundedEdges(rect, elevation, radius);
285     }
286 
buildRoundedEdges( Rect rect, float elevation, float radius)287     private static float[] buildRoundedEdges(
288             Rect rect, float elevation, float radius) {
289 
290         float[] roundedEdgeVertices = new float[(ShadowConstants.SPLICE_ROUNDED_EDGE + 1) * 4 * 3];
291         int index = 0;
292         // 1.0 LT. From theta 0 to pi/2 in K division.
293         for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) {
294             double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
295             float x = (float) (rect.left + (radius - radius * Math.cos(theta)));
296             float y = (float) (rect.top + (radius - radius * Math.sin(theta)));
297             roundedEdgeVertices[index++] = x;
298             roundedEdgeVertices[index++] = y;
299             roundedEdgeVertices[index++] = elevation;
300         }
301 
302         // 2.0 RT
303         for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) {
304             double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
305             float x = (float) (rect.right - (radius - radius * Math.cos(theta)));
306             float y = (float) (rect.top + (radius - radius * Math.sin(theta)));
307             roundedEdgeVertices[index++] = x;
308             roundedEdgeVertices[index++] = y;
309             roundedEdgeVertices[index++] = elevation;
310         }
311 
312         // 3.0 RB
313         for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) {
314             double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
315             float x = (float) (rect.right - (radius - radius * Math.cos(theta)));
316             float y = (float) (rect.bottom - (radius - radius * Math.sin(theta)));
317             roundedEdgeVertices[index++] = x;
318             roundedEdgeVertices[index++] = y;
319             roundedEdgeVertices[index++] = elevation;
320         }
321 
322         // 4.0 LB
323         for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) {
324             double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
325             float x = (float) (rect.left + (radius - radius * Math.cos(theta)));
326             float y = (float) (rect.bottom - (radius - radius * Math.sin(theta)));
327             roundedEdgeVertices[index++] = x;
328             roundedEdgeVertices[index++] = y;
329             roundedEdgeVertices[index++] = elevation;
330         }
331 
332         return roundedEdgeVertices;
333     }
334 }
335