package org.andengine.opengl.font;
import java.util.List;
import org.andengine.entity.text.AutoWrap;
import org.andengine.util.TextUtils;
import org.andengine.util.exception.MethodNotYetImplementedException;
/**
* (c) Zynga 2012
*
* @author Nicolas Gramlich <ngramlich@zynga.com>
* @since 15:06:32 - 25.01.2012
*/
public class FontUtils {
// ===========================================================
// Constants
// ===========================================================
private static final int UNSPECIFIED = -1;
// ===========================================================
// Fields
// ===========================================================
// ===========================================================
// Constructors
// ===========================================================
// ===========================================================
// Getter & Setter
// ===========================================================
// ===========================================================
// Methods for/from SuperClass/Interfaces
// ===========================================================
// ===========================================================
// Methods
// ===========================================================
/**
* @param pFont
* @param pText
* @return the width of pText.
*/
public static float measureText(final IFont pFont, final CharSequence pText) {
return FontUtils.measureText(pFont, pText, null);
}
/**
* @param pFont
* @param pText
* @param pStart the index of the first character to start measuring.
* @param pEnd <code>1</code> beyond the index of the last character to measure.
* @return the width of pText.
*/
public static float measureText(final IFont pFont, final CharSequence pText, final int pStart, final int pEnd) {
return FontUtils.measureText(pFont, pText, pStart, pEnd, null);
}
/**
* @param pFont
* @param pText
* @param pWidths (optional) If not <code>null</code>, returns the actual width measured.
* @return the width of pText.
*/
public static float measureText(final IFont pFont, final CharSequence pText, final float[] pWidths) {
return FontUtils.measureText(pFont, pText, 0, pText.length(), pWidths);
}
/**
* Does not respect linebreaks!
*
* @param pFont
* @param pText
* @param pStart the index of the first character to start measuring.
* @param pEnd <code>1</code> beyond the index of the last character to measure.
* @param pWidths (optional) If not <code>null</code>, returns the actual width after each character.
* @return the width of pText.
*/
public static float measureText(final IFont pFont, final CharSequence pText, final int pStart, final int pEnd, final float[] pWidths) {
final int textLength = pEnd - pStart;
/* Early exits. */
if(pStart == pEnd) {
return 0;
} else if(textLength == 1) {
return pFont.getLetter(pText.charAt(pStart)).mWidth;
}
Letter previousLetter = null;
float width = 0;
for(int pos = pStart, i = 0; pos < pEnd; pos++, i++) {
final Letter letter = pFont.getLetter(pText.charAt(pos));
if(previousLetter != null) {
width += previousLetter.getKerning(letter.mCharacter);
}
previousLetter = letter;
/* Check if this is the last character. */
if(pos == (pEnd - 1)) {
width += letter.mOffsetX + letter.mWidth;
} else {
width += letter.mAdvance;
}
if(pWidths != null) {
pWidths[i] = width;
}
}
return width;
}
/**
* Measure the text, stopping early if the measured width exceeds pMaximumWidth.
*
* @param pFont
* @param pText
* @param pMeasureDirection If {@link MeasureDirection#FORWARDS}, starts with the first character in the {@link CharSequence}. If {@link MeasureDirection#BACKWARDS} starts with the last character in the {@link CharSequence}.
* @param pWidthMaximum
* @param pMeasuredWidth (optional) If not <code>null</code>, returns the actual width measured. Must be an Array of size <code>1</code> or bigger.
* @return the number of chars that were measured.
*/
public static int breakText(final IFont pFont, final CharSequence pText, final MeasureDirection pMeasureDirection, final float pWidthMaximum, final float[] pMeasuredWidth) {
throw new MethodNotYetImplementedException();
}
public static <L extends List<CharSequence>> L splitLines(final CharSequence pText, final L pResult) {
return TextUtils.split(pText, '\n', pResult);
}
/**
* Does not respect linebreaks!
*
* @param pFont
* @param pText
* @param pResult
* @param pAutoWrapWidth
* @return
*/
public static <L extends List<CharSequence>> L splitLines(final IFont pFont, final CharSequence pText, final L pResult, final AutoWrap pAutoWrap, final float pAutoWrapWidth) {
/**
* TODO In order to respect already existing linebreaks, {@link FontUtils#split(CharSequence, List)} could be leveraged and than the following methods could be called for each line.
*/
switch(pAutoWrap) {
case LETTERS:
return FontUtils.splitLinesByLetters(pFont, pText, pResult, pAutoWrapWidth);
case WORDS:
return FontUtils.splitLinesByWords(pFont, pText, pResult, pAutoWrapWidth);
case CJK:
return FontUtils.splitLinesByCJK(pFont, pText, pResult, pAutoWrapWidth);
case NONE:
default:
throw new IllegalArgumentException("Unexpected " + AutoWrap.class.getSimpleName() + ": '" + pAutoWrap + "'.");
}
}
private static <L extends List<CharSequence>> L splitLinesByLetters(final IFont pFont, final CharSequence pText, final L pResult, final float pAutoWrapWidth) {
final int textLength = pText.length();
int lineStart = 0;
int lineEnd = 0;
int lastNonWhitespace = 0;
boolean charsAvailable = false;
for(int i = 0; i < textLength; i++) {
final char character = pText.charAt(i);
if(character != ' ') {
if(charsAvailable) {
lastNonWhitespace = i + 1;
} else {
charsAvailable = true;
lineStart = i;
lastNonWhitespace = lineStart + 1;
lineEnd = lastNonWhitespace;
}
}
if(charsAvailable) {
// /* Just for debugging. */
// final CharSequence line = pText.subSequence(lineStart, lineEnd);
// final float lineWidth = FontUtils.measureText(pFont, pText, lineStart, lineEnd);
//
// final CharSequence lookaheadLine = pText.subSequence(lineStart, lastNonWhitespace);
final float lookaheadLineWidth = FontUtils.measureText(pFont, pText, lineStart, lastNonWhitespace);
final boolean isEndReached = (i == (textLength - 1));
if(isEndReached) {
/* When the end of the string is reached, add remainder to result. */
if(lookaheadLineWidth <= pAutoWrapWidth) {
pResult.add(pText.subSequence(lineStart, lastNonWhitespace));
} else {
pResult.add(pText.subSequence(lineStart, lineEnd));
/* Avoid special case where last line is added twice. */
if(lineStart != i) {
pResult.add(pText.subSequence(i, lastNonWhitespace));
}
}
} else {
if(lookaheadLineWidth <= pAutoWrapWidth) {
lineEnd = lastNonWhitespace;
} else {
pResult.add(pText.subSequence(lineStart, lineEnd));
i = lineEnd - 1;
charsAvailable = false;
}
}
}
}
return pResult;
}
private static <L extends List<CharSequence>> L splitLinesByWords(final IFont pFont, final CharSequence pText, final L pResult, final float pAutoWrapWidth) {
final int textLength = pText.length();
if(textLength == 0) {
return pResult;
}
final float spaceWidth = pFont.getLetter(' ').mAdvance;
int lastWordEnd = FontUtils.UNSPECIFIED;
int lineStart = FontUtils.UNSPECIFIED;
int lineEnd = FontUtils.UNSPECIFIED;
float lineWidthRemaining = pAutoWrapWidth;
boolean firstWordInLine = true;
int i = 0;
while(i < textLength) {
int spacesSkipped = 0;
/* Find next word. */
{ /* Skip whitespaces. */
while((i < textLength) && (pText.charAt(i) == ' ')) {
i++;
spacesSkipped++;
}
}
final int wordStart = i;
/* Mark beginning of a new line. */
if(lineStart == FontUtils.UNSPECIFIED) {
lineStart = wordStart;
}
{ /* Skip non-whitespaces. */
while((i < textLength) && (pText.charAt(i) != ' ')) {
i++;
}
}
final int wordEnd = i;
/* Nothing more could be read. */
if(wordStart == wordEnd) {
if(!firstWordInLine) {
pResult.add(pText.subSequence(lineStart, lineEnd));
}
break;
}
// /* Just for debugging. */
// final CharSequence word = pText.subSequence(wordStart, wordEnd);
final float wordWidth = FontUtils.measureText(pFont, pText, wordStart, wordEnd);
/* Determine the width actually needed for the current word. */
final float widthNeeded;
if(firstWordInLine) {
widthNeeded = wordWidth;
} else {
widthNeeded = (spacesSkipped * spaceWidth) + wordWidth;
}
/* Check if the word fits into the rest of the line. */
if (widthNeeded <= lineWidthRemaining) {
if(firstWordInLine) {
firstWordInLine = false;
} else {
lineWidthRemaining -= FontUtils.getAdvanceCorrection(pFont, pText, lastWordEnd - 1);
}
lineWidthRemaining -= widthNeeded;
lastWordEnd = wordEnd;
lineEnd = wordEnd;
/* Check if the end was reached. */
if(wordEnd == textLength) {
pResult.add(pText.subSequence(lineStart, lineEnd));
/* Added the last line. */
break;
}
} else {
/* Special case for lines with only one word. */
if(firstWordInLine) {
/* Check for lines that are just too big. */
if(wordWidth >= pAutoWrapWidth) {
pResult.add(pText.subSequence(wordStart, wordEnd));
lineWidthRemaining = pAutoWrapWidth;
} else {
lineWidthRemaining = pAutoWrapWidth - wordWidth;
/* Check if the end was reached. */
if(wordEnd == textLength) {
pResult.add(pText.subSequence(wordStart, wordEnd));
/* Added the last line. */
break;
}
}
/* Start a completely new line. */
firstWordInLine = true;
lastWordEnd = FontUtils.UNSPECIFIED;
lineStart = FontUtils.UNSPECIFIED;
lineEnd = FontUtils.UNSPECIFIED;
} else {
/* Finish the current line. */
pResult.add(pText.subSequence(lineStart, lineEnd));
/* Check if the end was reached. */
if(wordEnd == textLength) {
/* Add the last word. */
pResult.add(pText.subSequence(wordStart, wordEnd)); // TODO Does this cover all cases?
break;
} else {
/* Start a new line, carrying over the current word. */
lineWidthRemaining = pAutoWrapWidth - wordWidth;
firstWordInLine = false;
lastWordEnd = wordEnd;
lineStart = wordStart;
lineEnd = wordEnd;
}
}
}
}
return pResult;
}
private static <L extends List<CharSequence>> L splitLinesByCJK(final IFont pFont, final CharSequence pText, final L pResult, final float pAutoWrapWidth) {
final int textLength = pText.length();
int lineStart = 0;
int lineEnd = 0;
/* Skip whitespaces at the beginning of the string. */
while((lineStart < textLength) && (pText.charAt(lineStart) == ' ')) {
lineStart++;
lineEnd++;
}
int i = lineEnd;
while(i < textLength) {
lineStart = lineEnd;
{ /* Look for a sub string */
boolean charsAvailable = true;
while(i < textLength) {
{ /* Skip whitespaces at the end of the string */
int j = lineEnd;
while ( j < textLength ) {
if ( pText.charAt( j ) == ' ' ) {
j++;
}
else {
break;
}
}
if ( j == textLength ) {
if ( lineStart == lineEnd ) {
charsAvailable = false;
}
i = textLength;
break;
}
}
lineEnd++;
final float lineWidth = FontUtils.measureText(pFont, pText, lineStart, lineEnd);
if(lineWidth > pAutoWrapWidth) {
if ( lineStart < lineEnd - 1 ) {
lineEnd--;
}
pResult.add(pText.subSequence(lineStart, lineEnd));
charsAvailable = false;
i = lineEnd;
break;
}
i = lineEnd;
}
if(charsAvailable) {
pResult.add(pText.subSequence(lineStart, lineEnd));
}
}
}
return pResult;
}
private static float getAdvanceCorrection(final IFont pFont, final CharSequence pText, final int pIndex) {
final Letter lastWordLastLetter = pFont.getLetter(pText.charAt(pIndex));
return -(lastWordLastLetter.mOffsetX + lastWordLastLetter.mWidth) + lastWordLastLetter.mAdvance;
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
public enum MeasureDirection {
// ===========================================================
// Elements
// ===========================================================
FORWARDS,
BACKWARDS;
// ===========================================================
// Constants
// ===========================================================
// ===========================================================
// Fields
// ===========================================================
// ===========================================================
// Constructors
// ===========================================================
// ===========================================================
// Getter & Setter
// ===========================================================
// ===========================================================
// Methods for/from SuperClass/Interfaces
// ===========================================================
// ===========================================================
// Methods
// ===========================================================
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
}
}