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