/* * Copyright (C) 2006 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 android.graphics; import android.graphics.BitmapFactory.Res_png_9patch; import android.util.Log; /** * The NinePatch class permits drawing a bitmap in nine sections. * The four corners are unscaled; the four edges are scaled in one axis, * and the middle is scaled in both axes. Normally, the middle is * transparent so that the patch can provide a selection about a rectangle. * Essentially, it allows the creation of custom graphics that will scale the * way that you define, when content added within the image exceeds the normal * location of the graphic. For a thorough explanation of a NinePatch image, * read the discussion in the * <a href="{@docRoot}guide/topics/graphics/2d-graphics.html#nine-patch">2D * Graphics</a> document. * <p> * The <a href="{@docRoot}guide/developing/tools/draw9patch.html">Draw 9-Patch</a> * tool offers an extremely handy way to create your NinePatch images, * using a WYSIWYG graphics editor. * </p> */ public class NinePatch { public static final boolean debug = true; /** * Create a drawable projection from a bitmap to nine patches. * * @param bitmap The bitmap describing the patches. * @param chunk The 9-patch data chunk describing how the underlying * bitmap is split apart and drawn. * @param srcName The name of the source for the bitmap. Might be null. */ public NinePatch(Bitmap bitmap, byte[] chunk, String srcName) { mBitmap = bitmap; mChunk = Res_png_9patch.deserialize(chunk); mSrcName = srcName; validateNinePatchChunk(mBitmap, mChunk); } /** * Create a drawable projection from a bitmap to nine patches. * * @param bitmap The bitmap describing the patches. * @param chunk The 9-patch data chunk describing how the underlying * bitmap is split apart and drawn. * @param srcName The name of the source for the bitmap. Might be null. */ public NinePatch(Bitmap bitmap, Res_png_9patch chunk, String srcName) { mBitmap = bitmap; mChunk = chunk; mSrcName = srcName; validateNinePatchChunk(mBitmap, chunk); } /** * @hide */ public NinePatch(NinePatch patch) { mBitmap = patch.mBitmap; mChunk = patch.mChunk; mSrcName = patch.mSrcName; if (patch.mPaint != null) { mPaint = new Paint(patch.mPaint); } validateNinePatchChunk(mBitmap, mChunk); } public void setPaint(Paint p) { mPaint = p; } /** * Draw a bitmap of nine patches. * * @param canvas A container for the current matrix and clip used to draw the bitmap. * @param location Where to draw the bitmap. */ public void draw(Canvas canvas, RectF location) { draw(canvas, location, mPaint, canvas.mDensity, mBitmap.getDensity()); } /** * Draw a bitmap of nine patches. * * @param canvas A container for the current matrix and clip used to draw the bitmap. * @param location Where to draw the bitmap. */ public void draw(Canvas canvas, Rect location) { draw(canvas, new RectF(location), mPaint, canvas.mDensity, mBitmap.getDensity()); } /** * Draw a bitmap of nine patches. * * @param canvas A container for the current matrix and clip used to draw the bitmap. * @param location Where to draw the bitmap. * @param paint The Paint to draw through. */ public void draw(Canvas canvas, Rect location, Paint paint) { draw(canvas, new RectF(location), paint, canvas.mDensity, mBitmap.getDensity()); } private void draw(Canvas canvas, RectF location, Paint paint, int destDensity, int srcDensity) { Res_png_9patch chunk = mBitmap.getNinePatch(); if (destDensity == srcDensity || destDensity == 0 || srcDensity == 0) { Log.v("NinePatch", "Drawing unscaled 9-patch: (" + location.left + "," + location.top + ")-(" + location.right + "," + location.bottom + ")"); NinePatch_Draw(canvas, location, mBitmap, chunk, paint); } else { canvas.save(); float scale = destDensity / srcDensity; canvas.translate(location.left, location.top); canvas.scale(scale, scale); location.right = (location.right - location.left) / scale; location.bottom = (location.bottom - location.top) / scale; location.left = location.top = 0; Log.v("NinePatch", "Drawing unscaled 9-patch: (" + location.left + "," + location.top + ")-(" + location.right + "," + location.bottom + ")" + " " + "srcDensity=" + srcDensity + " destDensity=" + destDensity); NinePatch_Draw(canvas, location, mBitmap, chunk, paint); canvas.restore(); } // In MayLoon, just draw the original bitmap. //canvas.drawBitmap(mBitmap, null, location, paint); } private void NinePatch_Draw(Canvas canvas, RectF location, Bitmap bitmap, Res_png_9patch chunk, Paint paint) { Paint defaultPaint = new Paint(); if (null == paint) { // matches default dither in NinePatchDrawable.java. defaultPaint.setDither(true); paint = defaultPaint; } if (debug) { Log.v("NinePatch", "======== ninepatch location [" + location.width() + " " + location.height() + "]"); Log.v("NinePatch", "======== ninepatch paint bm [" + bitmap.getWidth() + "," + bitmap.getHeight() + "]"); Log.v("NinePatch", "======== ninepatch xDivs [" + chunk.xDivs[0] + "," + chunk.xDivs[1] + "]"); Log.v("NinePatch", "======== ninepatch yDivs [" + chunk.yDivs[0] + "," + chunk.yDivs[1] + "]"); } if (location.isEmpty() || bitmap.getWidth() == 0 || bitmap.getHeight() == 0 /* * || (paint * && paint-> * getXfermode * () == NULL * && * paint->getAlpha * () == 0) */) { if (debug) Log.v("NinePatch", "======== abort ninepatch draw"); return; } // boolean hasXfer = paint.getXfermode() != null; boolean hasXfer = false; // MayLoon does not support xfermode now RectF dst = new RectF(); RectF src = new RectF(); int x0 = chunk.xDivs[0]; int y0 = chunk.yDivs[0]; int initColor = paint.getColor(); byte numXDivs = chunk.numXDivs; byte numYDivs = chunk.numYDivs; int i; int j; int colorIndex = 0; int color; boolean xIsStretchable; boolean initialXIsStretchable = (x0 == 0); boolean yIsStretchable = (y0 == 0); int bitmapWidth = bitmap.getWidth(); int bitmapHeight = bitmap.getHeight(); float[] dstRights = new float[numXDivs + 1]; boolean dstRightsHaveBeenCached = false; int numStretchyXPixelsRemaining = 0; for (i = 0; i < numXDivs; i += 2) { numStretchyXPixelsRemaining += chunk.xDivs[i + 1] - chunk.xDivs[i]; } int numFixedXPixelsRemaining = bitmapWidth - numStretchyXPixelsRemaining; int numStretchyYPixelsRemaining = 0; for (i = 0; i < numYDivs; i += 2) { numStretchyYPixelsRemaining += chunk.yDivs[i + 1] - chunk.yDivs[i]; } int numFixedYPixelsRemaining = bitmapHeight - numStretchyYPixelsRemaining; src.top = 0; dst.top = location.top; // The first row always starts with the top being at y=0 and the bottom // being either yDivs[1] (if yDivs[0]=0) of yDivs[0]. In the former case // the first row is stretchable along the Y axis, otherwise it is fixed. // The last row always ends with the bottom being bitmap.height and the // top being either yDivs[numYDivs-2] (if yDivs[numYDivs-1]=bitmap.height) // or yDivs[numYDivs-1]. In the former case the last row is stretchable // along the Y axis, otherwise it is fixed. // // The first and last columns are similarly treated with respect to the X // axis. // The above is to help explain some of the special casing that goes on // the code below. // The initial yDiv and whether the first row is considered stretchable // or not depends on whether yDiv[0] was zero or not. for (j = yIsStretchable ? 1 : 0; j <= numYDivs && src.top < bitmapHeight; j++, yIsStretchable = !yIsStretchable) { src.left = 0; dst.left = location.left; if (j == numYDivs) { src.bottom = bitmapHeight; dst.bottom = location.bottom; } else { src.bottom = chunk.yDivs[j]; float srcYSize = src.bottom - src.top; if (yIsStretchable) { dst.bottom = dst.top + calculateStretch(location.bottom, dst.top, srcYSize, numStretchyYPixelsRemaining, numFixedYPixelsRemaining); numStretchyYPixelsRemaining -= srcYSize; } else { dst.bottom = dst.top + srcYSize; numFixedYPixelsRemaining -= srcYSize; } } xIsStretchable = initialXIsStretchable; // The initial xDiv and whether the first column is considered // stretchable or not depends on whether xDiv[0] was zero or not. for (i = xIsStretchable ? 1 : 0; i <= numXDivs && src.left < bitmapWidth; i++, xIsStretchable = !xIsStretchable) { color = chunk.colors[colorIndex++]; if (i == numXDivs) { src.right = bitmapWidth; dst.right = location.right; } else { src.right = chunk.xDivs[i]; if (dstRightsHaveBeenCached) { dst.right = dstRights[i]; } else { float srcXSize = src.right - src.left; if (xIsStretchable) { dst.right = dst.left + calculateStretch(location.right, dst.left, srcXSize, numStretchyXPixelsRemaining, numFixedXPixelsRemaining); numStretchyXPixelsRemaining -= srcXSize; } else { dst.right = dst.left + srcXSize; numFixedXPixelsRemaining -= srcXSize; } dstRights[i] = dst.right; } } // If this horizontal patch is too small to be displayed, leave // the destination left edge where it is and go on to the next // patch // in the source. if (src.left >= src.right) { src.left = src.right; continue; } // Make sure that we actually have room to draw any bits if (dst.right <= dst.left || dst.bottom <= dst.top) { // goto nextDiv; } else { // Not handle this in MayLoon now. // // If this patch is transparent, skip and don't draw. // if (color == Res_png_9patch.TRANSPARENT_COLOR && // !hasXfer) { // if (outRegion) { // if (*outRegion == NULL) { // *outRegion = new SkRegion(); // } // SkIRect idst; // dst.round(&idst); // //LOGI("Adding trans rect: (%d,%d)-(%d,%d)\n", // // idst.left, idst.top, idst.right, idst.bottom); // (*outRegion)->op(idst, SkRegion::kUnion_Op); // } // goto nextDiv; // } if (canvas != null) { drawStretchyPatch(canvas, src, dst, bitmap, paint, initColor, color, hasXfer); } } src.left = src.right; dst.left = dst.right; } src.top = src.bottom; dst.top = dst.bottom; dstRightsHaveBeenCached = true; } } private float calculateStretch(float boundsLimit, float startingPoint, float srcSpace, float numStrechyPixelsRemaining, float numFixedPixelsRemaining) { float spaceRemaining = boundsLimit - startingPoint; float stretchySpaceRemaining = spaceRemaining - numFixedPixelsRemaining; return srcSpace * stretchySpaceRemaining / numStrechyPixelsRemaining; } private void drawStretchyPatch(Canvas canvas, RectF src, RectF dst, Bitmap bitmap, Paint paint, int initColor, int colorHint, boolean hasXfer) { if (colorHint != Res_png_9patch.NO_COLOR) { int modAlpha = (int) (Color.alpha(colorHint) * paint.getAlpha() / 256.0f); int color = Color.argb(modAlpha, Color.red(colorHint), Color.green(colorHint), Color.blue(colorHint)); paint.setColor(color); canvas.drawRect(dst, paint); paint.setColor(initColor); } else if (src.width() == 1 && src.height() == 1) { int c = bitmap.getPixel((int)src.left, (int)src.top); if (0 != c || hasXfer) { int prev = paint.getColor(); paint.setColor(c); canvas.drawRect(dst, paint); paint.setColor(prev); } } else { Rect tmpSrc = new Rect((int) src.left, (int) src.top, (int) src.right, (int) src.bottom); canvas.drawBitmap(bitmap, tmpSrc, dst, paint); } } /** * Return the underlying bitmap's density, as per * {@link Bitmap#getDensity() Bitmap.getDensity()}. */ public int getDensity() { return mBitmap.mDensity; } public int getWidth() { return mBitmap.getWidth(); } public int getHeight() { return mBitmap.getHeight(); } public final boolean hasAlpha() { return mBitmap.hasAlpha(); } public final Region getTransparentRegion(Rect location) { // int r = nativeGetTransparentRegion(mBitmap.ni(), mChunk, location); // return r != 0 ? new Region(r) : null; Log.e("NinePatch", "Not implemented yet!"); return null; } public static boolean isNinePatchChunk(byte[] chunk) { if (chunk == null) { return false; } if (chunk.length < 32) { return false; } if (chunk[0] != 0) { return false; } return true; } private final Bitmap mBitmap; private final Res_png_9patch mChunk; private Paint mPaint; private String mSrcName; // Useful for debugging private static void validateNinePatchChunk(Bitmap bitmap, Res_png_9patch chunk) { // if (chunk.length < 32) { // throw new RuntimeException("Array too small for chunk."); // } // XXX Also check that dimensions are correct. // In MayLoon, the chunk should have been deserialized already. if (chunk.wasDeserialized != 1) { throw new RuntimeException("NinePatchChunk is not deserialized!."); } } private static native int nativeGetTransparentRegion( int bitmap, byte[] chunk, Rect location); }