Home | History | Annotate | Download | only in ninepatch
      1 /*
      2  * Copyright (C) 2008 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 com.android.ninepatch;
     18 
     19 import java.awt.Graphics2D;
     20 import java.awt.Rectangle;
     21 import java.awt.RenderingHints;
     22 import java.awt.image.BufferedImage;
     23 import java.io.IOException;
     24 import java.io.InputStream;
     25 import java.net.MalformedURLException;
     26 import java.net.URL;
     27 import java.util.ArrayList;
     28 import java.util.List;
     29 
     30 /**
     31  * Represents a 9-Patch bitmap.
     32  */
     33 public class NinePatch {
     34     public static final String EXTENSION_9PATCH = ".9.png";
     35 
     36     private BufferedImage mImage;
     37 
     38     private int mMinWidth;
     39     private int mMinHeight;
     40 
     41     private int[] row;
     42     private int[] column;
     43 
     44     private boolean mVerticalStartWithPatch;
     45     private boolean mHorizontalStartWithPatch;
     46 
     47     private List<Rectangle> mFixed;
     48     private List<Rectangle> mPatches;
     49     private List<Rectangle> mHorizontalPatches;
     50     private List<Rectangle> mVerticalPatches;
     51 
     52     private Pair<Integer> mHorizontalPadding;
     53     private Pair<Integer> mVerticalPadding;
     54 
     55     private float mHorizontalPatchesSum;
     56     private float mVerticalPatchesSum;
     57 
     58     private int mRemainderHorizontal;
     59 
     60     private int mRemainderVertical;
     61 
     62     /**
     63      * Loads a 9 patch or regular bitmap.
     64      * @param fileUrl the URL of the file to load.
     65      * @param convert if <code>true</code>, non 9-patch bitmap will be converted into a 9 patch.
     66      * If <code>false</code> and the bitmap is not a 9 patch, the method will return
     67      * <code>null</code>.
     68      * @return a {@link NinePatch} or <code>null</code>.
     69      * @throws IOException
     70      */
     71     public static NinePatch load(URL fileUrl, boolean convert) throws IOException {
     72         BufferedImage image = null;
     73         try {
     74             image  = GraphicsUtilities.loadCompatibleImage(fileUrl);
     75         } catch (MalformedURLException e) {
     76             // really this shouldn't be happening since we're not creating the URL manually.
     77             return null;
     78         }
     79 
     80         boolean is9Patch = fileUrl.getPath().toLowerCase().endsWith(EXTENSION_9PATCH);
     81 
     82         return load(image, is9Patch, convert);
     83     }
     84 
     85     /**
     86      * Loads a 9 patch or regular bitmap.
     87      * @param stream the {@link InputStream} of the file to load.
     88      * @param is9Patch whether the file represents a 9-patch
     89      * @param convert if <code>true</code>, non 9-patch bitmap will be converted into a 9 patch.
     90      * If <code>false</code> and the bitmap is not a 9 patch, the method will return
     91      * <code>null</code>.
     92      * @return a {@link NinePatch} or <code>null</code>.
     93      * @throws IOException
     94      */
     95     public static NinePatch load(InputStream stream, boolean is9Patch, boolean convert)
     96             throws IOException {
     97         BufferedImage image = null;
     98         try {
     99             image  = GraphicsUtilities.loadCompatibleImage(stream);
    100         } catch (MalformedURLException e) {
    101             // really this shouldn't be happening since we're not creating the URL manually.
    102             return null;
    103         }
    104 
    105         return load(image, is9Patch, convert);
    106     }
    107 
    108     /**
    109      * Loads a 9 patch or regular bitmap.
    110      * @param image the source {@link BufferedImage}.
    111      * @param is9Patch whether the file represents a 9-patch
    112      * @param convert if <code>true</code>, non 9-patch bitmap will be converted into a 9 patch.
    113      * If <code>false</code> and the bitmap is not a 9 patch, the method will return
    114      * <code>null</code>.
    115      * @return a {@link NinePatch} or <code>null</code>.
    116      * @throws IOException
    117      */
    118     public static NinePatch load(BufferedImage image, boolean is9Patch, boolean convert) {
    119         if (is9Patch == false) {
    120             if (convert) {
    121                 image = convertTo9Patch(image);
    122             } else {
    123                 return null;
    124             }
    125         } else {
    126             ensure9Patch(image);
    127         }
    128 
    129         return new NinePatch(image);
    130     }
    131 
    132     public int getWidth() {
    133         return mImage.getWidth() - 2;
    134     }
    135 
    136     public int getHeight() {
    137         return mImage.getHeight() - 2;
    138     }
    139 
    140     /**
    141      *
    142      * @param padding array of left, top, right, bottom padding
    143      * @return
    144      */
    145     public boolean getPadding(int[] padding) {
    146         padding[0] = mHorizontalPadding.mFirst; // left
    147         padding[2] = mHorizontalPadding.mSecond; // right
    148         padding[1] = mVerticalPadding.mFirst; // top
    149         padding[3] = mVerticalPadding.mSecond; // bottom
    150         return true;
    151     }
    152 
    153 
    154     public void draw(Graphics2D graphics2D, int x, int y, int scaledWidth, int scaledHeight) {
    155         if (scaledWidth <= 1 || scaledHeight <= 1) {
    156             return;
    157         }
    158 
    159         Graphics2D g = (Graphics2D)graphics2D.create();
    160         g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
    161                 RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    162 
    163 
    164         try {
    165             if (mPatches.size() == 0) {
    166                 g.drawImage(mImage, x, y, scaledWidth, scaledHeight, null);
    167                 return;
    168             }
    169 
    170             g.translate(x, y);
    171             x = y = 0;
    172 
    173             computePatches(scaledWidth, scaledHeight);
    174 
    175             int fixedIndex = 0;
    176             int horizontalIndex = 0;
    177             int verticalIndex = 0;
    178             int patchIndex = 0;
    179 
    180             boolean hStretch;
    181             boolean vStretch;
    182 
    183             float vWeightSum = 1.0f;
    184             float vRemainder = mRemainderVertical;
    185 
    186             vStretch = mVerticalStartWithPatch;
    187             while (y < scaledHeight - 1) {
    188                 hStretch = mHorizontalStartWithPatch;
    189 
    190                 int height = 0;
    191                 float vExtra = 0.0f;
    192 
    193                 float hWeightSum = 1.0f;
    194                 float hRemainder = mRemainderHorizontal;
    195 
    196                 while (x < scaledWidth - 1) {
    197                     Rectangle r;
    198                     if (!vStretch) {
    199                         if (hStretch) {
    200                             r = mHorizontalPatches.get(horizontalIndex++);
    201                             float extra = r.width / mHorizontalPatchesSum;
    202                             int width = (int) (extra * hRemainder / hWeightSum);
    203                             hWeightSum -= extra;
    204                             hRemainder -= width;
    205                             g.drawImage(mImage, x, y, x + width, y + r.height, r.x, r.y,
    206                                     r.x + r.width, r.y + r.height, null);
    207                             x += width;
    208                         } else {
    209                             r = mFixed.get(fixedIndex++);
    210                             g.drawImage(mImage, x, y, x + r.width, y + r.height, r.x, r.y,
    211                                     r.x + r.width, r.y + r.height, null);
    212                             x += r.width;
    213                         }
    214                         height = r.height;
    215                     } else {
    216                         if (hStretch) {
    217                             r = mPatches.get(patchIndex++);
    218                             vExtra = r.height / mVerticalPatchesSum;
    219                             height = (int) (vExtra * vRemainder / vWeightSum);
    220                             float extra = r.width / mHorizontalPatchesSum;
    221                             int width = (int) (extra * hRemainder / hWeightSum);
    222                             hWeightSum -= extra;
    223                             hRemainder -= width;
    224                             g.drawImage(mImage, x, y, x + width, y + height, r.x, r.y,
    225                                     r.x + r.width, r.y + r.height, null);
    226                             x += width;
    227                         } else {
    228                             r = mVerticalPatches.get(verticalIndex++);
    229                             vExtra = r.height / mVerticalPatchesSum;
    230                             height = (int) (vExtra * vRemainder / vWeightSum);
    231                             g.drawImage(mImage, x, y, x + r.width, y + height, r.x, r.y,
    232                                     r.x + r.width, r.y + r.height, null);
    233                             x += r.width;
    234                         }
    235 
    236                     }
    237                     hStretch = !hStretch;
    238                 }
    239                 x = 0;
    240                 y += height;
    241                 if (vStretch) {
    242                     vWeightSum -= vExtra;
    243                     vRemainder -= height;
    244                 }
    245                 vStretch = !vStretch;
    246             }
    247 
    248         } finally {
    249             g.dispose();
    250         }
    251     }
    252 
    253     void computePatches(int scaledWidth, int scaledHeight) {
    254         boolean measuredWidth = false;
    255         boolean endRow = true;
    256 
    257         int remainderHorizontal = 0;
    258         int remainderVertical = 0;
    259 
    260         if (mFixed.size() > 0) {
    261             int start = mFixed.get(0).y;
    262             for (Rectangle rect : mFixed) {
    263                 if (rect.y > start) {
    264                     endRow = true;
    265                     measuredWidth = true;
    266                 }
    267                 if (!measuredWidth) {
    268                     remainderHorizontal += rect.width;
    269                 }
    270                 if (endRow) {
    271                     remainderVertical += rect.height;
    272                     endRow = false;
    273                     start = rect.y;
    274                 }
    275             }
    276         }
    277 
    278         mRemainderHorizontal = scaledWidth - remainderHorizontal;
    279 
    280         mRemainderVertical = scaledHeight - remainderVertical;
    281 
    282         mHorizontalPatchesSum = 0;
    283         if (mHorizontalPatches.size() > 0) {
    284             int start = -1;
    285             for (Rectangle rect : mHorizontalPatches) {
    286                 if (rect.x > start) {
    287                     mHorizontalPatchesSum += rect.width;
    288                     start = rect.x;
    289                 }
    290             }
    291         } else {
    292             int start = -1;
    293             for (Rectangle rect : mPatches) {
    294                 if (rect.x > start) {
    295                     mHorizontalPatchesSum += rect.width;
    296                     start = rect.x;
    297                 }
    298             }
    299         }
    300 
    301         mVerticalPatchesSum = 0;
    302         if (mVerticalPatches.size() > 0) {
    303             int start = -1;
    304             for (Rectangle rect : mVerticalPatches) {
    305                 if (rect.y > start) {
    306                     mVerticalPatchesSum += rect.height;
    307                     start = rect.y;
    308                 }
    309             }
    310         } else {
    311             int start = -1;
    312             for (Rectangle rect : mPatches) {
    313                 if (rect.y > start) {
    314                     mVerticalPatchesSum += rect.height;
    315                     start = rect.y;
    316                 }
    317             }
    318         }
    319     }
    320 
    321 
    322     private NinePatch(BufferedImage image) {
    323         mImage = image;
    324 
    325         findPatches();
    326     }
    327 
    328     private void findPatches() {
    329         int width = mImage.getWidth();
    330         int height = mImage.getHeight();
    331 
    332         row = GraphicsUtilities.getPixels(mImage, 0, 0, width, 1, row);
    333         column = GraphicsUtilities.getPixels(mImage, 0, 0, 1, height, column);
    334 
    335         boolean[] result = new boolean[1];
    336         Pair<List<Pair<Integer>>> left = getPatches(column, result);
    337         mVerticalStartWithPatch = result[0];
    338 
    339         result = new boolean[1];
    340         Pair<List<Pair<Integer>>> top = getPatches(row, result);
    341         mHorizontalStartWithPatch = result[0];
    342 
    343         mFixed = getRectangles(left.mFirst, top.mFirst);
    344         mPatches = getRectangles(left.mSecond, top.mSecond);
    345 
    346         if (mFixed.size() > 0) {
    347             mHorizontalPatches = getRectangles(left.mFirst, top.mSecond);
    348             mVerticalPatches = getRectangles(left.mSecond, top.mFirst);
    349         } else {
    350             if (top.mFirst.size() > 0) {
    351                 mHorizontalPatches = new ArrayList<Rectangle>(0);
    352                 mVerticalPatches = getVerticalRectangles(top.mFirst);
    353             } else if (left.mFirst.size() > 0) {
    354                 mHorizontalPatches = getHorizontalRectangles(left.mFirst);
    355                 mVerticalPatches = new ArrayList<Rectangle>(0);
    356             } else {
    357                 mHorizontalPatches = mVerticalPatches = new ArrayList<Rectangle>(0);
    358             }
    359         }
    360 
    361         row = GraphicsUtilities.getPixels(mImage, 0, height - 1, width, 1, row);
    362         column = GraphicsUtilities.getPixels(mImage, width - 1, 0, 1, height, column);
    363 
    364         top = getPatches(row, result);
    365         mHorizontalPadding = getPadding(top.mFirst);
    366 
    367         left = getPatches(column, result);
    368         mVerticalPadding = getPadding(left.mFirst);
    369     }
    370 
    371     private List<Rectangle> getVerticalRectangles(List<Pair<Integer>> topPairs) {
    372         List<Rectangle> rectangles = new ArrayList<Rectangle>();
    373         for (Pair<Integer> top : topPairs) {
    374             int x = top.mFirst;
    375             int width = top.mSecond - top.mFirst;
    376 
    377             rectangles.add(new Rectangle(x, 1, width, mImage.getHeight() - 2));
    378         }
    379         return rectangles;
    380     }
    381 
    382     private List<Rectangle> getHorizontalRectangles(List<Pair<Integer>> leftPairs) {
    383         List<Rectangle> rectangles = new ArrayList<Rectangle>();
    384         for (Pair<Integer> left : leftPairs) {
    385             int y = left.mFirst;
    386             int height = left.mSecond - left.mFirst;
    387 
    388             rectangles.add(new Rectangle(1, y, mImage.getWidth() - 2, height));
    389         }
    390         return rectangles;
    391     }
    392 
    393     private Pair<Integer> getPadding(List<Pair<Integer>> pairs) {
    394         if (pairs.size() == 0) {
    395             return new Pair<Integer>(0, 0);
    396         } else if (pairs.size() == 1) {
    397             if (pairs.get(0).mFirst == 1) {
    398                 return new Pair<Integer>(pairs.get(0).mSecond - pairs.get(0).mFirst, 0);
    399             } else {
    400                 return new Pair<Integer>(0, pairs.get(0).mSecond - pairs.get(0).mFirst);
    401             }
    402         } else {
    403             int index = pairs.size() - 1;
    404             return new Pair<Integer>(pairs.get(0).mSecond - pairs.get(0).mFirst,
    405                     pairs.get(index).mSecond - pairs.get(index).mFirst);
    406         }
    407     }
    408 
    409     private List<Rectangle> getRectangles(List<Pair<Integer>> leftPairs,
    410             List<Pair<Integer>> topPairs) {
    411         List<Rectangle> rectangles = new ArrayList<Rectangle>();
    412         for (Pair<Integer> left : leftPairs) {
    413             int y = left.mFirst;
    414             int height = left.mSecond - left.mFirst;
    415             for (Pair<Integer> top : topPairs) {
    416                 int x = top.mFirst;
    417                 int width = top.mSecond - top.mFirst;
    418 
    419                 rectangles.add(new Rectangle(x, y, width, height));
    420             }
    421         }
    422         return rectangles;
    423     }
    424 
    425     private Pair<List<Pair<Integer>>> getPatches(int[] pixels, boolean[] startWithPatch) {
    426         int lastIndex = 1;
    427         int lastPixel = pixels[1];
    428         boolean first = true;
    429 
    430         List<Pair<Integer>> fixed = new ArrayList<Pair<Integer>>();
    431         List<Pair<Integer>> patches = new ArrayList<Pair<Integer>>();
    432 
    433         for (int i = 1; i < pixels.length - 1; i++) {
    434             int pixel = pixels[i];
    435             if (pixel != lastPixel) {
    436                 if (lastPixel == 0xFF000000) {
    437                     if (first) startWithPatch[0] = true;
    438                     patches.add(new Pair<Integer>(lastIndex, i));
    439                 } else {
    440                     fixed.add(new Pair<Integer>(lastIndex, i));
    441                 }
    442                 first = false;
    443 
    444                 lastIndex = i;
    445                 lastPixel = pixel;
    446             }
    447         }
    448         if (lastPixel == 0xFF000000) {
    449             if (first) startWithPatch[0] = true;
    450             patches.add(new Pair<Integer>(lastIndex, pixels.length - 1));
    451         } else {
    452             fixed.add(new Pair<Integer>(lastIndex, pixels.length - 1));
    453         }
    454 
    455         if (patches.size() == 0) {
    456             patches.add(new Pair<Integer>(1, pixels.length - 1));
    457             startWithPatch[0] = true;
    458             fixed.clear();
    459         }
    460 
    461         return new Pair<List<Pair<Integer>>>(fixed, patches);
    462     }
    463 
    464     private static void ensure9Patch(BufferedImage image) {
    465         int width = image.getWidth();
    466         int height = image.getHeight();
    467         for (int i = 0; i < width; i++) {
    468             int pixel = image.getRGB(i, 0);
    469             if (pixel != 0 && pixel != 0xFF000000) {
    470                 image.setRGB(i, 0, 0);
    471             }
    472             pixel = image.getRGB(i, height - 1);
    473             if (pixel != 0 && pixel != 0xFF000000) {
    474                 image.setRGB(i, height - 1, 0);
    475             }
    476         }
    477         for (int i = 0; i < height; i++) {
    478             int pixel = image.getRGB(0, i);
    479             if (pixel != 0 && pixel != 0xFF000000) {
    480                 image.setRGB(0, i, 0);
    481             }
    482             pixel = image.getRGB(width - 1, i);
    483             if (pixel != 0 && pixel != 0xFF000000) {
    484                 image.setRGB(width - 1, i, 0);
    485             }
    486         }
    487     }
    488 
    489     private static BufferedImage convertTo9Patch(BufferedImage image) {
    490         BufferedImage buffer = GraphicsUtilities.createTranslucentCompatibleImage(
    491                 image.getWidth() + 2, image.getHeight() + 2);
    492 
    493         Graphics2D g2 = buffer.createGraphics();
    494         g2.drawImage(image, 1, 1, null);
    495         g2.dispose();
    496 
    497         return buffer;
    498     }
    499 
    500     static class Pair<E> {
    501         E mFirst;
    502         E mSecond;
    503 
    504         Pair(E first, E second) {
    505             mFirst = first;
    506             mSecond = second;
    507         }
    508 
    509         @Override
    510         public String toString() {
    511             return "Pair[" + mFirst + ", " + mSecond + "]";
    512         }
    513     }
    514 }
    515