Using Bitmap Fonts with Android
In this part I will try to explain what I understand on good game design elements. I will use droids in the examples and I will script a basic fight simulator to see how they behave.
Android is an OS running on many types of devices with different screen sizes. Because of that it is pretty difficult to address font related issues regarding both size and appearance on these platforms.
To use a consistent font face across devices I have used bitmap fonts. That is, each character is represented by a bitmap.
We can create an image for each letter in the alphabet and number for example and whenever we want to display a text, we draw the images of the characters from the string at the given position.
But wait! Isn’t loading an image for every character overkill? Yes it is. We can use android’s utilities for manipulating bitmaps to our advantage.
All we need is a bitmap containing all the characters we want to use and slice it up into individual bitmaps. To do this the easy way we will use a monospaced font. A monospaced font is a font whose letters and characters occupy the same horizontal space. In other words, the characters have the same width.
A simple character map can look like this.
Character Map
Note that the background is transparent.
The above character map is arranged in a grid. The width of a character is 8 pixels. The height is 12 pixels. Note that the second row starts at 15 pixels so there is a gap between the rows. It’s just a choice and also how Photoshop broke the lines and I didn’t bother to change them.
Transparent Background
The above image shows the beginning of the first row and how the characters are organised into cells.
Following this it is easy to slice it up. A simple for iteration will do the trick. To keep things simple I have created a map of only the English alphabet, digits and a few punctuation marks. You can extend it as you wish. Use a monospaced font in Photoshop or Gimp or whatever image editor you fancy and create your own sheet.
Here are plenty of bitmap fonts to choose from.
How can we use this? My idea to display text onto the screen is to create a drawString
method that takes the text to be displayed as a parameter along with the position where we want to display it.
Something like this will do it:
void drawString(Canvas canvas, String text, int x, int y)
I also pass the canvas
object in, onto which I want to draw the text. This is just for simplicity. In case of an OpenGL renderer we will have to use billboards (squares with textures). But for the purpose of slicing images and displaying fonts let’s stick with this approach.
Create a simple Android project that use simple 2D canvas. We will draw onto that.
I implemented the SurfaceView
to hold my canvas and called it DrawingPanel.
In its constructor I simply register it to receive events when touching the surface and load the resources. The resources are in fact just the images of the glyphs/characters.
Download the following image file and drag it into your eclipse’s projects resource folder under: /res/drawable-mdpi
for ADP to generate the id for the resource.
Glyphs
Create the DrawingPanel
class.
public class DrawingPanel extends SurfaceView implements SurfaceHolder.Callback {
private Canvas canvas; // the canvas to draw on
private Glyphs glyphs; // the glyphs
public DrawingPanel(Context context) {
super(context);
// adding the panel to handle events
getHolder().addCallback(this);
// initialise resources
loadResources();
// making the Panel focusable so it can handle events
setFocusable(true);
}
/** Loads the images of the glyphs */
private void loadResources() {
this.glyphs = new Glyphs(BitmapFactory.decodeResource(getResources(), R.drawable.glyphs_green));
Log.d(TAG, "Green glyphs loaded");
}
}
I omitted the other methods that need to be implemented as they are just stubs.
The canvas
variable is used to draw the text onto and is obtained at every touch event. You’ll see later in the onTouchEvent
method.
The most interesting class is the Glyphs
class which holds the association of characters with images. The glyphs
variable is instantiated in the loadResources()
method. It calls the constructor of the Glyphs
class with the character sheet image copied earlier.
Check out the Glyphs
class:
package net.obviam.fonts;
import java.util.HashMap;
import java.util.Map;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.util.Log;
/**
* @author impaler
*
*/
public class Glyphs {
private static final String TAG = Glyphs.class.getSimpleName();
private Bitmap bitmap; // bitmap containing the character map/sheet
// Map to associate a bitmap to each character
private Map<Character, Bitmap> glyphs = new HashMap<Character, Bitmap>(62);
private int width; // width in pixels of one character
private int height; // height in pixels of one character
// the characters in the English alphabet
private char[] charactersL = new char[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z' };
private char[] charactersU = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z' };
private char[] numbers = new char[] { '1', '2', '3', '4', '5', '6', '7',
'8', '9', '0' };
public Glyphs(Bitmap bitmap) {
super();
this.bitmap = bitmap;
this.width = 8;
this.height = 12;
// Cutting up the glyphs
// Starting with the first row - lower cases
for (int i = 0; i < 26; i++) {
glyphs.put(charactersL[i], Bitmap.createBitmap(bitmap,
0 + (i * width), 0, width, height));
}
Log.d(TAG, "Lowercases initialised");
// Continuing with the second row - upper cases
// Note that the row starts at 15px - hardcoded
for (int i = 0; i < 26; i++) {
glyphs.put(charactersU[i], Bitmap.createBitmap(bitmap,
0 + (i * width), 15, width, height));
}
// row 3 for numbers
Log.d(TAG, "Uppercases initialised");
for (int i = 0; i < 10; i++) {
glyphs.put(numbers[i], Bitmap.createBitmap(bitmap,
0 + (i * width), 30, width, height));
}
Log.d(TAG, "Numbers initialised");
// TODO - 4th row for punctuation
}
public Bitmap getBitmap() {
return bitmap;
}
/**
* Draws the string onto the canvas at <code>x</code> and <code>y</code>
* @param text
*/
public void drawString(Canvas canvas, String text, int x, int y) {
if (canvas == null) {
Log.d(TAG, "Canvas is null");
}
for (int i = 0; i < text.length(); i++) {
Character ch = text.charAt(i);
if (glyphs.get(ch) != null) {
canvas.drawBitmap(glyphs.get(ch), x + (i * width), y, null);
}
}
}
}
The line
private Map<Character, Bitmap> glyphs = new HashMap<Character, Bitmap>(62);
creates the map which associates a bitmap to each character.
To load this up we need the bitmaps for each character. I have created 3 arrays for the characters for which I have bitmaps.
charactersL[]
holds the lower case letters of the English alphabet, charactersU[]
the upper case letters and numbers[]
holds the numbers. As an exercise add the punctuation array as this one is missing.
Note that the order of the characters is the same as in the character sheet.
For convenience I have created an array for each row from the image. To associate the images with the characters I will iterate through them and cut out the respective image from the sheet based on the index. Having a fixed width for the characters, makes all this dead easy. Examine carefully and understand the constructor. It does the slicing and association.
The drawString(Canvas canvas, String text, int x, int y)
method does the drawing. It takes the position where to draw, iterates through the passed in text
and draws every character progressively. It is easy to calculate the horizontal offset as each character has the same width.
That is it. To try it out add the following method to the DrawingPanel
:
public boolean onTouchEvent(MotionEvent event) {
// draw text at touch
try {
canvas = getHolder().lockCanvas();
synchronized (getHolder()) {
if (event.getAction() == MotionEvent.ACTION_DOWN
|| event.getAction() == MotionEvent.ACTION_MOVE) {
// clear the screen
canvas.drawColor(Color.BLACK);
// draw glyphs
glyphs.drawString(canvas, "Drawing string at "
+ (int) event.getX() + " " + (int) event.getY(),
(int) event.getX(), (int) event.getY());
}
if (event.getAction() == MotionEvent.ACTION_UP) {
canvas.drawColor(Color.BLACK);
glyphs.drawString(canvas, "Drawn string at "
+ (int) event.getX() + " " + (int) event.getY(),
(int) event.getX(), (int) event.getY());
}
}
} finally {
if (canvas != null) {
getHolder().unlockCanvasAndPost(canvas);
}
}
// event was handled
return true;
}
Every time we touch the screen, we try to get hold of the canvas to be able to draw onto it. In case of a touch or drag we simply clear the canvas and draw the string onto it.
When we stopped touching the screen we display a slightly different text at the last position.
Finally we release the canvas. Make sure you return true
to signal that the event was handled.
That is it. One last thing needed is to set the DrawingPanel
to be our view. This one is done in the activity created with the project. I have also disabled the title.
public class PrintingActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// turn off title
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(new DrawingPanel(this));
}
}
The resulting application should look like this:
Resulting App
To do this with OpenGL follow the same principle and use the bitmaps for textures. I will publish an OpenGL version of this article at some point so stay tuned.
Download the code here (TBD).