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