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      * @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
     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
     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 }
    425