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