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