| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ninepatch; |
| |
| import java.awt.Graphics2D; |
| import java.awt.Rectangle; |
| import java.awt.RenderingHints; |
| import java.awt.image.BufferedImage; |
| import java.io.IOException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Represents a 9-Patch bitmap. |
| */ |
| public class NinePatch { |
| public static final String EXTENSION_9PATCH = ".9.png"; |
| |
| private BufferedImage mImage; |
| |
| private int mMinWidth; |
| private int mMinHeight; |
| |
| private int[] row; |
| private int[] column; |
| |
| private boolean mVerticalStartWithPatch; |
| private boolean mHorizontalStartWithPatch; |
| |
| private List<Rectangle> mFixed; |
| private List<Rectangle> mPatches; |
| private List<Rectangle> mHorizontalPatches; |
| private List<Rectangle> mVerticalPatches; |
| |
| private Pair<Integer> mHorizontalPadding; |
| private Pair<Integer> mVerticalPadding; |
| |
| private float mHorizontalPatchesSum; |
| private float mVerticalPatchesSum; |
| |
| private int mRemainderHorizontal; |
| |
| private int mRemainderVertical; |
| |
| private final URL mFileUrl; |
| |
| /** |
| * Loads a 9 patch or regular bitmap. |
| * @param fileUrl the URL of the file to load. |
| * @param convert if <code>true</code>, non 9-patch bitmpa will be converted into a 9 patch. |
| * If <code>false</code> and the bitmap is not a 9 patch, the method will return |
| * <code>null</code>. |
| * @return a {@link NinePatch} or <code>null</code>. |
| * @throws IOException |
| */ |
| public static NinePatch load(URL fileUrl, boolean convert) throws IOException { |
| BufferedImage image = null; |
| try { |
| image = GraphicsUtilities.loadCompatibleImage(fileUrl); |
| } catch (MalformedURLException e) { |
| // really this shouldn't be happening since we're not creating the URL manually. |
| return null; |
| } |
| |
| boolean is9Patch = fileUrl.getPath().toLowerCase().endsWith(EXTENSION_9PATCH); |
| |
| if (is9Patch == false) { |
| if (convert) { |
| image = convertTo9Patch(image); |
| } else { |
| return null; |
| } |
| } else { |
| ensure9Patch(image); |
| } |
| |
| |
| return new NinePatch(fileUrl, image); |
| } |
| |
| public int getWidth() { |
| return mImage.getWidth() - 2; |
| } |
| |
| public int getHeight() { |
| return mImage.getHeight() - 2; |
| } |
| |
| /** |
| * |
| * @param padding array of left, top, right, bottom padding |
| * @return |
| */ |
| public boolean getPadding(int[] padding) { |
| padding[0] = mHorizontalPadding.mFirst; // left |
| padding[2] = mHorizontalPadding.mSecond; // right |
| padding[1] = mVerticalPadding.mFirst; // top |
| padding[3] = mVerticalPadding.mSecond; // bottom |
| return true; |
| } |
| |
| |
| public void draw(Graphics2D graphics2D, int x, int y, int scaledWidth, int scaledHeight) { |
| if (scaledWidth <= 1 || scaledHeight <= 1) { |
| return; |
| } |
| |
| Graphics2D g = (Graphics2D)graphics2D.create(); |
| g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, |
| RenderingHints.VALUE_INTERPOLATION_BILINEAR); |
| |
| |
| try { |
| if (mPatches.size() == 0) { |
| g.drawImage(mImage, x, y, scaledWidth, scaledHeight, null); |
| return; |
| } |
| |
| g.translate(x, y); |
| x = y = 0; |
| |
| computePatches(scaledWidth, scaledHeight); |
| |
| int fixedIndex = 0; |
| int horizontalIndex = 0; |
| int verticalIndex = 0; |
| int patchIndex = 0; |
| |
| boolean hStretch; |
| boolean vStretch; |
| |
| float vWeightSum = 1.0f; |
| float vRemainder = mRemainderVertical; |
| |
| vStretch = mVerticalStartWithPatch; |
| while (y < scaledHeight - 1) { |
| hStretch = mHorizontalStartWithPatch; |
| |
| int height = 0; |
| float vExtra = 0.0f; |
| |
| float hWeightSum = 1.0f; |
| float hRemainder = mRemainderHorizontal; |
| |
| while (x < scaledWidth - 1) { |
| Rectangle r; |
| if (!vStretch) { |
| if (hStretch) { |
| r = mHorizontalPatches.get(horizontalIndex++); |
| float extra = r.width / mHorizontalPatchesSum; |
| int width = (int) (extra * hRemainder / hWeightSum); |
| hWeightSum -= extra; |
| hRemainder -= width; |
| g.drawImage(mImage, x, y, x + width, y + r.height, r.x, r.y, |
| r.x + r.width, r.y + r.height, null); |
| x += width; |
| } else { |
| r = mFixed.get(fixedIndex++); |
| g.drawImage(mImage, x, y, x + r.width, y + r.height, r.x, r.y, |
| r.x + r.width, r.y + r.height, null); |
| x += r.width; |
| } |
| height = r.height; |
| } else { |
| if (hStretch) { |
| r = mPatches.get(patchIndex++); |
| vExtra = r.height / mVerticalPatchesSum; |
| height = (int) (vExtra * vRemainder / vWeightSum); |
| float extra = r.width / mHorizontalPatchesSum; |
| int width = (int) (extra * hRemainder / hWeightSum); |
| hWeightSum -= extra; |
| hRemainder -= width; |
| g.drawImage(mImage, x, y, x + width, y + height, r.x, r.y, |
| r.x + r.width, r.y + r.height, null); |
| x += width; |
| } else { |
| r = mVerticalPatches.get(verticalIndex++); |
| vExtra = r.height / mVerticalPatchesSum; |
| height = (int) (vExtra * vRemainder / vWeightSum); |
| g.drawImage(mImage, x, y, x + r.width, y + height, r.x, r.y, |
| r.x + r.width, r.y + r.height, null); |
| x += r.width; |
| } |
| |
| } |
| hStretch = !hStretch; |
| } |
| x = 0; |
| y += height; |
| if (vStretch) { |
| vWeightSum -= vExtra; |
| vRemainder -= height; |
| } |
| vStretch = !vStretch; |
| } |
| |
| } finally { |
| g.dispose(); |
| } |
| } |
| |
| void computePatches(int scaledWidth, int scaledHeight) { |
| boolean measuredWidth = false; |
| boolean endRow = true; |
| |
| int remainderHorizontal = 0; |
| int remainderVertical = 0; |
| |
| if (mFixed.size() > 0) { |
| int start = mFixed.get(0).y; |
| for (Rectangle rect : mFixed) { |
| if (rect.y > start) { |
| endRow = true; |
| measuredWidth = true; |
| } |
| if (!measuredWidth) { |
| remainderHorizontal += rect.width; |
| } |
| if (endRow) { |
| remainderVertical += rect.height; |
| endRow = false; |
| start = rect.y; |
| } |
| } |
| } |
| |
| mRemainderHorizontal = scaledWidth - remainderHorizontal; |
| |
| mRemainderVertical = scaledHeight - remainderVertical; |
| |
| mHorizontalPatchesSum = 0; |
| if (mHorizontalPatches.size() > 0) { |
| int start = -1; |
| for (Rectangle rect : mHorizontalPatches) { |
| if (rect.x > start) { |
| mHorizontalPatchesSum += rect.width; |
| start = rect.x; |
| } |
| } |
| } else { |
| int start = -1; |
| for (Rectangle rect : mPatches) { |
| if (rect.x > start) { |
| mHorizontalPatchesSum += rect.width; |
| start = rect.x; |
| } |
| } |
| } |
| |
| mVerticalPatchesSum = 0; |
| if (mVerticalPatches.size() > 0) { |
| int start = -1; |
| for (Rectangle rect : mVerticalPatches) { |
| if (rect.y > start) { |
| mVerticalPatchesSum += rect.height; |
| start = rect.y; |
| } |
| } |
| } else { |
| int start = -1; |
| for (Rectangle rect : mPatches) { |
| if (rect.y > start) { |
| mVerticalPatchesSum += rect.height; |
| start = rect.y; |
| } |
| } |
| } |
| } |
| |
| |
| private NinePatch(URL fileUrl, BufferedImage image) { |
| mFileUrl = fileUrl; |
| mImage = image; |
| |
| findPatches(); |
| } |
| |
| private void findPatches() { |
| int width = mImage.getWidth(); |
| int height = mImage.getHeight(); |
| |
| row = GraphicsUtilities.getPixels(mImage, 0, 0, width, 1, row); |
| column = GraphicsUtilities.getPixels(mImage, 0, 0, 1, height, column); |
| |
| boolean[] result = new boolean[1]; |
| Pair<List<Pair<Integer>>> left = getPatches(column, result); |
| mVerticalStartWithPatch = result[0]; |
| |
| result = new boolean[1]; |
| Pair<List<Pair<Integer>>> top = getPatches(row, result); |
| mHorizontalStartWithPatch = result[0]; |
| |
| mFixed = getRectangles(left.mFirst, top.mFirst); |
| mPatches = getRectangles(left.mSecond, top.mSecond); |
| |
| if (mFixed.size() > 0) { |
| mHorizontalPatches = getRectangles(left.mFirst, top.mSecond); |
| mVerticalPatches = getRectangles(left.mSecond, top.mFirst); |
| } else { |
| if (top.mFirst.size() > 0) { |
| mHorizontalPatches = new ArrayList<Rectangle>(0); |
| mVerticalPatches = getVerticalRectangles(top.mFirst); |
| } else if (left.mFirst.size() > 0) { |
| mHorizontalPatches = getHorizontalRectangles(left.mFirst); |
| mVerticalPatches = new ArrayList<Rectangle>(0); |
| } else { |
| mHorizontalPatches = mVerticalPatches = new ArrayList<Rectangle>(0); |
| } |
| } |
| |
| row = GraphicsUtilities.getPixels(mImage, 0, height - 1, width, 1, row); |
| column = GraphicsUtilities.getPixels(mImage, width - 1, 0, 1, height, column); |
| |
| top = getPatches(row, result); |
| mHorizontalPadding = getPadding(top.mFirst); |
| |
| left = getPatches(column, result); |
| mVerticalPadding = getPadding(left.mFirst); |
| } |
| |
| private List<Rectangle> getVerticalRectangles(List<Pair<Integer>> topPairs) { |
| List<Rectangle> rectangles = new ArrayList<Rectangle>(); |
| for (Pair<Integer> top : topPairs) { |
| int x = top.mFirst; |
| int width = top.mSecond - top.mFirst; |
| |
| rectangles.add(new Rectangle(x, 1, width, mImage.getHeight() - 2)); |
| } |
| return rectangles; |
| } |
| |
| private List<Rectangle> getHorizontalRectangles(List<Pair<Integer>> leftPairs) { |
| List<Rectangle> rectangles = new ArrayList<Rectangle>(); |
| for (Pair<Integer> left : leftPairs) { |
| int y = left.mFirst; |
| int height = left.mSecond - left.mFirst; |
| |
| rectangles.add(new Rectangle(1, y, mImage.getWidth() - 2, height)); |
| } |
| return rectangles; |
| } |
| |
| private Pair<Integer> getPadding(List<Pair<Integer>> pairs) { |
| if (pairs.size() == 0) { |
| return new Pair<Integer>(0, 0); |
| } else if (pairs.size() == 1) { |
| if (pairs.get(0).mFirst == 1) { |
| return new Pair<Integer>(pairs.get(0).mSecond - pairs.get(0).mFirst, 0); |
| } else { |
| return new Pair<Integer>(0, pairs.get(0).mSecond - pairs.get(0).mFirst); |
| } |
| } else { |
| int index = pairs.size() - 1; |
| return new Pair<Integer>(pairs.get(0).mSecond - pairs.get(0).mFirst, |
| pairs.get(index).mSecond - pairs.get(index).mFirst); |
| } |
| } |
| |
| private List<Rectangle> getRectangles(List<Pair<Integer>> leftPairs, |
| List<Pair<Integer>> topPairs) { |
| List<Rectangle> rectangles = new ArrayList<Rectangle>(); |
| for (Pair<Integer> left : leftPairs) { |
| int y = left.mFirst; |
| int height = left.mSecond - left.mFirst; |
| for (Pair<Integer> top : topPairs) { |
| int x = top.mFirst; |
| int width = top.mSecond - top.mFirst; |
| |
| rectangles.add(new Rectangle(x, y, width, height)); |
| } |
| } |
| return rectangles; |
| } |
| |
| private Pair<List<Pair<Integer>>> getPatches(int[] pixels, boolean[] startWithPatch) { |
| int lastIndex = 1; |
| int lastPixel = pixels[1]; |
| boolean first = true; |
| |
| List<Pair<Integer>> fixed = new ArrayList<Pair<Integer>>(); |
| List<Pair<Integer>> patches = new ArrayList<Pair<Integer>>(); |
| |
| for (int i = 1; i < pixels.length - 1; i++) { |
| int pixel = pixels[i]; |
| if (pixel != lastPixel) { |
| if (lastPixel == 0xFF000000) { |
| if (first) startWithPatch[0] = true; |
| patches.add(new Pair<Integer>(lastIndex, i)); |
| } else { |
| fixed.add(new Pair<Integer>(lastIndex, i)); |
| } |
| first = false; |
| |
| lastIndex = i; |
| lastPixel = pixel; |
| } |
| } |
| if (lastPixel == 0xFF000000) { |
| if (first) startWithPatch[0] = true; |
| patches.add(new Pair<Integer>(lastIndex, pixels.length - 1)); |
| } else { |
| fixed.add(new Pair<Integer>(lastIndex, pixels.length - 1)); |
| } |
| |
| if (patches.size() == 0) { |
| patches.add(new Pair<Integer>(1, pixels.length - 1)); |
| startWithPatch[0] = true; |
| fixed.clear(); |
| } |
| |
| return new Pair<List<Pair<Integer>>>(fixed, patches); |
| } |
| |
| private static void ensure9Patch(BufferedImage image) { |
| int width = image.getWidth(); |
| int height = image.getHeight(); |
| for (int i = 0; i < width; i++) { |
| int pixel = image.getRGB(i, 0); |
| if (pixel != 0 && pixel != 0xFF000000) { |
| image.setRGB(i, 0, 0); |
| } |
| pixel = image.getRGB(i, height - 1); |
| if (pixel != 0 && pixel != 0xFF000000) { |
| image.setRGB(i, height - 1, 0); |
| } |
| } |
| for (int i = 0; i < height; i++) { |
| int pixel = image.getRGB(0, i); |
| if (pixel != 0 && pixel != 0xFF000000) { |
| image.setRGB(0, i, 0); |
| } |
| pixel = image.getRGB(width - 1, i); |
| if (pixel != 0 && pixel != 0xFF000000) { |
| image.setRGB(width - 1, i, 0); |
| } |
| } |
| } |
| |
| private static BufferedImage convertTo9Patch(BufferedImage image) { |
| BufferedImage buffer = GraphicsUtilities.createTranslucentCompatibleImage( |
| image.getWidth() + 2, image.getHeight() + 2); |
| |
| Graphics2D g2 = buffer.createGraphics(); |
| g2.drawImage(image, 1, 1, null); |
| g2.dispose(); |
| |
| return buffer; |
| } |
| |
| static class Pair<E> { |
| E mFirst; |
| E mSecond; |
| |
| Pair(E first, E second) { |
| mFirst = first; |
| mSecond = second; |
| } |
| |
| @Override |
| public String toString() { |
| return "Pair[" + mFirst + ", " + mSecond + "]"; |
| } |
| } |
| } |