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