How to: Create a scrollable Map with Cells
by Martin on Jan.04, 2010, under how to, tutorial
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

January 5th, 2010 on 8:39 pm
Hi and thanks for this nice tutorial!
I think this might be useful for a little game project i want to start soon.
January 6th, 2010 on 1:40 am
Great tutorial, thanks a bunch! Android development is much easier than iPhone development in my opinion. This same type of scrolling map on an iPhone is insanely more complex. Thanks again!
January 7th, 2010 on 8:28 am
thanks Martin, it’s very very useful for me. i have to work to implement a MapView like google map lib on OMS(release by China Mobile LTD) that does’t include google map adds-on
this tutorial give me some new ideas . you’ve done a good job . i learn a lot following your tutorials. thanks.
waiting for your new tutorial
January 16th, 2010 on 5:03 pm
Hey,
Great tutorial.. any thoughts on making this example screen resolution independent?
January 17th, 2010 on 12:37 am
This will be a topic in the next step. I’m currently at the end of my semester so its time for project releases and exams. I promise to post more in the future.
January 17th, 2010 on 7:56 pm
Just a friendly reflection. You name class-variables starting with _. E.g. _isMoving in this example.
This is probably needlessly confusing to beginners, looks kinda ugly to be honest, and more importantly goes against Java’s naming convention. The following comes straight from the official site:
“Variable names should not start with underscore _ or dollar sign $ characters, even though both are allowed.”
It’s probably just a bad habit, but habits are made to be broken.
January 17th, 2010 on 8:23 pm
Hey,
your right, my conventions are from the j2ee projects where I learned Java. We had Checkstyle installed to check that every variable has its prefix _
Currently I “learn” to use the m prefix, which is defined by the Android Style Guide.
January 26th, 2010 on 8:44 am
Hi Martin,
very nice and useful tutorial. Although rendering the complete scene, this is very useful. Can’t wait for the next tutorial part, showing of how to do with 200×200 cells
Just mail me, if you’re going to do so!
February 6th, 2010 on 12:26 am
Hi Martin!
Very nice tutorial, this helped me a lot! Keep the good work up!
Can’t wait to next one but i need some help.. pretty new to this! How do you implement a whole background? Or do you need to it for every cell?
And any tips before the next tutorial, how to do it smoother?
Thanks!
February 6th, 2010 on 12:12 pm
Whole background: Take a look at the onDraw method of this tutorial: http://www.droidnova.com/playing-with-graphics-in-android-part-vii,220.html
To make it smoother is one topic on the next two parts.
I’m in the final week in my current semester. Please give me the time