/* * Copyright (C) 2010 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.Serializable; import java.util.ArrayList; import java.util.List; /** * The chunk information for a nine patch. * * This does not represent the bitmap, only the chunk info responsible for the padding and the * stretching areas. * * Since android.graphics.drawable.NinePatchDrawable and android.graphics.NinePatch both deal with * the nine patch chunk as a byte[], this class is converted to and from byte[] through * serialization. * * This is meant to be used with the NinePatch_Delegate in Layoutlib API 5+. */ public class NinePatchChunk implements Serializable { /** Generated Serial Version UID */ private static final long serialVersionUID = -7353439224505296217L; private static final int[] sPaddingRect = new int[4]; 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; /** * Data computed during drawing. */ static final class DrawingData { private int mRemainderHorizontal; private int mRemainderVertical; private float mHorizontalPatchesSum; private float mVerticalPatchesSum; } /** * Computes and returns the 9-patch chunks. * @param image the image containing both the content and the control outer line. * @return the {@link NinePatchChunk}. */ public static NinePatchChunk create(BufferedImage image) { NinePatchChunk chunk = new NinePatchChunk(); chunk.findPatches(image); return chunk; } public void draw(BufferedImage image, Graphics2D graphics2D, int x, int y, int scaledWidth, int scaledHeight, int destDensity, int srcDensity) { boolean scaling = destDensity != srcDensity && destDensity != 0 && srcDensity != 0; if (scaling) { try { graphics2D = (Graphics2D) graphics2D.create(); // scale and transform float densityScale = ((float) destDensity) / (float) srcDensity; // translate/rotate the canvas. graphics2D.translate(x, y); graphics2D.scale(densityScale, densityScale); // sets the new drawing bounds. scaledWidth /= densityScale; scaledHeight /= densityScale; x = y = 0; // draw draw(image, graphics2D, x, y, scaledWidth, scaledHeight); } finally { graphics2D.dispose(); } } else { // non density-scaled rendering draw(image, graphics2D, x, y, scaledWidth, scaledHeight); } } private void draw(BufferedImage image, 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.isEmpty()) { g.drawImage(image, x, y, scaledWidth, scaledHeight, null); return; } g.translate(x, y); x = y = 0; DrawingData data = 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 = data.mRemainderVertical; vStretch = mVerticalStartWithPatch; while (y < scaledHeight - 1) { hStretch = mHorizontalStartWithPatch; int height = 0; float vExtra = 0.0f; float hWeightSum = 1.0f; float hRemainder = data.mRemainderHorizontal; while (x < scaledWidth - 1) { Rectangle r; if (!vStretch) { if (hStretch) { r = mHorizontalPatches.get(horizontalIndex++); float extra = r.width / data.mHorizontalPatchesSum; int width = (int) (extra * hRemainder / hWeightSum); hWeightSum -= extra; hRemainder -= width; g.drawImage(image, 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(image, 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 / data.mVerticalPatchesSum; height = (int) (vExtra * vRemainder / vWeightSum); float extra = r.width / data.mHorizontalPatchesSum; int width = (int) (extra * hRemainder / hWeightSum); hWeightSum -= extra; hRemainder -= width; g.drawImage(image, 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 / data.mVerticalPatchesSum; height = (int) (vExtra * vRemainder / vWeightSum); g.drawImage(image, 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(); } } /** * Fills the given array with the nine patch padding. * * @param padding array of left, top, right, bottom padding */ public void getPadding(int[] padding) { padding[0] = mHorizontalPadding.mFirst; // left padding[2] = mHorizontalPadding.mSecond; // right padding[1] = mVerticalPadding.mFirst; // top padding[3] = mVerticalPadding.mSecond; // bottom } /** * Returns the padding as an int[] describing left, top, right, bottom. * * This method is not thread-safe and returns an array owned by the {@link NinePatchChunk} * class. * @return an internal array filled with the padding. */ public int[] getPadding() { getPadding(sPaddingRect); return sPaddingRect; } private DrawingData computePatches(int scaledWidth, int scaledHeight) { DrawingData data = new DrawingData(); boolean measuredWidth = false; boolean endRow = true; int remainderHorizontal = 0; int remainderVertical = 0; if (!mFixed.isEmpty()) { 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; } } } else { /* fully stretched without fixed regions (often single pixel high or wide). Since * width of vertical patches (and height of horizontal patches) are fixed, use them to * determine fixed space */ for (Rectangle rect : mVerticalPatches) { remainderHorizontal += rect.width; } for (Rectangle rect : mHorizontalPatches) { remainderVertical += rect.height; } } data.mRemainderHorizontal = scaledWidth - remainderHorizontal; data.mRemainderVertical = scaledHeight - remainderVertical; data.mHorizontalPatchesSum = 0; if (!mHorizontalPatches.isEmpty()) { int start = -1; for (Rectangle rect : mHorizontalPatches) { if (rect.x > start) { data.mHorizontalPatchesSum += rect.width; start = rect.x; } } } else { int start = -1; for (Rectangle rect : mPatches) { if (rect.x > start) { data.mHorizontalPatchesSum += rect.width; start = rect.x; } } } data.mVerticalPatchesSum = 0; if (!mVerticalPatches.isEmpty()) { int start = -1; for (Rectangle rect : mVerticalPatches) { if (rect.y > start) { data.mVerticalPatchesSum += rect.height; start = rect.y; } } } else { int start = -1; for (Rectangle rect : mPatches) { if (rect.y > start) { data.mVerticalPatchesSum += rect.height; start = rect.y; } } } return data; } /** * Finds the 9-patch patches and padding from a {@link BufferedImage} image that contains * both the image content and the control outer lines. */ private void findPatches(BufferedImage image) { // the size of the actual image content int width = image.getWidth() - 2; int height = image.getHeight() - 2; int[] row = null; int[] column = null; // extract the patch line. Make sure to start at 1 and be only as long as the image content, // to not include the outer control line. row = GraphicsUtilities.getPixels(image, 1, 0, width, 1, row); column = GraphicsUtilities.getPixels(image, 0, 1, 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.isEmpty()) { mHorizontalPatches = getRectangles(left.mFirst, top.mSecond); mVerticalPatches = getRectangles(left.mSecond, top.mFirst); } else { if (!top.mFirst.isEmpty()) { mHorizontalPatches = new ArrayList<Rectangle>(0); mVerticalPatches = getVerticalRectangles(height, top.mFirst); } else if (!left.mFirst.isEmpty()) { mHorizontalPatches = getHorizontalRectangles(width, left.mFirst); mVerticalPatches = new ArrayList<Rectangle>(0); } else { mHorizontalPatches = mVerticalPatches = new ArrayList<Rectangle>(0); } } // extract the padding line. Make sure to start at 1 and be only as long as the image // content, to not include the outer control line. row = GraphicsUtilities.getPixels(image, 1, height + 1, width, 1, row); column = GraphicsUtilities.getPixels(image, width + 1, 1, 1, height, column); top = getPatches(row, result); mHorizontalPadding = getPadding(top.mFirst); left = getPatches(column, result); mVerticalPadding = getPadding(left.mFirst); } private List<Rectangle> getVerticalRectangles(int imageHeight, 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, 0, width, imageHeight)); } return rectangles; } private List<Rectangle> getHorizontalRectangles(int imageWidth, 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(0, y, imageWidth, height)); } return rectangles; } private Pair<Integer> getPadding(List<Pair<Integer>> pairs) { if (pairs.isEmpty()) { return new Pair<Integer>(0, 0); } else if (pairs.size() == 1) { if (pairs.get(0).mFirst == 0) { 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; } /** * Computes a list of Patch based on a pixel line. * * This returns both the fixed areas, and the patches (stretchable) areas. * * The return value is a pair of list. The first list ({@link Pair#mFirst}) is the list * of fixed area. The second list ({@link Pair#mSecond}) is the list of stretchable areas. * * Each area is defined as a Pair of (start, end) coordinate in the given line. * * @param pixels the pixels of the control line. The line should have the same length as the * content (i.e. it should be stripped of the first/last control pixel which are not * used) * @param startWithPatch a boolean array of size 1 used to return the boolean value of whether * a patch (stretchable area) is first or not. * @return */ private Pair<List<Pair<Integer>>> getPatches(int[] pixels, boolean[] startWithPatch) { int lastIndex = 0; int lastPixel = pixels[0]; boolean first = true; List<Pair<Integer>> fixed = new ArrayList<Pair<Integer>>(); List<Pair<Integer>> patches = new ArrayList<Pair<Integer>>(); for (int i = 0; i < pixels.length; 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)); } else { fixed.add(new Pair<Integer>(lastIndex, pixels.length)); } if (patches.isEmpty()) { patches.add(new Pair<Integer>(1, pixels.length)); startWithPatch[0] = true; fixed.clear(); } return new Pair<List<Pair<Integer>>>(fixed, patches); } /** * A pair of values. * * @param <E> */ /*package*/ static class Pair<E> implements Serializable { /** Generated Serial Version UID */ private static final long serialVersionUID = -2204108979541762418L; E mFirst; E mSecond; Pair(E first, E second) { mFirst = first; mSecond = second; } @Override public String toString() { return "Pair[" + mFirst + ", " + mSecond + "]"; } } }