/******************************************************************************* * Copyright (c) 2015 - 2017 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *******************************************************************************/ package go.graphics.android; import java.nio.ByteBuffer; import java.util.Arrays; import go.graphics.TextureHandle; import go.graphics.text.EFontSize; import go.graphics.text.TextDrawer; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.TypedValue; import android.widget.TextView; public class AndroidTextDrawer implements TextDrawer { private static final int TEXTURE_HEIGHT = 512; private static final int TEXTURE_WIDTH = 512; private static AndroidTextDrawer[] instances = new AndroidTextDrawer[EFontSize.values().length]; private final EFontSize size; private final AndroidContext context; private TextureHandle texture = null; /** * The number of lines we use on our texture. */ private int lines; /** * The current string starting in line i. * <p> */ private String[] linestrings; /** * The width of line i. This width can be higher than TEXTURE_WIDTH. Then the string is split to multiple lines. */ private int[] linewidths; /** * An index of the next tile if the width of the current line is bigger than TEXTURE_WIDTH. This forms an linked list. -1 means no next tile. */ private int[] nextTile; private int lineheight; /** * Data to do LRU */ private int lastUsedCount = 0; private int[] lastused; private TextView renderer; private float pixelScale; private float[] texturepos = { // top left 0, 0, 0, 0, 0, // bottom left 0, 0, 0, 0, 0, // bottom right TEXTURE_WIDTH, 0, 0, 1, 0, // top right TEXTURE_WIDTH, 0, 0, 1, 0, }; private AndroidTextDrawer(EFontSize size, AndroidContext context) { this.size = size; this.context = context; pixelScale = context.getAndroidContext().getResources().getDisplayMetrics().scaledDensity; } private void checkInvariants() { boolean[] isNextTile = new boolean[lines]; for (int i = 0; i < lines; i++) { int next = nextTile[i]; if (next >= 0) { if (isNextTile[next]) { System.err.println("WARNING: The line " + next + " is linked multiple times as next line."); } isNextTile[next] = true; } } for (int i = 0; i < lines; i++) { if (isNextTile[i]) { if (linestrings[i] != null) { System.out.println("Linestring should be null for line " + i); } if (lastused[i] != Integer.MAX_VALUE) { System.out.println("Last used should not be set for line " + i); } } } } @Override public void renderCentered(float cx, float cy, String text) { // TODO: we may want to optimize this. drawString(cx - (float) getWidth(text) / 2, cy - (float) getHeight(text) / 2, text); } @Override public void drawString(float x, float y, String string) { initialize(); int line = findLineFor(string); for (; line >= 0; line = nextTile[line], x += TEXTURE_WIDTH) { // texture mirrored float bottom = (float) ((line + 1) * lineheight) / TEXTURE_HEIGHT; float top = (float) (line * lineheight) / TEXTURE_HEIGHT; texturepos[4] = top; texturepos[9] = bottom; texturepos[14] = bottom; texturepos[19] = top; context.glPushMatrix(); context.glTranslatef(x, y, 0); context.drawQuadWithTexture(texture, texturepos); context.glPopMatrix(); } } private int findExistingString(String string) { int length = lines; for (int i = 0; i < length; i++) { if (string.equals(linestrings[i])) { lastused[i] = lastUsedCount++; return i; } } return -1; } private int findLineToUse() { int unnededline = 0; int unnededrating = Integer.MAX_VALUE; for (int i = 0; i < lines; i++) { if (lastused[i] < unnededrating) { unnededline = i; unnededrating = lastused[i]; } } // now free the next lines for (int next = unnededline; next > -1; next = nextTile[next]) { nextTile[next] = -1; lastused[next] = 0; linestrings[next] = null; } return unnededline; } private int findLineFor(String string) { int line = findExistingString(string); if (line >= 0) { return line; } int width = (int) Math.ceil(computeWidth(string) + 25); renderer = new TextView(context.getAndroidContext()); renderer.setTextColor(Color.WHITE); renderer.setSingleLine(true); renderer.setTextSize(TypedValue.COMPLEX_UNIT_PX, getScaledSize()); renderer.setText(string); int firstLine = findLineToUse(); // System.out.println("string cache miss for " + string + // ", allocating new line: " + firstLine); int lastLine = firstLine; for (int x = 0; x < width; x += TEXTURE_WIDTH) { if (x == 0) { line = firstLine; } else { line = findLineToUse(); nextTile[lastLine] = line; linestrings[line] = null; linewidths[line] = -1; } // important to not allow cycles. lastused[line] = Integer.MAX_VALUE; // just to be sure. nextTile[line] = -1; // render the new text to that line. Bitmap bitmap = Bitmap.createBitmap(TEXTURE_WIDTH, lineheight, Bitmap.Config.ALPHA_8); Canvas canvas = new Canvas(bitmap); renderer.layout(0, 0, width, lineheight); canvas.translate(-x, 0); renderer.draw(canvas); // canvas.translate(50, .8f * lineheight); ByteBuffer dst = ByteBuffer.allocateDirect(lineheight * TEXTURE_WIDTH); bitmap.copyPixelsToBuffer(dst); dst.rewind(); context.updateTextureAlpha(texture, 0, line * lineheight, TEXTURE_WIDTH, lineheight, dst); lastLine = line; } lastused[firstLine] = lastUsedCount++; linestrings[firstLine] = string; linewidths[firstLine] = width; checkInvariants(); return firstLine; } private void initialize() { if (texture == null || !texture.isValid()) { texture = context.generateTextureAlpha(TEXTURE_WIDTH, TEXTURE_HEIGHT); lineheight = (int) (getScaledSize() * 1.3); lines = TEXTURE_HEIGHT / lineheight; linestrings = new String[lines]; linewidths = new int[lines]; lastused = new int[lines]; nextTile = new int[lines]; Arrays.fill(nextTile, -1); texturepos[1] = lineheight; texturepos[16] = lineheight; } } @Override public float getWidth(String string) { int index = findExistingString(string); if (index < 0) { return computeWidth(string); } else { return linewidths[index]; } } private float computeWidth(String string) { Paint paint = new Paint(); paint.setTextSize(getScaledSize()); return paint.measureText(string); } @Override public float getHeight(String string) { return getScaledSize(); } private float getScaledSize() { return size.getSize() * pixelScale; } @Override public void setColor(float red, float green, float blue, float alpha) { context.color(red, green, blue, alpha); } public static TextDrawer getInstance(EFontSize size, AndroidContext context) { int ordinal = size.ordinal(); if (instances[ordinal] == null) { instances[ordinal] = new AndroidTextDrawer(size, context); } return instances[ordinal]; } public static void invalidateTextures() { for (int i = 0; i < instances.length; i++) { instances[i] = null; } } }