1 /* 2 * Copyright (C) 2010 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.Serializable; 24 import java.util.ArrayList; 25 import java.util.List; 26 27 /** 28 * The chunk information for a nine patch. 29 * 30 * This does not represent the bitmap, only the chunk info responsible for the padding and the 31 * stretching areas. 32 * 33 * Since android.graphics.drawable.NinePatchDrawable and android.graphics.NinePatch both deal with 34 * the nine patch chunk as a byte[], this class is converted to and from byte[] through 35 * serialization. 36 * 37 * This is meant to be used with the NinePatch_Delegate in Layoutlib API 5+. 38 */ 39 public class NinePatchChunk implements Serializable { 40 41 /** Generated Serial Version UID */ 42 private static final long serialVersionUID = -7353439224505296217L; 43 44 private static final int[] sPaddingRect = new int[4]; 45 46 private boolean mVerticalStartWithPatch; 47 private boolean mHorizontalStartWithPatch; 48 49 private List<Rectangle> mFixed; 50 private List<Rectangle> mPatches; 51 private List<Rectangle> mHorizontalPatches; 52 private List<Rectangle> mVerticalPatches; 53 54 private Pair<Integer> mHorizontalPadding; 55 private Pair<Integer> mVerticalPadding; 56 57 58 /** 59 * Data computed during drawing. 60 */ 61 static final class DrawingData { 62 private int mRemainderHorizontal; 63 private int mRemainderVertical; 64 private float mHorizontalPatchesSum; 65 private float mVerticalPatchesSum; 66 } 67 68 /** 69 * Computes and returns the 9-patch chunks. 70 * @param image the image containing both the content and the control outer line. 71 * @return the {@link NinePatchChunk}. 72 */ create(BufferedImage image)73 public static NinePatchChunk create(BufferedImage image) { 74 NinePatchChunk chunk = new NinePatchChunk(); 75 chunk.findPatches(image); 76 return chunk; 77 } 78 draw(BufferedImage image, Graphics2D graphics2D, int x, int y, int scaledWidth, int scaledHeight, int destDensity, int srcDensity)79 public void draw(BufferedImage image, Graphics2D graphics2D, int x, int y, int scaledWidth, 80 int scaledHeight, int destDensity, int srcDensity) { 81 82 boolean scaling = destDensity != srcDensity && destDensity != 0 && srcDensity != 0; 83 84 if (scaling) { 85 try { 86 graphics2D = (Graphics2D) graphics2D.create(); 87 88 // scale and transform 89 float densityScale = (float) destDensity / srcDensity; 90 91 // translate/rotate the canvas. 92 graphics2D.translate(x, y); 93 graphics2D.scale(densityScale, densityScale); 94 95 // sets the new drawing bounds. 96 scaledWidth /= densityScale; 97 scaledHeight /= densityScale; 98 x = y = 0; 99 100 // draw 101 draw(image, graphics2D, x, y, scaledWidth, scaledHeight); 102 } finally { 103 graphics2D.dispose(); 104 } 105 } else { 106 // non density-scaled rendering 107 draw(image, graphics2D, x, y, scaledWidth, scaledHeight); 108 } 109 } 110 draw(BufferedImage image, Graphics2D graphics2D, int x, int y, int scaledWidth, int scaledHeight)111 private void draw(BufferedImage image, Graphics2D graphics2D, int x, int y, int scaledWidth, 112 int scaledHeight) { 113 if (scaledWidth <= 1 || scaledHeight <= 1) { 114 return; 115 } 116 117 Graphics2D g = (Graphics2D)graphics2D.create(); 118 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 119 RenderingHints.VALUE_INTERPOLATION_BILINEAR); 120 121 try { 122 if (mPatches.size() == 0) { 123 g.drawImage(image, x, y, scaledWidth, scaledHeight, null); 124 return; 125 } 126 127 g.translate(x, y); 128 x = y = 0; 129 130 DrawingData data = computePatches(scaledWidth, scaledHeight); 131 132 int fixedIndex = 0; 133 int horizontalIndex = 0; 134 int verticalIndex = 0; 135 int patchIndex = 0; 136 137 boolean hStretch; 138 boolean vStretch; 139 140 float vWeightSum = 1.0f; 141 float vRemainder = data.mRemainderVertical; 142 143 vStretch = mVerticalStartWithPatch; 144 while (y < scaledHeight - 1) { 145 hStretch = mHorizontalStartWithPatch; 146 147 int height = 0; 148 float vExtra = 0.0f; 149 150 float hWeightSum = 1.0f; 151 float hRemainder = data.mRemainderHorizontal; 152 153 while (x < scaledWidth - 1) { 154 Rectangle r; 155 if (!vStretch) { 156 if (hStretch) { 157 r = mHorizontalPatches.get(horizontalIndex++); 158 float extra = r.width / data.mHorizontalPatchesSum; 159 int width = (int) (extra * hRemainder / hWeightSum); 160 hWeightSum -= extra; 161 hRemainder -= width; 162 g.drawImage(image, x, y, x + width, y + r.height, r.x, r.y, 163 r.x + r.width, r.y + r.height, null); 164 x += width; 165 } else { 166 r = mFixed.get(fixedIndex++); 167 g.drawImage(image, x, y, x + r.width, y + r.height, r.x, r.y, 168 r.x + r.width, r.y + r.height, null); 169 x += r.width; 170 } 171 height = r.height; 172 } else { 173 if (hStretch) { 174 r = mPatches.get(patchIndex++); 175 vExtra = r.height / data.mVerticalPatchesSum; 176 height = (int) (vExtra * vRemainder / vWeightSum); 177 float extra = r.width / data.mHorizontalPatchesSum; 178 int width = (int) (extra * hRemainder / hWeightSum); 179 hWeightSum -= extra; 180 hRemainder -= width; 181 g.drawImage(image, x, y, x + width, y + height, r.x, r.y, 182 r.x + r.width, r.y + r.height, null); 183 x += width; 184 } else { 185 r = mVerticalPatches.get(verticalIndex++); 186 vExtra = r.height / data.mVerticalPatchesSum; 187 height = (int) (vExtra * vRemainder / vWeightSum); 188 g.drawImage(image, x, y, x + r.width, y + height, r.x, r.y, 189 r.x + r.width, r.y + r.height, null); 190 x += r.width; 191 } 192 193 } 194 hStretch = !hStretch; 195 } 196 x = 0; 197 y += height; 198 if (vStretch) { 199 vWeightSum -= vExtra; 200 vRemainder -= height; 201 } 202 vStretch = !vStretch; 203 } 204 205 } finally { 206 g.dispose(); 207 } 208 } 209 210 /** 211 * Fills the given array with the nine patch padding. 212 * 213 * @param padding array of left, top, right, bottom padding 214 */ getPadding(int[] padding)215 public void getPadding(int[] padding) { 216 padding[0] = mHorizontalPadding.mFirst; // left 217 padding[2] = mHorizontalPadding.mSecond; // right 218 padding[1] = mVerticalPadding.mFirst; // top 219 padding[3] = mVerticalPadding.mSecond; // bottom 220 } 221 222 /** 223 * Returns the padding as an int[] describing left, top, right, bottom. 224 * 225 * This method is not thread-safe and returns an array owned by the {@link NinePatchChunk} 226 * class. 227 * @return an internal array filled with the padding. 228 */ getPadding()229 public int[] getPadding() { 230 getPadding(sPaddingRect); 231 return sPaddingRect; 232 } 233 computePatches(int scaledWidth, int scaledHeight)234 private DrawingData computePatches(int scaledWidth, int scaledHeight) { 235 DrawingData data = new DrawingData(); 236 boolean measuredWidth = false; 237 boolean endRow = true; 238 239 int remainderHorizontal = 0; 240 int remainderVertical = 0; 241 242 if (mFixed.size() > 0) { 243 int start = mFixed.get(0).y; 244 for (Rectangle rect : mFixed) { 245 if (rect.y > start) { 246 endRow = true; 247 measuredWidth = true; 248 } 249 if (!measuredWidth) { 250 remainderHorizontal += rect.width; 251 } 252 if (endRow) { 253 remainderVertical += rect.height; 254 endRow = false; 255 start = rect.y; 256 } 257 } 258 } 259 260 data.mRemainderHorizontal = scaledWidth - remainderHorizontal; 261 data.mRemainderVertical = scaledHeight - remainderVertical; 262 263 data.mHorizontalPatchesSum = 0; 264 if (mHorizontalPatches.size() > 0) { 265 int start = -1; 266 for (Rectangle rect : mHorizontalPatches) { 267 if (rect.x > start) { 268 data.mHorizontalPatchesSum += rect.width; 269 start = rect.x; 270 } 271 } 272 } else { 273 int start = -1; 274 for (Rectangle rect : mPatches) { 275 if (rect.x > start) { 276 data.mHorizontalPatchesSum += rect.width; 277 start = rect.x; 278 } 279 } 280 } 281 282 data.mVerticalPatchesSum = 0; 283 if (mVerticalPatches.size() > 0) { 284 int start = -1; 285 for (Rectangle rect : mVerticalPatches) { 286 if (rect.y > start) { 287 data.mVerticalPatchesSum += rect.height; 288 start = rect.y; 289 } 290 } 291 } else { 292 int start = -1; 293 for (Rectangle rect : mPatches) { 294 if (rect.y > start) { 295 data.mVerticalPatchesSum += rect.height; 296 start = rect.y; 297 } 298 } 299 } 300 301 return data; 302 } 303 304 305 /** 306 * Finds the 9-patch patches and padding from a {@link BufferedImage} image that contains 307 * both the image content and the control outer lines. 308 */ findPatches(BufferedImage image)309 private void findPatches(BufferedImage image) { 310 // the size of the actual image content 311 int width = image.getWidth() - 2; 312 int height = image.getHeight() - 2; 313 314 int[] row = null; 315 int[] column = null; 316 317 // extract the patch line. Make sure to start at 1 and be only as long as the image content, 318 // to not include the outer control line. 319 row = GraphicsUtilities.getPixels(image, 1, 0, width, 1, row); 320 column = GraphicsUtilities.getPixels(image, 0, 1, 1, height, column); 321 322 boolean[] result = new boolean[1]; 323 Pair<List<Pair<Integer>>> left = getPatches(column, result); 324 mVerticalStartWithPatch = result[0]; 325 326 result = new boolean[1]; 327 Pair<List<Pair<Integer>>> top = getPatches(row, result); 328 mHorizontalStartWithPatch = result[0]; 329 330 mFixed = getRectangles(left.mFirst, top.mFirst); 331 mPatches = getRectangles(left.mSecond, top.mSecond); 332 333 if (mFixed.size() > 0) { 334 mHorizontalPatches = getRectangles(left.mFirst, top.mSecond); 335 mVerticalPatches = getRectangles(left.mSecond, top.mFirst); 336 } else { 337 if (top.mFirst.size() > 0) { 338 mHorizontalPatches = new ArrayList<Rectangle>(0); 339 mVerticalPatches = getVerticalRectangles(height, top.mFirst); 340 } else if (left.mFirst.size() > 0) { 341 mHorizontalPatches = getHorizontalRectangles(width, left.mFirst); 342 mVerticalPatches = new ArrayList<Rectangle>(0); 343 } else { 344 mHorizontalPatches = mVerticalPatches = new ArrayList<Rectangle>(0); 345 } 346 } 347 348 // extract the padding line. Make sure to start at 1 and be only as long as the image 349 // content, to not include the outer control line. 350 row = GraphicsUtilities.getPixels(image, 1, height + 1, width, 1, row); 351 column = GraphicsUtilities.getPixels(image, width + 1, 1, 1, height, column); 352 353 top = getPatches(row, result); 354 mHorizontalPadding = getPadding(top.mFirst); 355 356 left = getPatches(column, result); 357 mVerticalPadding = getPadding(left.mFirst); 358 } 359 getVerticalRectangles(int imageHeight, List<Pair<Integer>> topPairs)360 private List<Rectangle> getVerticalRectangles(int imageHeight, 361 List<Pair<Integer>> topPairs) { 362 List<Rectangle> rectangles = new ArrayList<Rectangle>(); 363 for (Pair<Integer> top : topPairs) { 364 int x = top.mFirst; 365 int width = top.mSecond - top.mFirst; 366 367 rectangles.add(new Rectangle(x, 0, width, imageHeight)); 368 } 369 return rectangles; 370 } 371 getHorizontalRectangles(int imageWidth, List<Pair<Integer>> leftPairs)372 private List<Rectangle> getHorizontalRectangles(int imageWidth, 373 List<Pair<Integer>> leftPairs) { 374 List<Rectangle> rectangles = new ArrayList<Rectangle>(); 375 for (Pair<Integer> left : leftPairs) { 376 int y = left.mFirst; 377 int height = left.mSecond - left.mFirst; 378 379 rectangles.add(new Rectangle(0, y, imageWidth, height)); 380 } 381 return rectangles; 382 } 383 getPadding(List<Pair<Integer>> pairs)384 private Pair<Integer> getPadding(List<Pair<Integer>> pairs) { 385 if (pairs.size() == 0) { 386 return new Pair<Integer>(0, 0); 387 } else if (pairs.size() == 1) { 388 if (pairs.get(0).mFirst == 0) { 389 return new Pair<Integer>(pairs.get(0).mSecond - pairs.get(0).mFirst, 0); 390 } else { 391 return new Pair<Integer>(0, pairs.get(0).mSecond - pairs.get(0).mFirst); 392 } 393 } else { 394 int index = pairs.size() - 1; 395 return new Pair<Integer>(pairs.get(0).mSecond - pairs.get(0).mFirst, 396 pairs.get(index).mSecond - pairs.get(index).mFirst); 397 } 398 } 399 getRectangles(List<Pair<Integer>> leftPairs, List<Pair<Integer>> topPairs)400 private List<Rectangle> getRectangles(List<Pair<Integer>> leftPairs, 401 List<Pair<Integer>> topPairs) { 402 List<Rectangle> rectangles = new ArrayList<Rectangle>(); 403 for (Pair<Integer> left : leftPairs) { 404 int y = left.mFirst; 405 int height = left.mSecond - left.mFirst; 406 for (Pair<Integer> top : topPairs) { 407 int x = top.mFirst; 408 int width = top.mSecond - top.mFirst; 409 410 rectangles.add(new Rectangle(x, y, width, height)); 411 } 412 } 413 return rectangles; 414 } 415 416 /** 417 * Computes a list of Patch based on a pixel line. 418 * 419 * This returns both the fixed areas, and the patches (stretchable) areas. 420 * 421 * The return value is a pair of list. The first list ({@link Pair#mFirst}) is the list 422 * of fixed area. The second list ({@link Pair#mSecond}) is the list of stretchable areas. 423 * 424 * Each area is defined as a Pair of (start, end) coordinate in the given line. 425 * 426 * @param pixels the pixels of the control line. The line should have the same length as the 427 * content (i.e. it should be stripped of the first/last control pixel which are not 428 * used) 429 * @param startWithPatch a boolean array of size 1 used to return the boolean value of whether 430 * a patch (stretchable area) is first or not. 431 * @return 432 */ getPatches(int[] pixels, boolean[] startWithPatch)433 private Pair<List<Pair<Integer>>> getPatches(int[] pixels, boolean[] startWithPatch) { 434 int lastIndex = 0; 435 int lastPixel = pixels[0]; 436 boolean first = true; 437 438 List<Pair<Integer>> fixed = new ArrayList<Pair<Integer>>(); 439 List<Pair<Integer>> patches = new ArrayList<Pair<Integer>>(); 440 441 for (int i = 0; i < pixels.length; i++) { 442 int pixel = pixels[i]; 443 if (pixel != lastPixel) { 444 if (lastPixel == 0xFF000000) { 445 if (first) startWithPatch[0] = true; 446 patches.add(new Pair<Integer>(lastIndex, i)); 447 } else { 448 fixed.add(new Pair<Integer>(lastIndex, i)); 449 } 450 first = false; 451 452 lastIndex = i; 453 lastPixel = pixel; 454 } 455 } 456 if (lastPixel == 0xFF000000) { 457 if (first) startWithPatch[0] = true; 458 patches.add(new Pair<Integer>(lastIndex, pixels.length)); 459 } else { 460 fixed.add(new Pair<Integer>(lastIndex, pixels.length)); 461 } 462 463 if (patches.size() == 0) { 464 patches.add(new Pair<Integer>(1, pixels.length)); 465 startWithPatch[0] = true; 466 fixed.clear(); 467 } 468 469 return new Pair<List<Pair<Integer>>>(fixed, patches); 470 } 471 472 /** 473 * A pair of values. 474 * 475 * @param <E> 476 */ 477 /*package*/ static class Pair<E> implements Serializable { 478 /** Generated Serial Version UID */ 479 private static final long serialVersionUID = -2204108979541762418L; 480 E mFirst; 481 E mSecond; 482 Pair(E first, E second)483 Pair(E first, E second) { 484 mFirst = first; 485 mSecond = second; 486 } 487 488 @Override toString()489 public String toString() { 490 return "Pair[" + mFirst + ", " + mSecond + "]"; 491 } 492 } 493 494 } 495