blob: 788c6c35332b2b56742e82ccd45192a2361c521e [file] [log] [blame]
Justin Klaassen10d07c82017-09-15 17:58:39 -04001/*
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}}
Justin Klaassen46c77c22017-10-30 17:25:37 -040044 * @param alpha alpha value to apply to the shadow
Justin Klaassen10d07c82017-09-15 17:58:39 -040045 *
46 * @return an image with the shadow painted in or the source image if shadowSize <= 1
47 */
48 @NonNull
Justin Klaassen46c77c22017-10-30 17:25:37 -040049 public static BufferedImage createDropShadow(BufferedImage source, int shadowSize, float
50 alpha) {
Justin Klaassen10d07c82017-09-15 17:58:39 -040051 shadowSize /= 2; // make shadow size have the same meaning as in the other shadow paint methods in this class
52
Justin Klaassen46c77c22017-10-30 17:25:37 -040053 return createDropShadow(source, shadowSize, 0.7f * alpha, 0);
Justin Klaassen10d07c82017-09-15 17:58:39 -040054 }
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
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")
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")
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 */
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 */
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 */
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 */
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
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}