package org.andengine.opengl.font; import java.util.ArrayList; import org.andengine.opengl.font.exception.FontException; import org.andengine.opengl.texture.ITexture; import org.andengine.opengl.texture.PixelFormat; import org.andengine.opengl.util.GLState; import org.andengine.util.adt.map.SparseArrayUtils; import org.andengine.util.color.Color; import org.andengine.util.math.MathUtils; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.FontMetrics; import android.graphics.Paint.Style; import android.graphics.Rect; import android.graphics.Typeface; import android.opengl.GLES20; import android.opengl.GLUtils; import android.util.SparseArray; /** * (c) 2010 Nicolas Gramlich * (c) 2011 Zynga Inc. * * @author Nicolas Gramlich * @since 10:39:33 - 03.04.2010 */ public class Font implements IFont { // =========================================================== // Constants // =========================================================== protected static final int LETTER_TEXTURE_PADDING = 1; // =========================================================== // Fields // =========================================================== private final FontManager mFontManager; private final ITexture mTexture; private final int mTextureWidth; private final int mTextureHeight; private int mCurrentTextureX = Font.LETTER_TEXTURE_PADDING; private int mCurrentTextureY = Font.LETTER_TEXTURE_PADDING; private int mCurrentTextureYHeightMax; private final SparseArray<Letter> mManagedCharacterToLetterMap = new SparseArray<Letter>(); private final ArrayList<Letter> mLettersPendingToBeDrawnToTexture = new ArrayList<Letter>(); protected final Paint mPaint; private final Paint mBackgroundPaint; protected final FontMetrics mFontMetrics; protected final Canvas mCanvas = new Canvas(); protected final Rect mTextBounds = new Rect(); protected final float[] mTextWidthContainer = new float[1]; // =========================================================== // Constructors // =========================================================== public Font(final FontManager pFontManager, final ITexture pTexture, final Typeface pTypeface, final float pSize, final boolean pAntiAlias, final Color pColor) { this(pFontManager, pTexture, pTypeface, pSize, pAntiAlias, pColor.getARGBPackedInt()); } public Font(final FontManager pFontManager, final ITexture pTexture, final Typeface pTypeface, final float pSize, final boolean pAntiAlias, final int pColorARGBPackedInt) { this.mFontManager = pFontManager; this.mTexture = pTexture; this.mTextureWidth = pTexture.getWidth(); this.mTextureHeight = pTexture.getHeight(); this.mBackgroundPaint = new Paint(); this.mBackgroundPaint.setColor(Color.TRANSPARENT_ARGB_PACKED_INT); this.mBackgroundPaint.setStyle(Style.FILL); this.mPaint = new Paint(); this.mPaint.setTypeface(pTypeface); this.mPaint.setColor(pColorARGBPackedInt); this.mPaint.setTextSize(pSize); this.mPaint.setAntiAlias(pAntiAlias); this.mFontMetrics = this.mPaint.getFontMetrics(); } // =========================================================== // Getter & Setter // =========================================================== /** * @return the gap between the lines. */ public float getLeading() { return this.mFontMetrics.leading; } /** * @return the distance from the baseline to the top, which is usually negative. */ public float getAscent() { return this.mFontMetrics.ascent; } /** * @return the distance from the baseline to the bottom, which is usually positive. */ public float getDescent() { return this.mFontMetrics.descent; } // =========================================================== // Methods for/from SuperClass/Interfaces // =========================================================== @Override public ITexture getTexture() { return this.mTexture; } @Override public void load() { this.mTexture.load(); this.mFontManager.loadFont(this); } @Override public void unload() { this.mTexture.unload(); this.mFontManager.unloadFont(this); } @Override public float getLineHeight() { return -this.getAscent() + this.getDescent(); } @Override public synchronized Letter getLetter(final char pCharacter) throws FontException { Letter letter = this.mManagedCharacterToLetterMap.get(pCharacter); if(letter == null) { letter = this.createLetter(pCharacter); this.mLettersPendingToBeDrawnToTexture.add(letter); this.mManagedCharacterToLetterMap.put(pCharacter, letter); } return letter; } // =========================================================== // Methods // =========================================================== public synchronized void invalidateLetters() { final ArrayList<Letter> lettersPendingToBeDrawnToTexture = this.mLettersPendingToBeDrawnToTexture; final SparseArray<Letter> managedCharacterToLetterMap = this.mManagedCharacterToLetterMap; /* Make all letters redraw to the texture. */ for(int i = managedCharacterToLetterMap.size() - 1; i >= 0; i--) { lettersPendingToBeDrawnToTexture.add(managedCharacterToLetterMap.valueAt(i)); } } private float getLetterAdvance(final String pCharacterAsString) { this.mPaint.getTextWidths(pCharacterAsString, this.mTextWidthContainer); return this.mTextWidthContainer[0]; } protected Bitmap getLetterBitmap(final Letter pLetter) throws FontException { final char character = pLetter.mCharacter; final String characterAsString = String.valueOf(character); final int width = pLetter.mWidth + (2 * Font.LETTER_TEXTURE_PADDING); final int height = pLetter.mHeight + (2 * Font.LETTER_TEXTURE_PADDING); final Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); this.mCanvas.setBitmap(bitmap); /* Make background transparent. */ this.mCanvas.drawRect(0, 0, bitmap.getWidth(), bitmap.getHeight(), this.mBackgroundPaint); /* Actually draw the character. */ final float drawLetterLeft = -pLetter.mOffsetX; final float drawLetterTop = -(pLetter.mOffsetY + this.getAscent()); this.drawLetter(characterAsString, drawLetterLeft, drawLetterTop); return bitmap; } protected void drawLetter(final String pCharacterAsString, final float pLeft, final float pTop) { this.mCanvas.drawText(pCharacterAsString, pLeft + Font.LETTER_TEXTURE_PADDING, pTop + Font.LETTER_TEXTURE_PADDING, this.mPaint); } public void prepareLetters(final char... pCharacters) throws FontException { for(final char character : pCharacters) { this.getLetter(character); } } private Letter createLetter(final char pCharacter) throws FontException { final String characterAsString = String.valueOf(pCharacter); final float textureWidth = this.mTextureWidth; final float textureHeight = this.mTextureHeight; this.updateTextBounds(characterAsString); final int letterLeft = this.mTextBounds.left; final int letterTop = this.mTextBounds.top; final int letterWidth = this.mTextBounds.width(); final int letterHeight = this.mTextBounds.height(); final Letter letter; final float advance = this.getLetterAdvance(characterAsString); final boolean whitespace = Character.isWhitespace(pCharacter) || (letterWidth == 0) || (letterHeight == 0); if(whitespace) { letter = new Letter(pCharacter, advance); } else { if((this.mCurrentTextureX + Font.LETTER_TEXTURE_PADDING + letterWidth) >= textureWidth) { this.mCurrentTextureX = 0; this.mCurrentTextureY += this.mCurrentTextureYHeightMax + (2 * Font.LETTER_TEXTURE_PADDING); this.mCurrentTextureYHeightMax = 0; } if((this.mCurrentTextureY + letterHeight) >= textureHeight) { throw new FontException("Not enough space for " + Letter.class.getSimpleName() + ": '" + pCharacter + "' on the " + this.mTexture.getClass().getSimpleName() + ". Existing Letters: " + SparseArrayUtils.toString(this.mManagedCharacterToLetterMap)); } this.mCurrentTextureYHeightMax = Math.max(letterHeight, this.mCurrentTextureYHeightMax); this.mCurrentTextureX += Font.LETTER_TEXTURE_PADDING; final float u = this.mCurrentTextureX / textureWidth; final float v = this.mCurrentTextureY / textureHeight; final float u2 = (this.mCurrentTextureX + letterWidth) / textureWidth; final float v2 = (this.mCurrentTextureY + letterHeight) / textureHeight; letter = new Letter(pCharacter, this.mCurrentTextureX - Font.LETTER_TEXTURE_PADDING, this.mCurrentTextureY - Font.LETTER_TEXTURE_PADDING, letterWidth, letterHeight, letterLeft, letterTop - this.getAscent(), advance, u, v, u2, v2); this.mCurrentTextureX += letterWidth + Font.LETTER_TEXTURE_PADDING; } return letter; } protected void updateTextBounds(final String pCharacterAsString) { this.mPaint.getTextBounds(pCharacterAsString, 0, 1, this.mTextBounds); } public synchronized void update(final GLState pGLState) { if(this.mTexture.isLoadedToHardware()) { final ArrayList<Letter> lettersPendingToBeDrawnToTexture = this.mLettersPendingToBeDrawnToTexture; if(lettersPendingToBeDrawnToTexture.size() > 0) { this.mTexture.bind(pGLState); final PixelFormat pixelFormat = this.mTexture.getPixelFormat(); final boolean preMultipyAlpha = this.mTexture.getTextureOptions().mPreMultiplyAlpha; for(int i = lettersPendingToBeDrawnToTexture.size() - 1; i >= 0; i--) { final Letter letter = lettersPendingToBeDrawnToTexture.get(i); if(!letter.isWhitespace()) { final Bitmap bitmap = this.getLetterBitmap(letter); final boolean useDefaultAlignment = MathUtils.isPowerOfTwo(bitmap.getWidth()) && MathUtils.isPowerOfTwo(bitmap.getHeight()) && (pixelFormat == PixelFormat.RGBA_8888); if(!useDefaultAlignment) { /* Adjust unpack alignment. */ GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1); } if(preMultipyAlpha) { GLUtils.texSubImage2D(GLES20.GL_TEXTURE_2D, 0, letter.mTextureX, letter.mTextureY, bitmap); } else { pGLState.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, letter.mTextureX, letter.mTextureY, bitmap, pixelFormat); } if(!useDefaultAlignment) { /* Restore default unpack alignment. */ GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, GLState.GL_UNPACK_ALIGNMENT_DEFAULT); } bitmap.recycle(); } } lettersPendingToBeDrawnToTexture.clear(); System.gc(); } } } // =========================================================== // Inner and Anonymous Classes // =========================================================== }