1 /*
2  * Copyright (C) 2014 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 
21 import java.awt.Graphics2D;
22 import java.awt.Image;
23 import java.awt.image.BufferedImage;
24 import java.awt.image.DataBufferInt;
25 import java.io.IOException;
26 import java.io.InputStream;
27 
28 import javax.imageio.ImageIO;
29 
30 public class ShadowPainter {
31 
32     /**
33      * Adds a drop shadow to a semi-transparent image (of an arbitrary shape) and returns it as a
34      * new image. This method attempts to mimic the same visual characteristics as the rectangular
35      * shadow painting methods in this class, {@link #createRectangularDropShadow(java.awt.image.BufferedImage)}
36      * and {@link #createSmallRectangularDropShadow(java.awt.image.BufferedImage)}.
37      * <p/>
38      * If shadowSize is less or equals to 1, no shadow will be painted and the source image will be
39      * returned instead.
40      *
41      * @param source the source image
42      * @param shadowSize the size of the shadow, normally {@link #SHADOW_SIZE or {@link
43      * #SMALL_SHADOW_SIZE}}
44      * @param alpha alpha value to apply to the shadow
45      *
46      * @return an image with the shadow painted in or the source image if shadowSize <= 1
47      */
48     @NonNull
createDropShadow(BufferedImage source, int shadowSize, float alpha)49     public static BufferedImage createDropShadow(BufferedImage source, int shadowSize, float
50             alpha) {
51         shadowSize /= 2; // make shadow size have the same meaning as in the other shadow paint methods in this class
52 
53         return createDropShadow(source, shadowSize, 0.7f * alpha, 0);
54     }
55 
56     /**
57      * Creates a drop shadow of a given image and returns a new image which shows the input image on
58      * top of its drop shadow.
59      * <p/>
60      * <b>NOTE: If the shape is rectangular and opaque, consider using {@link
61      * #drawRectangleShadow(Graphics2D, int, int, int, int)} instead.</b>
62      *
63      * @param source the source image to be shadowed
64      * @param shadowSize the size of the shadow in pixels
65      * @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque
66      * @param shadowRgb the RGB int to use for the shadow color
67      *
68      * @return a new image with the source image on top of its shadow when shadowSize > 0 or the
69      * source image otherwise
70      */
71     @SuppressWarnings({"SuspiciousNameCombination", "UnnecessaryLocalVariable"})  // Imported code
createDropShadow(BufferedImage source, int shadowSize, float shadowOpacity, int shadowRgb)72     public static BufferedImage createDropShadow(BufferedImage source, int shadowSize,
73             float shadowOpacity, int shadowRgb) {
74         if (shadowSize <= 0) {
75             return source;
76         }
77 
78         // This code is based on
79         //      http://www.jroller.com/gfx/entry/non_rectangular_shadow
80 
81         BufferedImage image;
82         int width = source.getWidth();
83         int height = source.getHeight();
84         image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE,
85                 BufferedImage.TYPE_INT_ARGB);
86 
87         Graphics2D g2 = image.createGraphics();
88         g2.drawImage(image, shadowSize, shadowSize, null);
89 
90         int dstWidth = image.getWidth();
91         int dstHeight = image.getHeight();
92 
93         int left = (shadowSize - 1) >> 1;
94         int right = shadowSize - left;
95         int xStart = left;
96         int xStop = dstWidth - right;
97         int yStart = left;
98         int yStop = dstHeight - right;
99 
100         shadowRgb &= 0x00FFFFFF;
101 
102         int[] aHistory = new int[shadowSize];
103         int historyIdx;
104 
105         int aSum;
106 
107         int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
108         int lastPixelOffset = right * dstWidth;
109         float sumDivider = shadowOpacity / shadowSize;
110 
111         // horizontal pass
112         for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) {
113             aSum = 0;
114             historyIdx = 0;
115             for (int x = 0; x < shadowSize; x++, bufferOffset++) {
116                 int a = dataBuffer[bufferOffset] >>> 24;
117                 aHistory[x] = a;
118                 aSum += a;
119             }
120 
121             bufferOffset -= right;
122 
123             for (int x = xStart; x < xStop; x++, bufferOffset++) {
124                 int a = (int) (aSum * sumDivider);
125                 dataBuffer[bufferOffset] = a << 24 | shadowRgb;
126 
127                 // subtract the oldest pixel from the sum
128                 aSum -= aHistory[historyIdx];
129 
130                 // get the latest pixel
131                 a = dataBuffer[bufferOffset + right] >>> 24;
132                 aHistory[historyIdx] = a;
133                 aSum += a;
134 
135                 if (++historyIdx >= shadowSize) {
136                     historyIdx -= shadowSize;
137                 }
138             }
139         }
140         // vertical pass
141         for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
142             aSum = 0;
143             historyIdx = 0;
144             for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) {
145                 int a = dataBuffer[bufferOffset] >>> 24;
146                 aHistory[y] = a;
147                 aSum += a;
148             }
149 
150             bufferOffset -= lastPixelOffset;
151 
152             for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) {
153                 int a = (int) (aSum * sumDivider);
154                 dataBuffer[bufferOffset] = a << 24 | shadowRgb;
155 
156                 // subtract the oldest pixel from the sum
157                 aSum -= aHistory[historyIdx];
158 
159                 // get the latest pixel
160                 a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24;
161                 aHistory[historyIdx] = a;
162                 aSum += a;
163 
164                 if (++historyIdx >= shadowSize) {
165                     historyIdx -= shadowSize;
166                 }
167             }
168         }
169 
170         g2.drawImage(source, null, 0, 0);
171         g2.dispose();
172 
173         return image;
174     }
175 
176     /**
177      * Draws a rectangular drop shadow (of size {@link #SHADOW_SIZE} by {@link #SHADOW_SIZE} around
178      * the given source and returns a new image with both combined
179      *
180      * @param source the source image
181      *
182      * @return the source image with a drop shadow on the bottom and right
183      */
184     @SuppressWarnings("UnusedDeclaration")
createRectangularDropShadow(BufferedImage source)185     public static BufferedImage createRectangularDropShadow(BufferedImage source) {
186         int type = source.getType();
187         if (type == BufferedImage.TYPE_CUSTOM) {
188             type = BufferedImage.TYPE_INT_ARGB;
189         }
190 
191         int width = source.getWidth();
192         int height = source.getHeight();
193         BufferedImage image;
194         image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE, type);
195         Graphics2D g = image.createGraphics();
196         g.drawImage(source, 0, 0, null);
197         drawRectangleShadow(image, 0, 0, width, height);
198         g.dispose();
199 
200         return image;
201     }
202 
203     /**
204      * Draws a small rectangular drop shadow (of size {@link #SMALL_SHADOW_SIZE} by {@link
205      * #SMALL_SHADOW_SIZE} around the given source and returns a new image with both combined
206      *
207      * @param source the source image
208      *
209      * @return the source image with a drop shadow on the bottom and right
210      */
211     @SuppressWarnings("UnusedDeclaration")
createSmallRectangularDropShadow(BufferedImage source)212     public static BufferedImage createSmallRectangularDropShadow(BufferedImage source) {
213         int type = source.getType();
214         if (type == BufferedImage.TYPE_CUSTOM) {
215             type = BufferedImage.TYPE_INT_ARGB;
216         }
217 
218         int width = source.getWidth();
219         int height = source.getHeight();
220 
221         BufferedImage image;
222         image = new BufferedImage(width + SMALL_SHADOW_SIZE, height + SMALL_SHADOW_SIZE, type);
223 
224         Graphics2D g = image.createGraphics();
225         g.drawImage(source, 0, 0, null);
226         drawSmallRectangleShadow(image, 0, 0, width, height);
227         g.dispose();
228 
229         return image;
230     }
231 
232     /**
233      * Draws a drop shadow for the given rectangle into the given context. It will not draw anything
234      * if the rectangle is smaller than a minimum determined by the assets used to draw the shadow
235      * graphics. The size of the shadow is {@link #SHADOW_SIZE}.
236      *
237      * @param image the image to draw the shadow into
238      * @param x the left coordinate of the left hand side of the rectangle
239      * @param y the top coordinate of the top of the rectangle
240      * @param width the width of the rectangle
241      * @param height the height of the rectangle
242      */
drawRectangleShadow(BufferedImage image, int x, int y, int width, int height)243     public static void drawRectangleShadow(BufferedImage image,
244             int x, int y, int width, int height) {
245         Graphics2D gc = image.createGraphics();
246         try {
247             drawRectangleShadow(gc, x, y, width, height);
248         } finally {
249             gc.dispose();
250         }
251     }
252 
253     /**
254      * Draws a small drop shadow for the given rectangle into the given context. It will not draw
255      * anything if the rectangle is smaller than a minimum determined by the assets used to draw the
256      * shadow graphics. The size of the shadow is {@link #SMALL_SHADOW_SIZE}.
257      *
258      * @param image the image to draw the shadow into
259      * @param x the left coordinate of the left hand side of the rectangle
260      * @param y the top coordinate of the top of the rectangle
261      * @param width the width of the rectangle
262      * @param height the height of the rectangle
263      */
drawSmallRectangleShadow(BufferedImage image, int x, int y, int width, int height)264     public static void drawSmallRectangleShadow(BufferedImage image,
265             int x, int y, int width, int height) {
266         Graphics2D gc = image.createGraphics();
267         try {
268             drawSmallRectangleShadow(gc, x, y, width, height);
269         } finally {
270             gc.dispose();
271         }
272     }
273 
274     /**
275      * The width and height of the drop shadow painted by
276      * {@link #drawRectangleShadow(Graphics2D, int, int, int, int)}
277      */
278     public static final int SHADOW_SIZE = 20; // DO NOT EDIT. This corresponds to bitmap graphics
279 
280     /**
281      * The width and height of the drop shadow painted by
282      * {@link #drawSmallRectangleShadow(Graphics2D, int, int, int, int)}
283      */
284     public static final int SMALL_SHADOW_SIZE = 10; // DO NOT EDIT. Corresponds to bitmap graphics
285 
286     /**
287      * Draws a drop shadow for the given rectangle into the given context. It will not draw anything
288      * if the rectangle is smaller than a minimum determined by the assets used to draw the shadow
289      * graphics.
290      *
291      * @param gc the graphics context to draw into
292      * @param x the left coordinate of the left hand side of the rectangle
293      * @param y the top coordinate of the top of the rectangle
294      * @param width the width of the rectangle
295      * @param height the height of the rectangle
296      */
drawRectangleShadow(Graphics2D gc, int x, int y, int width, int height)297     public static void drawRectangleShadow(Graphics2D gc, int x, int y, int width, int height) {
298         assert ShadowBottomLeft != null;
299         assert ShadowBottomRight.getWidth(null) == SHADOW_SIZE;
300         assert ShadowBottomRight.getHeight(null) == SHADOW_SIZE;
301 
302         int blWidth = ShadowBottomLeft.getWidth(null);
303         int trHeight = ShadowTopRight.getHeight(null);
304         if (width < blWidth) {
305             return;
306         }
307         if (height < trHeight) {
308             return;
309         }
310 
311         gc.drawImage(ShadowBottomLeft, x - ShadowBottomLeft.getWidth(null), y + height, null);
312         gc.drawImage(ShadowBottomRight, x + width, y + height, null);
313         gc.drawImage(ShadowTopRight, x + width, y, null);
314         gc.drawImage(ShadowTopLeft, x - ShadowTopLeft.getWidth(null), y, null);
315         gc.drawImage(ShadowBottom,
316                 x, y + height, x + width, y + height + ShadowBottom.getHeight(null),
317                 0, 0, ShadowBottom.getWidth(null), ShadowBottom.getHeight(null), null);
318         gc.drawImage(ShadowRight,
319                 x + width, y + ShadowTopRight.getHeight(null), x + width + ShadowRight.getWidth(null), y + height,
320                 0, 0, ShadowRight.getWidth(null), ShadowRight.getHeight(null), null);
321         gc.drawImage(ShadowLeft,
322                 x - ShadowLeft.getWidth(null), y + ShadowTopLeft.getHeight(null), x, y + height,
323                 0, 0, ShadowLeft.getWidth(null), ShadowLeft.getHeight(null), null);
324     }
325 
326     /**
327      * Draws a small drop shadow for the given rectangle into the given context. It will not draw
328      * anything if the rectangle is smaller than a minimum determined by the assets used to draw the
329      * shadow graphics.
330      * <p/>
331      *
332      * @param gc the graphics context to draw into
333      * @param x the left coordinate of the left hand side of the rectangle
334      * @param y the top coordinate of the top of the rectangle
335      * @param width the width of the rectangle
336      * @param height the height of the rectangle
337      */
drawSmallRectangleShadow(Graphics2D gc, int x, int y, int width, int height)338     public static void drawSmallRectangleShadow(Graphics2D gc, int x, int y, int width,
339             int height) {
340         assert Shadow2BottomLeft != null;
341         assert Shadow2TopRight != null;
342         assert Shadow2BottomRight.getWidth(null) == SMALL_SHADOW_SIZE;
343         assert Shadow2BottomRight.getHeight(null) == SMALL_SHADOW_SIZE;
344 
345         int blWidth = Shadow2BottomLeft.getWidth(null);
346         int trHeight = Shadow2TopRight.getHeight(null);
347         if (width < blWidth) {
348             return;
349         }
350         if (height < trHeight) {
351             return;
352         }
353 
354         gc.drawImage(Shadow2BottomLeft, x - Shadow2BottomLeft.getWidth(null), y + height, null);
355         gc.drawImage(Shadow2BottomRight, x + width, y + height, null);
356         gc.drawImage(Shadow2TopRight, x + width, y, null);
357         gc.drawImage(Shadow2TopLeft, x - Shadow2TopLeft.getWidth(null), y, null);
358         gc.drawImage(Shadow2Bottom,
359                 x, y + height, x + width, y + height + Shadow2Bottom.getHeight(null),
360                 0, 0, Shadow2Bottom.getWidth(null), Shadow2Bottom.getHeight(null), null);
361         gc.drawImage(Shadow2Right,
362                 x + width, y + Shadow2TopRight.getHeight(null), x + width + Shadow2Right.getWidth(null), y + height,
363                 0, 0, Shadow2Right.getWidth(null), Shadow2Right.getHeight(null), null);
364         gc.drawImage(Shadow2Left,
365                 x - Shadow2Left.getWidth(null), y + Shadow2TopLeft.getHeight(null), x, y + height,
366                 0, 0, Shadow2Left.getWidth(null), Shadow2Left.getHeight(null), null);
367     }
368 
loadIcon(String name)369     private static Image loadIcon(String name) {
370         InputStream inputStream = ShadowPainter.class.getResourceAsStream(name);
371         if (inputStream == null) {
372             throw new RuntimeException("Unable to load image for shadow: " + name);
373         }
374         try {
375             return ImageIO.read(inputStream);
376         } catch (IOException e) {
377             throw new RuntimeException("Unable to load image for shadow:" + name, e);
378         } finally {
379             try {
380                 inputStream.close();
381             } catch (IOException e) {
382                 // ignore.
383             }
384         }
385     }
386 
387     // Shadow graphics. This was generated by creating a drop shadow in
388     // Gimp, using the parameters x offset=10, y offset=10, blur radius=10,
389     // (for the small drop shadows x offset=10, y offset=10, blur radius=10)
390     // color=black, and opacity=51. These values attempt to make a shadow
391     // that is legible both for dark and light themes, on top of the
392     // canvas background (rgb(150,150,150). Darker shadows would tend to
393     // blend into the foreground for a dark holo screen, and lighter shadows
394     // would be hard to spot on the canvas background. If you make adjustments,
395     // make sure to check the shadow with both dark and light themes.
396     //
397     // After making the graphics, I cut out the top right, bottom left
398     // and bottom right corners as 20x20 images, and these are reproduced by
399     // painting them in the corresponding places in the target graphics context.
400     // I then grabbed a single horizontal gradient line from the middle of the
401     // right edge,and a single vertical gradient line from the bottom. These
402     // are then painted scaled/stretched in the target to fill the gaps between
403     // the three corner images.
404     //
405     // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right
406 
407     // Normal Drop Shadow
408     private static final Image ShadowBottom = loadIcon("/icons/shadow-b.png");
409     private static final Image ShadowBottomLeft = loadIcon("/icons/shadow-bl.png");
410     private static final Image ShadowBottomRight = loadIcon("/icons/shadow-br.png");
411     private static final Image ShadowRight = loadIcon("/icons/shadow-r.png");
412     private static final Image ShadowTopRight = loadIcon("/icons/shadow-tr.png");
413     private static final Image ShadowTopLeft = loadIcon("/icons/shadow-tl.png");
414     private static final Image ShadowLeft = loadIcon("/icons/shadow-l.png");
415 
416     // Small Drop Shadow
417     private static final Image Shadow2Bottom = loadIcon("/icons/shadow2-b.png");
418     private static final Image Shadow2BottomLeft = loadIcon("/icons/shadow2-bl.png");
419     private static final Image Shadow2BottomRight = loadIcon("/icons/shadow2-br.png");
420     private static final Image Shadow2Right = loadIcon("/icons/shadow2-r.png");
421     private static final Image Shadow2TopRight = loadIcon("/icons/shadow2-tr.png");
422     private static final Image Shadow2TopLeft = loadIcon("/icons/shadow2-tl.png");
423     private static final Image Shadow2Left = loadIcon("/icons/shadow2-l.png");
424 }
425