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

Comments
Leave a comment Trackback