blob: 6a684ead299a566d2ba4f2faa36c9a841c2bc113 [file] [log] [blame]
/*
* 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 + "]";
}
}
}