Playing with graphics in Android – Part IV
by Martin on May.18, 2009, under tutorial
You are new to this series? Please start with the first part.
The fourth part of the series will show you how to add more bitmaps on your screen.
First we have to add is graphic class containing a bitmap and the coordinates where it is located.
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 | class GraphicObject { private Bitmap _bitmap; private Coordinates _coordinates; public GraphicObject(Bitmap bitmap) { _bitmap = bitmap; _coordinates = new Coordinates(); } public Bitmap getGraphic() { return _bitmap; } public Coordinates getCoordinates() { return _coordinates; } /** * Contains the coordinates of the graphic. */ public class Coordinates { private int _x = 100; private int _y = 0; public int getX() { return _x + _bitmap.getWidth() / 2; } public void setX(int value) { _x = value - _bitmap.getWidth() / 2; } public int getY() { return _y + _bitmap.getHeight() / 2; } public void setY(int value) { _y = value - _bitmap.getHeight() / 2; } public String toString() { return "Coordinates: (" + _x + "/" + _y + ")"; } } } |
Next is a new variable which will contain all of our added GraphicObjects. We will use an ArrayList because we don’t know how many bitmaps we will add during our run. Every GraphicObject instance will store their own coordinates, so we can remove the _x and _y variable in our Panel class.
1 2 3 4 5 | class Panel extends SurfaceView implements SurfaceHolder.Callback { private TutorialThread _thread; private ArrayList<GraphicObject> _graphics = new ArrayList<GraphicObject>(); // code snipped } |
Now we will modify the onTouchEvent() method to add a new element at the location we touched. The onDraw() method will iterate over every GraphicObject in our ArrayList and will draw it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @Override public boolean onTouchEvent(MotionEvent event) { GraphicObject graphic = new GraphicObject(BitmapFactory.decodeResource(getResources(), R.drawable.icon)); graphic.getCoordinates().setX((int) event.getX() - graphic.getGraphic().getWidth() / 2); graphic.getCoordinates().setY((int) event.getY() - graphic.getGraphic().getHeight() / 2); return _graphics.add(graphic); } @Override public void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); Bitmap bitmap; Coordinates coords; for (GraphicObject graphic : _graphics) { bitmap = graphic.getGraphic(); coords = graphic.getCoordinates(); canvas.drawBitmap(bitmap, coords.getX(), coords.getY(), null); } } |
Our panel now looks like that:
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 | class Panel extends SurfaceView implements SurfaceHolder.Callback { private TutorialThread _thread; private ArrayList<GraphicObject> _graphics = new ArrayList<GraphicObject>(); public Panel(Context context) { super(context); getHolder().addCallback(this); _thread = new TutorialThread(getHolder(), this); setFocusable(true); } @Override public boolean onTouchEvent(MotionEvent event) { GraphicObject graphic = new GraphicObject(BitmapFactory.decodeResource(getResources(), R.drawable.icon)); graphic.getCoordinates().setX((int) event.getX() - graphic.getGraphic().getWidth() / 2); graphic.getCoordinates().setY((int) event.getY() - graphic.getGraphic().getHeight() / 2); return _graphics.add(graphic); } @Override public void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); Bitmap bitmap; GraphicObject.Coordinates coords; for (GraphicObject graphic : _graphics) { bitmap = graphic.getGraphic(); coords = graphic.getCoordinates(); canvas.drawBitmap(bitmap, coords.getX(), coords.getY(), null); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // TODO Auto-generated method stub } @Override public void surfaceCreated(SurfaceHolder holder) { _thread.setRunning(true); _thread.start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { // simply copied from sample application LunarLander: // we have to tell thread to shut down & wait for it to finish, or else // it might touch the Surface after we return and explode boolean retry = true; _thread.setRunning(false); while (retry) { try { _thread.join(); retry = false; } catch (InterruptedException e) { // we will try it again and again... } } } } |
Please compile and run the application. Try to add 40 bitmaps on different locations…
As you will recognize it is not possible. The application will crash. The chance the application crashes increase with the number of bitmaps on the screen.
The reason therefor is simple: if the application runs in the for loop of the onDraw() method and you touch in this very moment the screen, you will generate a ConcurrentModificationException which means, you change the number of elements in the list while iterating over the list.
To prevent that from happening, we should use the synchronization with the _surfaceHolder.
Add a getter for the _surfaceHolder to the TutorialThread class.
1 2 3 | public SurfaceHolder getSurfaceHolder() { return _surfaceHolder; } |
And modify the onTouchEvent() method until it look like this:
1 2 3 4 5 6 7 8 9 10 | @Override public boolean onTouchEvent(MotionEvent event) { synchronized (_thread.getSurfaceHolder()) { GraphicObject graphic = new GraphicObject(BitmapFactory.decodeResource(getResources(), R.drawable.icon)); graphic.getCoordinates().setX((int) event.getX() - graphic.getGraphic().getWidth() / 2); graphic.getCoordinates().setY((int) event.getY() - graphic.getGraphic().getHeight() / 2); _graphics.add(graphic); return true; } } |
Try it now and you will see, the application runs stable.
Note:
Maybe you already recognized, that that you can add bitmaps by simply moving the finger over the touch screen. Thats because we add a bitmap on every event not only if it was a click.
To change that behavior simply add an if statement to the onTouchEvent() method.
1 2 3 4 5 6 7 8 9 10 11 12 | @Override public boolean onTouchEvent(MotionEvent event) { synchronized (_thread.getSurfaceHolder()) { if (event.getAction() == MotionEvent.ACTION_DOWN) { GraphicObject graphic = new GraphicObject(BitmapFactory.decodeResource(getResources(), R.drawable.icon)); graphic.getCoordinates().setX((int) event.getX() - graphic.getGraphic().getWidth() / 2); graphic.getCoordinates().setY((int) event.getY() - graphic.getGraphic().getHeight() / 2); _graphics.add(graphic); } return true; } } |
Full source code:
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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | package com.droidnova.android.tutorial2d; import java.util.ArrayList; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.os.Bundle; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.Window; public class Tutorial2D extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(new Panel(this)); } class Panel extends SurfaceView implements SurfaceHolder.Callback { private TutorialThread _thread; private ArrayList<GraphicObject> _graphics = new ArrayList<GraphicObject>(); public Panel(Context context) { super(context); getHolder().addCallback(this); _thread = new TutorialThread(getHolder(), this); setFocusable(true); } @Override public boolean onTouchEvent(MotionEvent event) { synchronized (_thread.getSurfaceHolder()) { if (event.getAction() == MotionEvent.ACTION_DOWN) { GraphicObject graphic = new GraphicObject(BitmapFactory.decodeResource(getResources(), R.drawable.icon)); graphic.getCoordinates().setX((int) event.getX() - graphic.getGraphic().getWidth() / 2); graphic.getCoordinates().setY((int) event.getY() - graphic.getGraphic().getHeight() / 2); _graphics.add(graphic); } return true; } } @Override public void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); Bitmap bitmap; GraphicObject.Coordinates coords; for (GraphicObject graphic : _graphics) { bitmap = graphic.getGraphic(); coords = graphic.getCoordinates(); canvas.drawBitmap(bitmap, coords.getX(), coords.getY(), null); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // TODO Auto-generated method stub } @Override public void surfaceCreated(SurfaceHolder holder) { _thread.setRunning(true); _thread.start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { // simply copied from sample application LunarLander: // we have to tell thread to shut down & wait for it to finish, or else // it might touch the Surface after we return and explode boolean retry = true; _thread.setRunning(false); while (retry) { try { _thread.join(); retry = false; } catch (InterruptedException e) { // we will try it again and again... } } } } class TutorialThread extends Thread { private SurfaceHolder _surfaceHolder; private Panel _panel; private boolean _run = false; public TutorialThread(SurfaceHolder surfaceHolder, Panel panel) { _surfaceHolder = surfaceHolder; _panel = panel; } public void setRunning(boolean run) { _run = run; } public SurfaceHolder getSurfaceHolder() { return _surfaceHolder; } @Override public void run() { Canvas c; while (_run) { c = null; try { c = _surfaceHolder.lockCanvas(null); synchronized (_surfaceHolder) { _panel.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) { _surfaceHolder.unlockCanvasAndPost(c); } } } } } class GraphicObject { /** * Contains the coordinates of the graphic. */ public class Coordinates { private int _x = 100; private int _y = 0; public int getX() { return _x + _bitmap.getWidth() / 2; } public void setX(int value) { _x = value - _bitmap.getWidth() / 2; } public int getY() { return _y + _bitmap.getHeight() / 2; } public void setY(int value) { _y = value - _bitmap.getHeight() / 2; } public String toString() { return "Coordinates: (" + _x + "/" + _y + ")"; } } private Bitmap _bitmap; private Coordinates _coordinates; public GraphicObject(Bitmap bitmap) { _bitmap = bitmap; _coordinates = new Coordinates(); } public Bitmap getGraphic() { return _bitmap; } public Coordinates getCoordinates() { return _coordinates; } } } |
Screenshot:

May 21st, 2009 on 6:33 am
Thank you for your tutorials. I’ve been watching them closely.
Please keep it up!
May 23rd, 2009 on 8:35 pm
Thank you! It is always good to see that the work is not useless.
I found a minor bug which happens why i blindly copy and paste from my test project into this tutorial project.
Change the onDraw() method to the following (already corrected in the post):
July 26th, 2009 on 10:20 pm
Hello there!!!
For for all these great tutorials!
I have a small problem, when I try to compile the above code, the line:
GraphicObject.Speed speed;
seems to be invalid. Shouldn’t we have the method getSpeed() somewhere in our GraphicObject class??
Thanks in advanced!
July 27th, 2009 on 8:12 pm
GraphicObject.Speed is not defined in this tutorial.
Very helpful tutorials here, thanks for posting these!
July 28th, 2009 on 5:22 pm
Hi,
sorry you are absolutely right. GraphicObject.Speed is a inner class defined in the next part of this series.
I don’t know why it already appeared here.
I removed the not yet used method updatePhysics(). The code should compile now correctly.
Sorry for that and thank you for your bug report!
July 28th, 2009 on 8:53 pm
Now it works perfectly!
Thank you very much and we are waiting for the 3D tutorial!
Have a nice day!
July 31st, 2009 on 5:19 pm
very nice! thanks!
September 6th, 2009 on 11:04 pm
I get the following compile errors. Did I do something wrong?
The method surfaceCreated(SurfaceHolder) of type HelloAndroid.Panel must override a superclass method
The method surfaceDestroyed(SurfaceHolder) of type HelloAndroid.Panel must override a superclass method
The method surfaceChanged(SurfaceHolder, int, int, int) of type HelloAndroid.Panel must override a superclass method
September 7th, 2009 on 5:07 am
There are two possible solutions:
1. Simply remove the @Override annotations
2. (the better solution) update your java sdk to version 1.6
December 9th, 2009 on 4:36 am
thanks a lot
but a question:
why u use a thread(TutorialThread invoke ondraw() ) to
repaint the View instead of
invalidate() ?
and i’m not familar with SurfaceView ,
is there any advantage than View ?
thank you
December 10th, 2009 on 11:20 am
The SurfaceView is simply the way you should do 2D drawing.
A normal View is more for layouts you can do with by xml.
December 12th, 2009 on 2:21 am
it’s clear
thank you
January 6th, 2010 on 5:24 pm
Great tutorial…one thing i noticed is the lack of good tutorials for android develoment. Glad you took the time.
Do you know how instead of handling a touch event you could move the bitmap by listening for SensorEvent? I have accomplished this with out any problems just using a view..but when i switch it to SurfaceView i am running into problems.
Thanks.
January 31st, 2010 on 3:11 pm
Hello,
Nice tutorial you have and I’ve been following you since the start of this tutorial…Just now I have question which was just raised by one of the comment above. Why should we used or create another Thread which we called TutorialThread? Is using postInvalidate() inside the onTouchEvent() function to tell the Panel to redraw is not good? Just now I haved tried using postInvalidate() in redrawing the Panel without using Threads and it works the same but I just need any information maybe using Threads is more ok or lets just say the proper way around…if it is whats the advantage using threads in this scenario?
Thank you so much…Nice post.
-Brian
January 31st, 2010 on 3:28 pm
Well, I wanted to make it the right way. The game loop of a game will do more than just the redrawing. So to introduce it correctly from the beginning, I used the thread.
Later the thread will handle update of physics or game logic.