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 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