1 /*
2  * Copyright (C) 2015 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 com.android.layoutlib.bridge.impl.GcSnapshot;
20 import com.android.layoutlib.bridge.impl.ResourceHelper;
21 
22 import android.graphics.BaseCanvas_Delegate;
23 import android.graphics.Canvas;
24 import android.graphics.Canvas_Delegate;
25 import android.graphics.Color;
26 import android.graphics.LinearGradient;
27 import android.graphics.Outline;
28 import android.graphics.Paint;
29 import android.graphics.Paint.Style;
30 import android.graphics.Path;
31 import android.graphics.Path.FillType;
32 import android.graphics.RadialGradient;
33 import android.graphics.Rect;
34 import android.graphics.RectF;
35 import android.graphics.Region.Op;
36 import android.graphics.Shader.TileMode;
37 
38 import java.awt.Rectangle;
39 
40 /**
41  * Paints shadow for rounded rectangles. Inspiration from CardView. Couldn't use that directly,
42  * since it modifies the size of the content, that we can't do.
43  */
44 public class RectShadowPainter {
45 
46 
47     private static final int START_COLOR = ResourceHelper.getColor("#37000000");
48     private static final int END_COLOR = ResourceHelper.getColor("#03000000");
49     private static final float PERPENDICULAR_ANGLE = 90f;
50 
paintShadow(Outline viewOutline, float elevation, Canvas canvas, float alpha)51     public static void paintShadow(Outline viewOutline, float elevation, Canvas canvas,
52             float alpha) {
53         Rect outline = new Rect();
54         if (!viewOutline.getRect(outline)) {
55             assert false : "Outline is not a rect shadow";
56             return;
57         }
58 
59         // TODO replacing the algorithm here to create better shadow
60 
61         float shadowSize = elevationToShadow(elevation);
62         int saved = modifyCanvas(canvas, shadowSize);
63         if (saved == -1) {
64             return;
65         }
66 
67         float radius = viewOutline.getRadius();
68         if (radius <= 0) {
69             // We can not paint a shadow with radius 0
70             return;
71         }
72 
73         try {
74             Paint cornerPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
75             cornerPaint.setStyle(Style.FILL);
76             Paint edgePaint = new Paint(cornerPaint);
77             edgePaint.setAntiAlias(false);
78             float outerArcRadius = radius + shadowSize;
79             int[] colors = {START_COLOR, START_COLOR, END_COLOR};
80             if (alpha != 1f) {
81                 // Correct colors using the given component alpha
82                 for (int i = 0; i < colors.length; i++) {
83                     colors[i] = Color.argb((int) (Color.alpha(colors[i]) * alpha), Color.red(colors[i]),
84                             Color.green(colors[i]), Color.blue(colors[i]));
85                 }
86             }
87             cornerPaint.setShader(new RadialGradient(0, 0, outerArcRadius, colors,
88                     new float[]{0f, radius / outerArcRadius, 1f}, TileMode.CLAMP));
89             edgePaint.setShader(new LinearGradient(0, 0, -shadowSize, 0, colors[0], colors[2],
90                     TileMode.CLAMP));
91             Path path = new Path();
92             path.setFillType(FillType.EVEN_ODD);
93             // A rectangle bounding the complete shadow.
94             RectF shadowRect = new RectF(outline);
95             shadowRect.inset(-shadowSize, -shadowSize);
96             // A rectangle with edges corresponding to the straight edges of the outline.
97             RectF inset = new RectF(outline);
98             inset.inset(radius, radius);
99             // A rectangle used to represent the edge shadow.
100             RectF edgeShadowRect = new RectF();
101 
102 
103             // left and right sides.
104             edgeShadowRect.set(-shadowSize, 0f, 0f, inset.height());
105             // Left shadow
106             sideShadow(canvas, edgePaint, edgeShadowRect, outline.left, inset.top, 0);
107             // Right shadow
108             sideShadow(canvas, edgePaint, edgeShadowRect, outline.right, inset.bottom, 2);
109             // Top shadow
110             edgeShadowRect.set(-shadowSize, 0, 0, inset.width());
111             sideShadow(canvas, edgePaint, edgeShadowRect, inset.right, outline.top, 1);
112             // bottom shadow. This needs an inset so that blank doesn't appear when the content is
113             // moved up.
114             edgeShadowRect.set(-shadowSize, 0, shadowSize / 2f, inset.width());
115             edgePaint.setShader(new LinearGradient(edgeShadowRect.right, 0, edgeShadowRect.left, 0,
116                     colors, new float[]{0f, 1 / 3f, 1f}, TileMode.CLAMP));
117             sideShadow(canvas, edgePaint, edgeShadowRect, inset.left, outline.bottom, 3);
118 
119             // Draw corners.
120             drawCorner(canvas, cornerPaint, path, inset.right, inset.bottom, outerArcRadius, 0);
121             drawCorner(canvas, cornerPaint, path, inset.left, inset.bottom, outerArcRadius, 1);
122             drawCorner(canvas, cornerPaint, path, inset.left, inset.top, outerArcRadius, 2);
123             drawCorner(canvas, cornerPaint, path, inset.right, inset.top, outerArcRadius, 3);
124         } finally {
125             canvas.restoreToCount(saved);
126         }
127     }
128 
elevationToShadow(float elevation)129     private static float elevationToShadow(float elevation) {
130         // The factor is chosen by eyeballing the shadow size on device and preview.
131         return elevation * 0.5f;
132     }
133 
134     /**
135      * Translate canvas by half of shadow size up, so that it appears that light is coming
136      * slightly from above. Also, remove clipping, so that shadow is not clipped.
137      */
modifyCanvas(Canvas canvas, float shadowSize)138     private static int modifyCanvas(Canvas canvas, float shadowSize) {
139         Rect clipBounds = canvas.getClipBounds();
140         if (clipBounds.isEmpty()) {
141             return -1;
142         }
143         int saved = canvas.save();
144         // Usually canvas has been translated to the top left corner of the view when this is
145         // called. So, setting a clip rect at 0,0 will clip the top left part of the shadow.
146         // Thus, we just expand in each direction by width and height of the canvas, while staying
147         // inside the original drawing region.
148         GcSnapshot snapshot = Canvas_Delegate.getDelegate(canvas).getSnapshot();
149         Rectangle originalClip = snapshot.getOriginalClip();
150         if (originalClip != null) {
151             canvas.clipRect(originalClip.x, originalClip.y, originalClip.x + originalClip.width,
152               originalClip.y + originalClip.height, Op.REPLACE);
153             canvas.clipRect(-canvas.getWidth(), -canvas.getHeight(), canvas.getWidth(),
154               canvas.getHeight(), Op.INTERSECT);
155         }
156         canvas.translate(0, shadowSize / 2f);
157         return saved;
158     }
159 
sideShadow(Canvas canvas, Paint edgePaint, RectF edgeShadowRect, float dx, float dy, int rotations)160     private static void sideShadow(Canvas canvas, Paint edgePaint,
161             RectF edgeShadowRect, float dx, float dy, int rotations) {
162         if (isRectEmpty(edgeShadowRect)) {
163             return;
164         }
165         int saved = canvas.save();
166         canvas.translate(dx, dy);
167         canvas.rotate(rotations * PERPENDICULAR_ANGLE);
168         canvas.drawRect(edgeShadowRect, edgePaint);
169         canvas.restoreToCount(saved);
170     }
171 
172     /**
173      * @param canvas Canvas to draw the rectangle on.
174      * @param paint Paint to use when drawing the corner.
175      * @param path A path to reuse. Prevents allocating memory for each path.
176      * @param x Center of circle, which this corner is a part of.
177      * @param y Center of circle, which this corner is a part of.
178      * @param radius radius of the arc
179      * @param rotations number of quarter rotations before starting to paint the arc.
180      */
drawCorner(Canvas canvas, Paint paint, Path path, float x, float y, float radius, int rotations)181     private static void drawCorner(Canvas canvas, Paint paint, Path path, float x, float y,
182             float radius, int rotations) {
183         int saved = canvas.save();
184         canvas.translate(x, y);
185         path.reset();
186         path.arcTo(-radius, -radius, radius, radius, rotations * PERPENDICULAR_ANGLE,
187                 PERPENDICULAR_ANGLE, false);
188         path.lineTo(0, 0);
189         path.close();
190         canvas.drawPath(path, paint);
191         canvas.restoreToCount(saved);
192     }
193 
194     /**
195      * Differs from {@link RectF#isEmpty()} as this first converts the rect to int and then checks.
196      * <p/>
197      * This is required because {@link BaseCanvas_Delegate#native_drawRect(long, float, float,
198      * float,
199      * float, long)} casts the co-ordinates to int and we want to ensure that it doesn't end up
200      * drawing empty rectangles, which results in IllegalArgumentException.
201      */
isRectEmpty(RectF rect)202     private static boolean isRectEmpty(RectF rect) {
203         return (int) rect.left >= (int) rect.right || (int) rect.top >= (int) rect.bottom;
204     }
205 }
206