1 /*
2  * Copyright (C) 2015, 2017 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;
18 
19 import android.annotation.NonNull;
20 import android.graphics.Canvas;
21 import android.graphics.Outline;
22 import android.graphics.Rect;
23 import com.android.layoutlib.bridge.shadowutil.SpotShadow;
24 import com.android.layoutlib.bridge.shadowutil.ShadowBuffer;
25 
26 public class RectShadowPainter {
27 
28     private static final float SHADOW_STRENGTH = 0.1f;
29     private static final int LIGHT_POINTS = 8;
30 
31     private static final int QUADRANT_DIVIDED_COUNT = 8;
32 
33     private static final int RAY_TRACING_RAYS = 180;
34     private static final int RAY_TRACING_LAYERS = 10;
35 
paintShadow(@onNull Outline viewOutline, float elevation, @NonNull Canvas canvas)36     public static void paintShadow(@NonNull Outline viewOutline, float elevation,
37             @NonNull Canvas canvas) {
38         Rect outline = new Rect();
39         if (!viewOutline.getRect(outline)) {
40             assert false : "Outline is not a rect shadow";
41             return;
42         }
43 
44         Rect originCanvasRect = canvas.getClipBounds();
45         int saved = modifyCanvas(canvas);
46         if (saved == -1) {
47             return;
48         }
49         try {
50             float radius = viewOutline.getRadius();
51             if (radius <= 0) {
52                 // We can not paint a shadow with radius 0
53                 return;
54             }
55 
56             // view's absolute position in this canvas.
57             int viewLeft = -originCanvasRect.left + outline.left;
58             int viewTop = -originCanvasRect.top + outline.top;
59             int viewRight = viewLeft + outline.width();
60             int viewBottom = viewTop + outline.height();
61 
62             float[][] rectangleCoordinators = generateRectangleCoordinates(viewLeft, viewTop,
63                     viewRight, viewBottom, radius, elevation);
64 
65             // TODO: get these values from resources.
66             float lightPosX = canvas.getWidth() / 2;
67             float lightPosY = 0;
68             float lightHeight = 1800;
69             float lightSize = 200;
70 
71             paintGeometricShadow(rectangleCoordinators, lightPosX, lightPosY, lightHeight,
72                     lightSize, canvas);
73         } finally {
74             canvas.restoreToCount(saved);
75         }
76     }
77 
modifyCanvas(@onNull Canvas canvas)78     private static int modifyCanvas(@NonNull Canvas canvas) {
79         Rect rect = canvas.getClipBounds();
80         canvas.translate(rect.left, rect.top);
81         return canvas.save();
82     }
83 
84     @NonNull
generateRectangleCoordinates(float left, float top, float right, float bottom, float radius, float elevation)85     private static float[][] generateRectangleCoordinates(float left, float top, float right,
86             float bottom, float radius, float elevation) {
87         left = left + radius;
88         top = top + radius;
89         right = right - radius;
90         bottom = bottom - radius;
91 
92         final double RADIANS_STEP = 2 * Math.PI / 4 / QUADRANT_DIVIDED_COUNT;
93 
94         float[][] ret = new float[QUADRANT_DIVIDED_COUNT * 4][3];
95 
96         int points = 0;
97         // left-bottom points
98         for (int i = 0; i < QUADRANT_DIVIDED_COUNT; i++) {
99             ret[points][0] = (float) (left - radius + radius * Math.cos(RADIANS_STEP * i));
100             ret[points][1] = (float) (bottom + radius - radius * Math.cos(RADIANS_STEP * i));
101             ret[points][2] = elevation;
102             points++;
103         }
104         // left-top points
105         for (int i = 0; i < QUADRANT_DIVIDED_COUNT; i++) {
106             ret[points][0] = (float) (left + radius - radius * Math.cos(RADIANS_STEP * i));
107             ret[points][1] = (float) (top + radius - radius * Math.cos(RADIANS_STEP * i));
108             ret[points][2] = elevation;
109             points++;
110         }
111         // right-top points
112         for (int i = 0; i < QUADRANT_DIVIDED_COUNT; i++) {
113             ret[points][0] = (float) (right + radius - radius * Math.cos(RADIANS_STEP * i));
114             ret[points][1] = (float) (top + radius + radius * Math.cos(RADIANS_STEP * i));
115             ret[points][2] = elevation;
116             points++;
117         }
118         // right-bottom point
119         for (int i = 0; i < QUADRANT_DIVIDED_COUNT; i++) {
120             ret[points][0] = (float) (right - radius + radius * Math.cos(RADIANS_STEP * i));
121             ret[points][1] = (float) (bottom - radius + radius * Math.cos(RADIANS_STEP * i));
122             ret[points][2] = elevation;
123             points++;
124         }
125 
126         return ret;
127     }
128 
paintGeometricShadow(@onNull float[][] coordinates, float lightPosX, float lightPosY, float lightHeight, float lightSize, Canvas canvas)129     private static void paintGeometricShadow(@NonNull float[][] coordinates, float lightPosX,
130             float lightPosY, float lightHeight, float lightSize, Canvas canvas) {
131         if (canvas == null || canvas.getWidth() == 0 || canvas.getHeight() == 0) {
132             return;
133         }
134 
135         // The polygon of shadow (same as the original item)
136         float[] shadowPoly = new float[coordinates.length * 3];
137         for (int i = 0; i < coordinates.length; i++) {
138             shadowPoly[i * 3 + 0] = coordinates[i][0];
139             shadowPoly[i * 3 + 1] = coordinates[i][1];
140             shadowPoly[i * 3 + 2] = coordinates[i][2];
141         }
142 
143         // TODO: calculate the ambient shadow and mix with Spot shadow.
144 
145         // Calculate the shadow of SpotLight
146         float[] light = SpotShadow.calculateLight(lightSize, LIGHT_POINTS, lightPosX,
147                 lightPosY, lightHeight);
148 
149         int stripSize = 3 * SpotShadow.getStripSize(RAY_TRACING_RAYS, RAY_TRACING_LAYERS);
150         if (stripSize < 9) {
151             return;
152         }
153         float[] strip = new float[stripSize];
154         SpotShadow.calcShadow(light, LIGHT_POINTS, shadowPoly, coordinates.length, RAY_TRACING_RAYS,
155                 RAY_TRACING_LAYERS, 1f, strip);
156 
157         ShadowBuffer buff = new ShadowBuffer(canvas.getWidth(), canvas.getHeight());
158         buff.generateTriangles(strip, SHADOW_STRENGTH);
159         buff.draw(canvas);
160     }
161 }
162