This how to will show you how you can create a simple 2D Map with Cells to place stuff on it. Just like the old school SimCity.

The first thing you need is an Activity with a SurfaceView and a Thread to trigger the drawing. Who doesn’t know these fundamentals, please read my series on 2d graphics first.

Lets start with the smallest unit for our map: the Cell.
Each Cell will have a background color and a unique ID.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.droidnova.android.games;
 
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
 
/**
 * A part of the map.
 */
public class Cell {
    public int _id = 0;
    public int _backgroundColor = Color.GREEN;
 
    /**
     * Konstruktor.
     * @param id
     */
    public Cell(int id) {
        _id = id;
    }
 
    /**
     * Draw the cell
     *  
     * @param canvas Canvas to draw on.
     * @param paint Color of the "pencil".
     * @param x X coordinate.
     * @param y Y coordinate.
     */
    public void draw(Canvas canvas, Paint paint, int x, int y) {
        paint.setColor(_backgroundColor);
        canvas.drawRect(x, y, x + CellMap._cellSize, y + CellMap._cellSize, paint);
 
        paint.setColor(Color.BLACK);
        canvas.drawText("" + _id, x + 1, y + 10, paint);
    }
}

On line 32 you see, how we draw the cell. The variable _cellSize is a static variable from CellMap, which will be introduced later. Everything else should be already known.

The next thing we need is the Thread which will trigger the onDraw() method of our view.
If you have done my series about SurfaceViews you should already know everything. Its a plain copy from the series.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.droidnova.android.games;
 
import android.graphics.Canvas;
 
/**
 * Thread class to perform the so called "game loop".
 * 
 * @author martin
 */
public class MapThread extends Thread {
    private CellMap _cellMap;
    public boolean _run = false;
 
    /**
     * Constructor.
     * 
     * @param panel View class on which we trigger the drawing.
     */
    public MapThread(CellMap map) {
        _cellMap = map;
    }
 
    /**
     * Perform the game loop.
     */
    @Override
    public void run() {
        Canvas c;
        while (_run) {
            c = null;
            try {
                c = _cellMap.getHolder().lockCanvas(null);
                synchronized (_cellMap.getHolder()) {
                    _cellMap.onDraw(c);
                }
            } finally {
                // do this in a finally so that if an exception is thrown
                // during the above, we don't leave the Surface in an
                // inconsistent state
                if (c != null) {
                    _cellMap.getHolder().unlockCanvasAndPost(c);
                }
            }
        }
    }
}

The last one before we dive into the Map itself is our Activity. Nothing new here, too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.droidnova.android.games;
 
import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
 
public class ScrollMap extends Activity {
    private static final String LOG_TAG = ScrollMap.class.getSimpleName();
    private CellMap _map;
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        _map = new CellMap(this);
        setContentView(_map);
    }
}

Ok, now we are good to start the real map.
We create a new class which extends from SurfaceView and implements SurfaceHolder.Callback. We will also override all needed methods for the Callback Interface and the onDraw() method. Inside the interface methods we will handle the Thread. We also add the _cellSize we used in the Cell class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.droidnova.android.games;
 
import android.content.Context;
import android.graphics.Canvas;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
 
/**
 * The SurfaceView, on which we draw the map.
 */
public class CellMap extends SurfaceView implements SurfaceHolder.Callback {
    public static int _cellSize = 30;
    private MapThread _mapThread;
 
    /**
     * Constructor, fills the map on creation.
     * 
     * @param context
     */
    public CellMap(Context context) {
        super(context);
 
        // register the view to the surfaceholder
        getHolder().addCallback(this);
 
        // set the thread - not yet started
        _mapThread = new MapThread(this);
 
        // secure the view is focusable
        setFocusable(true);
    }
 
    /**
     * Draw what we want to see.
     */
    @Override
    public void onDraw(Canvas canvas) {
 
    }
 
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
 
    /**
     * Called when surface created.
     * Starts the thread if not already running.
     */
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (!_mapThread.isAlive()) {
            _mapThread = new MapThread(this);
        }
        _mapThread._run = true;
        _mapThread.start();
    }
 
    /**
     * Called when surface destroyed
     * Stops the thread.
     */
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        boolean retry = true;
        _mapThread._run = false;
        while (retry) {
            try {
                _mapThread.join();
                retry = false;
            } catch (InterruptedException e) {
                // we will try it again and again...
            }
        }
    }
}

If you run this application, you will see a black screen, but thats ok.

How do we implement a Map? Well, the Map itself says what we should use: a class which implements the Map interface. Lets take the HashMap.
The HashMap has one dimension, so we have to combine two HashMaps to realize two dimensions. One HashMap to store the rows and one to store the columns.

1
private HashMap<Integer, HashMap<Integer, Cell>> _mapCells = new HashMap<Integer, HashMap<Integer, Cell>>();

Ok this is one hell of a declaration, lets split this up to show why we have to use it this way.

1
HashMap<Integer, Cell>

represents one row with a non specific number of cells. The Integer will be used as an index to grant easy access to a Cell at the index x.

1
private HashMap<Integer, HashMap<Integer, Cell>>

This is finally the HashMap which contains a non specific number of rows. Each row is a HashMap with Cells. The Integer will be also used as an index to grant easy access to a row at the index y.

Now lets fill the map with cells.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class CellMap extends SurfaceView implements SurfaceHolder.Callback {
    public static int _cellSize = 30;
 
    // HashMap, first level are the rows, second level are the columns
    private HashMap<Integer, HashMap<Integer, Cell>> _mapCells = new HashMap<Integer, HashMap<Integer, Cell>>();
    private MapThread _mapThread;
    private Paint paint = new Paint();
 
    // variables we will use often
    private HashMap<Integer, Cell> _row;
 
    // map size in cells
    public static int _mapSize = 10;
 
    /**
     * Constructor, fills the map on creation.
     * 
     * @param context
     */
    public CellMap(Context context) {
        super(context);
 
        // fill the map with cells
        int id = 0;
        for (int i = 0; i < _mapSize; i++) {
            _row = new HashMap<Integer, Cell>();
            for (int j = 0; j < _mapSize; j++) {
                _row.put(j, new Cell(id++));
            }
            _mapCells.put(i, _row);
        }
 
        // register the view to the surfaceholder
        getHolder().addCallback(this);
 
        // set the thread - not yet started
        _mapThread = new MapThread(this);
 
        // secure the view is focusable
        setFocusable(true);
    }
    // snipped
}

First the new variables: We need to define a _mapSize, which defines the number of rows and columns and the HashMap _mapCells mentioned above. We also need a Paint object to pass it to the onDraw() method of our cell.
Now the interesting part: Between the lines 25 and 32 we have a count variable called id and two loops. The outer loop represents the rows we go through. The inner loop is too fill a HashMap with Cells which represents all columns in one row. On line 29 we fill the row with Cells and on line 31 we add the row to our HashMap _mapCells.
Creation of the map itself is not enough to see something. We have to override the onDraw() method to finally draw the Cells we want to see.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * Draw what we want to see.
 */
@Override
public void onDraw(Canvas canvas) {
    // help variables
    int x = 0;
    int y = 0;
 
    for (int i = 0; i < _mapSize; i++) {
        // get the row
        _row = _mapCells.get(i);
 
        // calculate the correct y for the row
        y = i * _cellSize;
        // go through the row
        for (int j = 0; j < _mapSize; j++) {
            // calculate the x coordinate for the columns
            x = j * _cellSize;
 
            _row.get(j).draw(canvas, paint, x, y);
        }
    }
}

Here we see a loop construction like the one we used to fill the map. This time we want to go through the HashMap and draw each cell at the right place.
A row has always the same y coordinate, because a row is horizontal. So we calculate once the y for the first rows. It will be 0*30 = 0, 1*30 = 30, 2*30 = 60 and so on.
In the inner loop we calculate the x coordinate which will change for every column/cell. For the first columns it will be 0*30 = 0, 1*30 = 30, 2*30 = 60 and so on.
Then we draw it on the coordinates we calculated.
Remember: The draw method in our cell draws the ID of the cell too, so you can see, which order they have.

Please compile/run the app and see the result. We startet with 10, because one drawing of the Map with 100×100 Cells needs nearly 15 seconds on my Emulator.

To enable scrolling, we need to handle the touch events.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// snipped
 
// Offset to the upper left corner of the map
private int _xOffset = 0;
private int _yOffset = 0;
 
// last touch point
private int _xTouch = 0;
private int _yTouch = 0;
 
// scrolling active?
private boolean _isMoving = false;
 
//snipped
 
/**
 * Handle touch event on the map.
 */
@Override
public boolean onTouchEvent(MotionEvent event) {
    // touch down
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        // start of a new event, reset the flag
        _isMoving = false;
        // store the current touch coordinates for scroll calculation
        _xTouch = (int) event.getX();
        _yTouch = (int) event.getY();
    } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
        // touch starts moving, set the flag
        _isMoving = true;
 
        // get the new offset
        _xOffset += _xTouch - (int) event.getX();
        _yOffset += _yTouch - (int) event.getY();
 
        // secure, that the offset is never out of view bounds
        if (_xOffset < 0) {
            _xOffset = 0;
        } else if (_xOffset > _mapSize * _cellSize - getWidth()) {
            _xOffset = _mapSize * _cellSize - getWidth();
        }
        if (_yOffset < 0) {
            _yOffset = 0;
        } else if (_yOffset > _mapSize * _cellSize - getHeight()) {
            _yOffset = _mapSize * _cellSize - getHeight();
        }
 
        // store the last position
        _xTouch = (int) event.getX();
        _yTouch = (int) event.getY();
    } else if (event.getAction() == MotionEvent.ACTION_UP) {
        // touch released
        if (!_isMoving) {
            // calculate the touched cell
            int column = (int) Math.ceil((_xOffset + event.getX()) / _cellSize) - 1;
            int row = (int) Math.ceil((_yOffset + event.getY()) / _cellSize) - 1;
            Cell cell = _mapCells.get(row).get(column);
            // show the id of the touched cell
            Toast.makeText(getContext(), "Cell id #" + cell._id, Toast.LENGTH_SHORT).show();
        }
    }
    return true;
}

First the variables: We need two int variables to store the coordinates of our last touch event, _xTouch and _yTouch, and we need two int variables to store our calculated offset, _xOffset and _yOffset. To make the difference between a tap and a move, we need a flag of type boolean, _isMoving.
Now the magic: When a touch event occurs, the start of every touch will be the ACTION_DOWN. When this occurs, we will make sure, that the flag will be reseted to false and the location of the touch is stored. Now are two different ways the touch event may continue: First we start to move the finger around or we lift the finger and release the touch.
Simple first: When the ACTION_UP occurs, we check for our flag, in this case the flag should be false, so we enter the if code block. We calculate the column/row considering the offset, in this case 0. With the divide of 0, we get the Cell number. Our HashMap index is 0 based, so we have to subtract 1 to get the real index. After getting the Cell at this index pair, we show a short Toast message with the id. The id should be the same of the Cell you just touched.
Now the scrolling: When the ACTION_MOVE occurs, we set our flag to true. That will prevent the Toast mentioned above to be shown when we finally stop the scrolling and lift the finger from the screen.
Now we calculate the offset we need to draw the map at different coordinates. The order of the subtraction is important. When you change it, the map will move the opposite direction of your motion. The if/else after that are only important to make sure, that you never will see space outside the map. Mainly because we draw no background or something else, so if we scroll outside the map, we will see a lot of graphic failures (test it with 10 or less as _mapSize and scroll around). The last thing for this event, we have to store the touch position.

To get the scrolling finally to work, we need to subtract the offset from the coordinate calculation in our onDraw() method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * Draw what we want to see.
 */
@Override
public void onDraw(Canvas canvas) {
    // help variables
    int x = 0;
    int y = 0;
 
    for (int i = 0; i < _mapSize; i++) {
        // get the row
        _row = _mapCells.get(i);
 
        // calculate the correct y for the row
        y = i * _cellSize - _yOffset;
        // go through the row
        for (int j = 0; j < _mapSize; j++) {
            // calculate the x coordinate for the columns
            x = j * _cellSize - _xOffset;
 
            _row.get(j).draw(canvas, paint, x, y);
        }
    }
}

As you see on lines 15 and 19, we subtract the offsets.

Now compile and run it. Make sure you check the performance, so try a map size of 100 or more. You will see it starts to get very laggy.

On the next part, I show you how to make a smooth scrolling with a map size up to 300 cells (which means 90.000 Cells).

Sources as Eclipse project: ScrollMap

Go to the next part: Create a scrollable Map with Cells – Part II

  • Share/Bookmark