Home | History | Annotate | Download | only in view
      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      *
     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