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