/******************************************************************************* * Copyright 2011 See AUTHORS file. * * 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.badlogic.gdx.graphics.g2d; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.Texture.TextureFilter; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.utils.GdxRuntimeException; /** A 3x3 grid of texture regions. Any of the regions may be omitted. Padding may be set as a hint on how to inset content on top * of the ninepatch (by default the eight "edge" textures of the nine-patch define the padding). When drawn the eight "edge" * patches will not be scaled, only the interior patch will be scaled. * * <p> * <b>NOTE</b>: This class expects a "post-processed" nine-patch, and not a raw ".9.png" texture. That is, the textures given to * this class should <em>not</em> include the meta-data pixels from a ".9.png" that describe the layout of the ninepatch over the * interior of the graphic. That information should be passed into the constructor either implicitly as the size of the individual * patch textures, or via the <code>left, right, top, bottom</code> parameters to {@link #NinePatch(Texture, int, int, int, int)} * or {@link #NinePatch(TextureRegion, int, int, int, int)}. * * <p> * A correctly created {@link TextureAtlas} is one way to generate a post-processed nine-patch from a ".9.png" file. */ public class NinePatch { public static final int TOP_LEFT = 0; public static final int TOP_CENTER = 1; public static final int TOP_RIGHT = 2; public static final int MIDDLE_LEFT = 3; public static final int MIDDLE_CENTER = 4; public static final int MIDDLE_RIGHT = 5; public static final int BOTTOM_LEFT = 6; /** Indices for {@link #NinePatch(TextureRegion...)} constructor */ // alphabetically first in javadoc public static final int BOTTOM_CENTER = 7; public static final int BOTTOM_RIGHT = 8; static private final Color tmpDrawColor = new Color(); private Texture texture; private int bottomLeft = -1, bottomCenter = -1, bottomRight = -1; private int middleLeft = -1, middleCenter = -1, middleRight = -1; private int topLeft = -1, topCenter = -1, topRight = -1; private float leftWidth, rightWidth, middleWidth, middleHeight, topHeight, bottomHeight; private float[] vertices = new float[9 * 4 * 5]; private int idx; private final Color color = new Color(Color.WHITE); private float padLeft = -1, padRight = -1, padTop = -1, padBottom = -1; /** Create a ninepatch by cutting up the given texture into nine patches. The subsequent parameters define the 4 lines that * will cut the texture region into 9 pieces. * * @param left Pixels from left edge. * @param right Pixels from right edge. * @param top Pixels from top edge. * @param bottom Pixels from bottom edge. */ public NinePatch (Texture texture, int left, int right, int top, int bottom) { this(new TextureRegion(texture), left, right, top, bottom); } /** Create a ninepatch by cutting up the given texture region into nine patches. The subsequent parameters define the 4 lines * that will cut the texture region into 9 pieces. * * @param left Pixels from left edge. * @param right Pixels from right edge. * @param top Pixels from top edge. * @param bottom Pixels from bottom edge. */ public NinePatch (TextureRegion region, int left, int right, int top, int bottom) { if (region == null) throw new IllegalArgumentException("region cannot be null."); final int middleWidth = region.getRegionWidth() - left - right; final int middleHeight = region.getRegionHeight() - top - bottom; TextureRegion[] patches = new TextureRegion[9]; if (top > 0) { if (left > 0) patches[TOP_LEFT] = new TextureRegion(region, 0, 0, left, top); if (middleWidth > 0) patches[TOP_CENTER] = new TextureRegion(region, left, 0, middleWidth, top); if (right > 0) patches[TOP_RIGHT] = new TextureRegion(region, left + middleWidth, 0, right, top); } if (middleHeight > 0) { if (left > 0) patches[MIDDLE_LEFT] = new TextureRegion(region, 0, top, left, middleHeight); if (middleWidth > 0) patches[MIDDLE_CENTER] = new TextureRegion(region, left, top, middleWidth, middleHeight); if (right > 0) patches[MIDDLE_RIGHT] = new TextureRegion(region, left + middleWidth, top, right, middleHeight); } if (bottom > 0) { if (left > 0) patches[BOTTOM_LEFT] = new TextureRegion(region, 0, top + middleHeight, left, bottom); if (middleWidth > 0) patches[BOTTOM_CENTER] = new TextureRegion(region, left, top + middleHeight, middleWidth, bottom); if (right > 0) patches[BOTTOM_RIGHT] = new TextureRegion(region, left + middleWidth, top + middleHeight, right, bottom); } // If split only vertical, move splits from right to center. if (left == 0 && middleWidth == 0) { patches[TOP_CENTER] = patches[TOP_RIGHT]; patches[MIDDLE_CENTER] = patches[MIDDLE_RIGHT]; patches[BOTTOM_CENTER] = patches[BOTTOM_RIGHT]; patches[TOP_RIGHT] = null; patches[MIDDLE_RIGHT] = null; patches[BOTTOM_RIGHT] = null; } // If split only horizontal, move splits from bottom to center. if (top == 0 && middleHeight == 0) { patches[MIDDLE_LEFT] = patches[BOTTOM_LEFT]; patches[MIDDLE_CENTER] = patches[BOTTOM_CENTER]; patches[MIDDLE_RIGHT] = patches[BOTTOM_RIGHT]; patches[BOTTOM_LEFT] = null; patches[BOTTOM_CENTER] = null; patches[BOTTOM_RIGHT] = null; } load(patches); } /** Construct a degenerate "nine" patch with only a center component. */ public NinePatch (Texture texture, Color color) { this(texture); setColor(color); } /** Construct a degenerate "nine" patch with only a center component. */ public NinePatch (Texture texture) { this(new TextureRegion(texture)); } /** Construct a degenerate "nine" patch with only a center component. */ public NinePatch (TextureRegion region, Color color) { this(region); setColor(color); } /** Construct a degenerate "nine" patch with only a center component. */ public NinePatch (TextureRegion region) { load(new TextureRegion[] { // null, null, null, // null, region, null, // null, null, null // }); } /** Construct a nine patch from the given nine texture regions. The provided patches must be consistently sized (e.g., any left * edge textures must have the same width, etc). Patches may be <code>null</code>. Patch indices are specified via the public * members {@link #TOP_LEFT}, {@link #TOP_CENTER}, etc. */ public NinePatch (TextureRegion... patches) { if (patches == null || patches.length != 9) throw new IllegalArgumentException("NinePatch needs nine TextureRegions"); load(patches); float leftWidth = getLeftWidth(); if ((patches[TOP_LEFT] != null && patches[TOP_LEFT].getRegionWidth() != leftWidth) || (patches[MIDDLE_LEFT] != null && patches[MIDDLE_LEFT].getRegionWidth() != leftWidth) || (patches[BOTTOM_LEFT] != null && patches[BOTTOM_LEFT].getRegionWidth() != leftWidth)) { throw new GdxRuntimeException("Left side patches must have the same width"); } float rightWidth = getRightWidth(); if ((patches[TOP_RIGHT] != null && patches[TOP_RIGHT].getRegionWidth() != rightWidth) || (patches[MIDDLE_RIGHT] != null && patches[MIDDLE_RIGHT].getRegionWidth() != rightWidth) || (patches[BOTTOM_RIGHT] != null && patches[BOTTOM_RIGHT].getRegionWidth() != rightWidth)) { throw new GdxRuntimeException("Right side patches must have the same width"); } float bottomHeight = getBottomHeight(); if ((patches[BOTTOM_LEFT] != null && patches[BOTTOM_LEFT].getRegionHeight() != bottomHeight) || (patches[BOTTOM_CENTER] != null && patches[BOTTOM_CENTER].getRegionHeight() != bottomHeight) || (patches[BOTTOM_RIGHT] != null && patches[BOTTOM_RIGHT].getRegionHeight() != bottomHeight)) { throw new GdxRuntimeException("Bottom side patches must have the same height"); } float topHeight = getTopHeight(); if ((patches[TOP_LEFT] != null && patches[TOP_LEFT].getRegionHeight() != topHeight) || (patches[TOP_CENTER] != null && patches[TOP_CENTER].getRegionHeight() != topHeight) || (patches[TOP_RIGHT] != null && patches[TOP_RIGHT].getRegionHeight() != topHeight)) { throw new GdxRuntimeException("Top side patches must have the same height"); } } public NinePatch (NinePatch ninePatch) { this(ninePatch, ninePatch.color); } public NinePatch (NinePatch ninePatch, Color color) { texture = ninePatch.texture; bottomLeft = ninePatch.bottomLeft; bottomCenter = ninePatch.bottomCenter; bottomRight = ninePatch.bottomRight; middleLeft = ninePatch.middleLeft; middleCenter = ninePatch.middleCenter; middleRight = ninePatch.middleRight; topLeft = ninePatch.topLeft; topCenter = ninePatch.topCenter; topRight = ninePatch.topRight; leftWidth = ninePatch.leftWidth; rightWidth = ninePatch.rightWidth; middleWidth = ninePatch.middleWidth; middleHeight = ninePatch.middleHeight; topHeight = ninePatch.topHeight; bottomHeight = ninePatch.bottomHeight; padLeft = ninePatch.padLeft; padTop = ninePatch.padTop; padBottom = ninePatch.padBottom; padRight = ninePatch.padRight; vertices = new float[ninePatch.vertices.length]; System.arraycopy(ninePatch.vertices, 0, vertices, 0, ninePatch.vertices.length); idx = ninePatch.idx; this.color.set(color); } private void load (TextureRegion[] patches) { final float color = Color.WHITE.toFloatBits(); // placeholder color, overwritten at draw time if (patches[BOTTOM_LEFT] != null) { bottomLeft = add(patches[BOTTOM_LEFT], color, false, false); leftWidth = patches[BOTTOM_LEFT].getRegionWidth(); bottomHeight = patches[BOTTOM_LEFT].getRegionHeight(); } if (patches[BOTTOM_CENTER] != null) { bottomCenter = add(patches[BOTTOM_CENTER], color, true, false); middleWidth = Math.max(middleWidth, patches[BOTTOM_CENTER].getRegionWidth()); bottomHeight = Math.max(bottomHeight, patches[BOTTOM_CENTER].getRegionHeight()); } if (patches[BOTTOM_RIGHT] != null) { bottomRight = add(patches[BOTTOM_RIGHT], color, false, false); rightWidth = Math.max(rightWidth, patches[BOTTOM_RIGHT].getRegionWidth()); bottomHeight = Math.max(bottomHeight, patches[BOTTOM_RIGHT].getRegionHeight()); } if (patches[MIDDLE_LEFT] != null) { middleLeft = add(patches[MIDDLE_LEFT], color, false, true); leftWidth = Math.max(leftWidth, patches[MIDDLE_LEFT].getRegionWidth()); middleHeight = Math.max(middleHeight, patches[MIDDLE_LEFT].getRegionHeight()); } if (patches[MIDDLE_CENTER] != null) { middleCenter = add(patches[MIDDLE_CENTER], color, true, true); middleWidth = Math.max(middleWidth, patches[MIDDLE_CENTER].getRegionWidth()); middleHeight = Math.max(middleHeight, patches[MIDDLE_CENTER].getRegionHeight()); } if (patches[MIDDLE_RIGHT] != null) { middleRight = add(patches[MIDDLE_RIGHT], color, false, true); rightWidth = Math.max(rightWidth, patches[MIDDLE_RIGHT].getRegionWidth()); middleHeight = Math.max(middleHeight, patches[MIDDLE_RIGHT].getRegionHeight()); } if (patches[TOP_LEFT] != null) { topLeft = add(patches[TOP_LEFT], color, false, false); leftWidth = Math.max(leftWidth, patches[TOP_LEFT].getRegionWidth()); topHeight = Math.max(topHeight, patches[TOP_LEFT].getRegionHeight()); } if (patches[TOP_CENTER] != null) { topCenter = add(patches[TOP_CENTER], color, true, false); middleWidth = Math.max(middleWidth, patches[TOP_CENTER].getRegionWidth()); topHeight = Math.max(topHeight, patches[TOP_CENTER].getRegionHeight()); } if (patches[TOP_RIGHT] != null) { topRight = add(patches[TOP_RIGHT], color, false, false); rightWidth = Math.max(rightWidth, patches[TOP_RIGHT].getRegionWidth()); topHeight = Math.max(topHeight, patches[TOP_RIGHT].getRegionHeight()); } if (idx < vertices.length) { float[] newVertices = new float[idx]; System.arraycopy(vertices, 0, newVertices, 0, idx); vertices = newVertices; } } private int add (TextureRegion region, float color, boolean isStretchW, boolean isStretchH) { if (texture == null) texture = region.getTexture(); else if (texture != region.getTexture()) // throw new IllegalArgumentException("All regions must be from the same texture."); float u = region.u; float v = region.v2; float u2 = region.u2; float v2 = region.v; // Add half pixel offsets on stretchable dimensions to avoid color bleeding when GL_LINEAR // filtering is used for the texture. This nudges the texture coordinate to the center // of the texel where the neighboring pixel has 0% contribution in linear blending mode. if (texture.getMagFilter() == TextureFilter.Linear || texture.getMinFilter() == TextureFilter.Linear) { if (isStretchW) { float halfTexelWidth = 0.5f * 1.0f / texture.getWidth(); u += halfTexelWidth; u2 -= halfTexelWidth; } if (isStretchH) { float halfTexelHeight = 0.5f * 1.0f / texture.getHeight(); v -= halfTexelHeight; v2 += halfTexelHeight; } } final float[] vertices = this.vertices; vertices[idx + 2] = color; vertices[idx + 3] = u; vertices[idx + 4] = v; vertices[idx + 7] = color; vertices[idx + 8] = u; vertices[idx + 9] = v2; vertices[idx + 12] = color; vertices[idx + 13] = u2; vertices[idx + 14] = v2; vertices[idx + 17] = color; vertices[idx + 18] = u2; vertices[idx + 19] = v; idx += 20; return idx - 20; } /** Set the coordinates and color of a ninth of the patch. */ private void set (int idx, float x, float y, float width, float height, float color) { final float fx2 = x + width; final float fy2 = y + height; final float[] vertices = this.vertices; vertices[idx] = x; vertices[idx + 1] = y; vertices[idx + 2] = color; vertices[idx + 5] = x; vertices[idx + 6] = fy2; vertices[idx + 7] = color; vertices[idx + 10] = fx2; vertices[idx + 11] = fy2; vertices[idx + 12] = color; vertices[idx + 15] = fx2; vertices[idx + 16] = y; vertices[idx + 17] = color; } private void prepareVertices (Batch batch, float x, float y, float width, float height) { final float centerColumnX = x + leftWidth; final float rightColumnX = x + width - rightWidth; final float middleRowY = y + bottomHeight; final float topRowY = y + height - topHeight; final float c = tmpDrawColor.set(color).mul(batch.getColor()).toFloatBits(); if (bottomLeft != -1) set(bottomLeft, x, y, centerColumnX - x, middleRowY - y, c); if (bottomCenter != -1) set(bottomCenter, centerColumnX, y, rightColumnX - centerColumnX, middleRowY - y, c); if (bottomRight != -1) set(bottomRight, rightColumnX, y, x + width - rightColumnX, middleRowY - y, c); if (middleLeft != -1) set(middleLeft, x, middleRowY, centerColumnX - x, topRowY - middleRowY, c); if (middleCenter != -1) set(middleCenter, centerColumnX, middleRowY, rightColumnX - centerColumnX, topRowY - middleRowY, c); if (middleRight != -1) set(middleRight, rightColumnX, middleRowY, x + width - rightColumnX, topRowY - middleRowY, c); if (topLeft != -1) set(topLeft, x, topRowY, centerColumnX - x, y + height - topRowY, c); if (topCenter != -1) set(topCenter, centerColumnX, topRowY, rightColumnX - centerColumnX, y + height - topRowY, c); if (topRight != -1) set(topRight, rightColumnX, topRowY, x + width - rightColumnX, y + height - topRowY, c); } public void draw (Batch batch, float x, float y, float width, float height) { prepareVertices(batch, x, y, width, height); batch.draw(texture, vertices, 0, idx); } public void draw (Batch batch, float x, float y, float originX, float originY, float width, float height, float scaleX, float scaleY, float rotation) { prepareVertices(batch, x, y, width, height); float worldOriginX = x + originX, worldOriginY = y + originY; int n = this.idx; float[] vertices = this.vertices; if (rotation != 0) { for (int i = 0; i < n; i += 5) { float vx = (vertices[i] - worldOriginX) * scaleX, vy = (vertices[i + 1] - worldOriginY) * scaleY; float cos = MathUtils.cosDeg(rotation), sin = MathUtils.sinDeg(rotation); vertices[i] = cos * vx - sin * vy + worldOriginX; vertices[i + 1] = sin * vx + cos * vy + worldOriginY; } } else if (scaleX != 1 || scaleY != 1) { for (int i = 0; i < n; i += 5) { vertices[i] = (vertices[i] - worldOriginX) * scaleX + worldOriginX; vertices[i + 1] = (vertices[i + 1] - worldOriginY) * scaleY + worldOriginY; } } batch.draw(texture, vertices, 0, n); } /** Copy given color. The color will be blended with the batch color, then combined with the texture colors at * {@link NinePatch#draw(Batch, float, float, float, float) draw} time. Default is {@link Color#WHITE}. */ public void setColor (Color color) { this.color.set(color); } public Color getColor () { return color; } public float getLeftWidth () { return leftWidth; } /** Set the draw-time width of the three left edge patches */ public void setLeftWidth (float leftWidth) { this.leftWidth = leftWidth; } public float getRightWidth () { return rightWidth; } /** Set the draw-time width of the three right edge patches */ public void setRightWidth (float rightWidth) { this.rightWidth = rightWidth; } public float getTopHeight () { return topHeight; } /** Set the draw-time height of the three top edge patches */ public void setTopHeight (float topHeight) { this.topHeight = topHeight; } public float getBottomHeight () { return bottomHeight; } /** Set the draw-time height of the three bottom edge patches */ public void setBottomHeight (float bottomHeight) { this.bottomHeight = bottomHeight; } public float getMiddleWidth () { return middleWidth; } /** Set the width of the middle column of the patch. At render time, this is implicitly the requested render-width of the * entire nine patch, minus the left and right width. This value is only used for computing the {@link #getTotalWidth() default * total width}. */ public void setMiddleWidth (float middleWidth) { this.middleWidth = middleWidth; } public float getMiddleHeight () { return middleHeight; } /** Set the height of the middle row of the patch. At render time, this is implicitly the requested render-height of the entire * nine patch, minus the top and bottom height. This value is only used for computing the {@link #getTotalHeight() default * total height}. */ public void setMiddleHeight (float middleHeight) { this.middleHeight = middleHeight; } public float getTotalWidth () { return leftWidth + middleWidth + rightWidth; } public float getTotalHeight () { return topHeight + middleHeight + bottomHeight; } /** Set the padding for content inside this ninepatch. By default the padding is set to match the exterior of the ninepatch, so * the content should fit exactly within the middle patch. */ public void setPadding (float left, float right, float top, float bottom) { this.padLeft = left; this.padRight = right; this.padTop = top; this.padBottom = bottom; } /** Returns the left padding if set, else returns {@link #getLeftWidth()}. */ public float getPadLeft () { if (padLeft == -1) return getLeftWidth(); return padLeft; } /** See {@link #setPadding(float, float, float, float)} */ public void setPadLeft (float left) { this.padLeft = left; } /** Returns the right padding if set, else returns {@link #getRightWidth()}. */ public float getPadRight () { if (padRight == -1) return getRightWidth(); return padRight; } /** See {@link #setPadding(float, float, float, float)} */ public void setPadRight (float right) { this.padRight = right; } /** Returns the top padding if set, else returns {@link #getTopHeight()}. */ public float getPadTop () { if (padTop == -1) return getTopHeight(); return padTop; } /** See {@link #setPadding(float, float, float, float)} */ public void setPadTop (float top) { this.padTop = top; } /** Returns the bottom padding if set, else returns {@link #getBottomHeight()}. */ public float getPadBottom () { if (padBottom == -1) return getBottomHeight(); return padBottom; } /** See {@link #setPadding(float, float, float, float)} */ public void setPadBottom (float bottom) { this.padBottom = bottom; } /** Multiplies the top/left/bottom/right sizes and padding by the specified amount. */ public void scale (float scaleX, float scaleY) { leftWidth *= scaleX; rightWidth *= scaleX; topHeight *= scaleY; bottomHeight *= scaleY; middleWidth *= scaleX; middleHeight *= scaleY; if (padLeft != -1) padLeft *= scaleX; if (padRight != -1) padRight *= scaleX; if (padTop != -1) padTop *= scaleY; if (padBottom != -1) padBottom *= scaleY; } public Texture getTexture () { return texture; } }