1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.apis.graphics.spritetext;
18 
19 import android.graphics.Bitmap;
20 import android.graphics.Canvas;
21 import android.graphics.Paint;
22 import android.graphics.Rect;
23 import android.graphics.Paint.Style;
24 import android.graphics.drawable.Drawable;
25 import android.opengl.GLUtils;
26 
27 import java.util.ArrayList;
28 
29 import javax.microedition.khronos.opengles.GL10;
30 import javax.microedition.khronos.opengles.GL11;
31 import javax.microedition.khronos.opengles.GL11Ext;
32 
33 /**
34  * An OpenGL text label maker.
35  *
36  *
37  * OpenGL labels are implemented by creating a Bitmap, drawing all the labels
38  * into the Bitmap, converting the Bitmap into an Alpha texture, and drawing
39  * portions of the texture using glDrawTexiOES.
40  *
41  * The benefits of this approach are that the labels are drawn using the high
42  * quality anti-aliased font rasterizer, full character set support, and all the
43  * text labels are stored on a single texture, which makes it faster to use.
44  *
45  * The drawbacks are that you can only have as many labels as will fit onto one
46  * texture, and you have to recreate the whole texture if any label text
47  * changes.
48  *
49  */
50 public class LabelMaker {
51     /**
52      * Create a label maker
53      * or maximum compatibility with various OpenGL ES implementations,
54      * the strike width and height must be powers of two,
55      * We want the strike width to be at least as wide as the widest window.
56      *
57      * @param fullColor true if we want a full color backing store (4444),
58      * otherwise we generate a grey L8 backing store.
59      * @param strikeWidth width of strike
60      * @param strikeHeight height of strike
61      */
LabelMaker(boolean fullColor, int strikeWidth, int strikeHeight)62     public LabelMaker(boolean fullColor, int strikeWidth, int strikeHeight) {
63         mFullColor = fullColor;
64         mStrikeWidth = strikeWidth;
65         mStrikeHeight = strikeHeight;
66         mTexelWidth = (float) (1.0 / mStrikeWidth);
67         mTexelHeight = (float) (1.0 / mStrikeHeight);
68         mClearPaint = new Paint();
69         mClearPaint.setARGB(0, 0, 0, 0);
70         mClearPaint.setStyle(Style.FILL);
71         mState = STATE_NEW;
72     }
73 
74     /**
75      * Call to initialize the class.
76      * Call whenever the surface has been created.
77      *
78      * @param gl
79      */
initialize(GL10 gl)80     public void initialize(GL10 gl) {
81         mState = STATE_INITIALIZED;
82         int[] textures = new int[1];
83         gl.glGenTextures(1, textures, 0);
84         mTextureID = textures[0];
85         gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureID);
86 
87         // Use Nearest for performance.
88         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
89                 GL10.GL_NEAREST);
90         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
91                 GL10.GL_NEAREST);
92 
93         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
94                 GL10.GL_CLAMP_TO_EDGE);
95         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
96                 GL10.GL_CLAMP_TO_EDGE);
97 
98         gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE,
99                 GL10.GL_REPLACE);
100     }
101 
102     /**
103      * Call when the surface has been destroyed
104      */
shutdown(GL10 gl)105     public void shutdown(GL10 gl) {
106         if ( gl != null) {
107             if (mState > STATE_NEW) {
108                 int[] textures = new int[1];
109                 textures[0] = mTextureID;
110                 gl.glDeleteTextures(1, textures, 0);
111                 mState = STATE_NEW;
112             }
113         }
114     }
115 
116     /**
117      * Call before adding labels. Clears out any existing labels.
118      *
119      * @param gl
120      */
beginAdding(GL10 gl)121     public void beginAdding(GL10 gl) {
122         checkState(STATE_INITIALIZED, STATE_ADDING);
123         mLabels.clear();
124         mU = 0;
125         mV = 0;
126         mLineHeight = 0;
127         Bitmap.Config config = mFullColor ?
128                 Bitmap.Config.ARGB_4444 : Bitmap.Config.ALPHA_8;
129         mBitmap = Bitmap.createBitmap(mStrikeWidth, mStrikeHeight, config);
130         mCanvas = new Canvas(mBitmap);
131         mBitmap.eraseColor(0);
132     }
133 
134     /**
135      * Call to add a label
136      *
137      * @param gl
138      * @param text the text of the label
139      * @param textPaint the paint of the label
140      * @return the id of the label, used to measure and draw the label
141      */
add(GL10 gl, String text, Paint textPaint)142     public int add(GL10 gl, String text, Paint textPaint) {
143         return add(gl, null, text, textPaint);
144     }
145 
146     /**
147      * Call to add a label
148      *
149      * @param gl
150      * @param text the text of the label
151      * @param textPaint the paint of the label
152      * @return the id of the label, used to measure and draw the label
153      */
add(GL10 gl, Drawable background, String text, Paint textPaint)154     public int add(GL10 gl, Drawable background, String text, Paint textPaint) {
155         return add(gl, background, text, textPaint, 0, 0);
156     }
157 
158     /**
159      * Call to add a label
160      * @return the id of the label, used to measure and draw the label
161      */
add(GL10 gl, Drawable drawable, int minWidth, int minHeight)162     public int add(GL10 gl, Drawable drawable, int minWidth, int minHeight) {
163         return add(gl, drawable, null, null, minWidth, minHeight);
164     }
165 
166     /**
167      * Call to add a label
168      *
169      * @param gl
170      * @param text the text of the label
171      * @param textPaint the paint of the label
172      * @return the id of the label, used to measure and draw the label
173      */
add(GL10 gl, Drawable background, String text, Paint textPaint, int minWidth, int minHeight)174     public int add(GL10 gl, Drawable background, String text, Paint textPaint,
175             int minWidth, int minHeight) {
176         checkState(STATE_ADDING, STATE_ADDING);
177         boolean drawBackground = background != null;
178         boolean drawText = (text != null) && (textPaint != null);
179 
180         Rect padding = new Rect();
181         if (drawBackground) {
182             background.getPadding(padding);
183             minWidth = Math.max(minWidth, background.getMinimumWidth());
184             minHeight = Math.max(minHeight, background.getMinimumHeight());
185         }
186 
187         int ascent = 0;
188         int descent = 0;
189         int measuredTextWidth = 0;
190         if (drawText) {
191             // Paint.ascent is negative, so negate it.
192             ascent = (int) Math.ceil(-textPaint.ascent());
193             descent = (int) Math.ceil(textPaint.descent());
194             measuredTextWidth = (int) Math.ceil(textPaint.measureText(text));
195         }
196         int textHeight = ascent + descent;
197         int textWidth = Math.min(mStrikeWidth,measuredTextWidth);
198 
199         int padHeight = padding.top + padding.bottom;
200         int padWidth = padding.left + padding.right;
201         int height = Math.max(minHeight, textHeight + padHeight);
202         int width = Math.max(minWidth, textWidth + padWidth);
203         int effectiveTextHeight = height - padHeight;
204         int effectiveTextWidth = width - padWidth;
205 
206         int centerOffsetHeight = (effectiveTextHeight - textHeight) / 2;
207         int centerOffsetWidth = (effectiveTextWidth - textWidth) / 2;
208 
209         // Make changes to the local variables, only commit them
210         // to the member variables after we've decided not to throw
211         // any exceptions.
212 
213         int u = mU;
214         int v = mV;
215         int lineHeight = mLineHeight;
216 
217         if (width > mStrikeWidth) {
218             width = mStrikeWidth;
219         }
220 
221         // Is there room for this string on the current line?
222         if (u + width > mStrikeWidth) {
223             // No room, go to the next line:
224             u = 0;
225             v += lineHeight;
226             lineHeight = 0;
227         }
228         lineHeight = Math.max(lineHeight, height);
229         if (v + lineHeight > mStrikeHeight) {
230             throw new IllegalArgumentException("Out of texture space.");
231         }
232 
233         int u2 = u + width;
234         int vBase = v + ascent;
235         int v2 = v + height;
236 
237         if (drawBackground) {
238             background.setBounds(u, v, u + width, v + height);
239             background.draw(mCanvas);
240         }
241 
242         if (drawText) {
243             mCanvas.drawText(text,
244                     u + padding.left + centerOffsetWidth,
245                     vBase + padding.top + centerOffsetHeight,
246                     textPaint);
247         }
248 
249         // We know there's enough space, so update the member variables
250         mU = u + width;
251         mV = v;
252         mLineHeight = lineHeight;
253         mLabels.add(new Label(width, height, ascent,
254                 u, v + height, width, -height));
255         return mLabels.size() - 1;
256     }
257 
258     /**
259      * Call to end adding labels. Must be called before drawing starts.
260      *
261      * @param gl
262      */
endAdding(GL10 gl)263     public void endAdding(GL10 gl) {
264         checkState(STATE_ADDING, STATE_INITIALIZED);
265         gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureID);
266         GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmap, 0);
267         // Reclaim storage used by bitmap and canvas.
268         mBitmap.recycle();
269         mBitmap = null;
270         mCanvas = null;
271     }
272 
273     /**
274      * Get the width in pixels of a given label.
275      *
276      * @param labelID
277      * @return the width in pixels
278      */
getWidth(int labelID)279     public float getWidth(int labelID) {
280         return mLabels.get(labelID).width;
281     }
282 
283     /**
284      * Get the height in pixels of a given label.
285      *
286      * @param labelID
287      * @return the height in pixels
288      */
getHeight(int labelID)289     public float getHeight(int labelID) {
290         return mLabels.get(labelID).height;
291     }
292 
293     /**
294      * Get the baseline of a given label. That's how many pixels from the top of
295      * the label to the text baseline. (This is equivalent to the negative of
296      * the label's paint's ascent.)
297      *
298      * @param labelID
299      * @return the baseline in pixels.
300      */
getBaseline(int labelID)301     public float getBaseline(int labelID) {
302         return mLabels.get(labelID).baseline;
303     }
304 
305     /**
306      * Begin drawing labels. Sets the OpenGL state for rapid drawing.
307      *
308      * @param gl
309      * @param viewWidth
310      * @param viewHeight
311      */
beginDrawing(GL10 gl, float viewWidth, float viewHeight)312     public void beginDrawing(GL10 gl, float viewWidth, float viewHeight) {
313         checkState(STATE_INITIALIZED, STATE_DRAWING);
314         gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureID);
315         gl.glShadeModel(GL10.GL_FLAT);
316         gl.glEnable(GL10.GL_BLEND);
317         gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
318         gl.glColor4x(0x10000, 0x10000, 0x10000, 0x10000);
319         gl.glMatrixMode(GL10.GL_PROJECTION);
320         gl.glPushMatrix();
321         gl.glLoadIdentity();
322         gl.glOrthof(0.0f, viewWidth, 0.0f, viewHeight, 0.0f, 1.0f);
323         gl.glMatrixMode(GL10.GL_MODELVIEW);
324         gl.glPushMatrix();
325         gl.glLoadIdentity();
326         // Magic offsets to promote consistent rasterization.
327         gl.glTranslatef(0.375f, 0.375f, 0.0f);
328     }
329 
330     /**
331      * Draw a given label at a given x,y position, expressed in pixels, with the
332      * lower-left-hand-corner of the view being (0,0).
333      *
334      * @param gl
335      * @param x
336      * @param y
337      * @param labelID
338      */
draw(GL10 gl, float x, float y, int labelID)339     public void draw(GL10 gl, float x, float y, int labelID) {
340         checkState(STATE_DRAWING, STATE_DRAWING);
341         Label label = mLabels.get(labelID);
342         gl.glEnable(GL10.GL_TEXTURE_2D);
343         ((GL11)gl).glTexParameteriv(GL10.GL_TEXTURE_2D,
344                 GL11Ext.GL_TEXTURE_CROP_RECT_OES, label.mCrop, 0);
345         ((GL11Ext)gl).glDrawTexiOES((int) x, (int) y, 0,
346                 (int) label.width, (int) label.height);
347     }
348 
349     /**
350      * Ends the drawing and restores the OpenGL state.
351      *
352      * @param gl
353      */
endDrawing(GL10 gl)354     public void endDrawing(GL10 gl) {
355         checkState(STATE_DRAWING, STATE_INITIALIZED);
356         gl.glDisable(GL10.GL_BLEND);
357         gl.glMatrixMode(GL10.GL_PROJECTION);
358         gl.glPopMatrix();
359         gl.glMatrixMode(GL10.GL_MODELVIEW);
360         gl.glPopMatrix();
361     }
362 
checkState(int oldState, int newState)363     private void checkState(int oldState, int newState) {
364         if (mState != oldState) {
365             throw new IllegalArgumentException("Can't call this method now.");
366         }
367         mState = newState;
368     }
369 
370     private static class Label {
Label(float width, float height, float baseLine, int cropU, int cropV, int cropW, int cropH)371         public Label(float width, float height, float baseLine,
372                 int cropU, int cropV, int cropW, int cropH) {
373             this.width = width;
374             this.height = height;
375             this.baseline = baseLine;
376             int[] crop = new int[4];
377             crop[0] = cropU;
378             crop[1] = cropV;
379             crop[2] = cropW;
380             crop[3] = cropH;
381             mCrop = crop;
382         }
383 
384         public float width;
385         public float height;
386         public float baseline;
387         public int[] mCrop;
388     }
389 
390     private int mStrikeWidth;
391     private int mStrikeHeight;
392     private boolean mFullColor;
393     private Bitmap mBitmap;
394     private Canvas mCanvas;
395     private Paint mClearPaint;
396 
397     private int mTextureID;
398 
399     private float mTexelWidth;  // Convert texel to U
400     private float mTexelHeight; // Convert texel to V
401     private int mU;
402     private int mV;
403     private int mLineHeight;
404     private ArrayList<Label> mLabels = new ArrayList<Label>();
405 
406     private static final int STATE_NEW = 0;
407     private static final int STATE_INITIALIZED = 1;
408     private static final int STATE_ADDING = 2;
409     private static final int STATE_DRAWING = 3;
410     private int mState;
411 }
412