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