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