Android Development

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:
tutorial2d-muliple-images

Go to Playing with graphics in Android – Part V

  • Share/Bookmark
:, , , ,

15 Comments for this entry

  • Melvin

    Thank you for your tutorials. I’ve been watching them closely.

    Please keep it up! :)

  • Martin

    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):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    @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);
        }
    }
  • stam

    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!

  • Shanjaq

    GraphicObject.Speed is not defined in this tutorial.

    Very helpful tutorials here, thanks for posting these!

  • Martin

    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!

  • stam

    Now it works perfectly!

    Thank you very much and we are waiting for the 3D tutorial! :)

    Have a nice day!

  • Ben

    very nice! thanks!

  • Fred

    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

  • Martin

    There are two possible solutions:
    1. Simply remove the @Override annotations
    2. (the better solution) update your java sdk to version 1.6

  • dg

    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

  • Martin

    The SurfaceView is simply the way you should do 2D drawing.
    A normal View is more for layouts you can do with by xml.

  • dg

    it’s clear
    thank you

  • krebchak

    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.

  • brian

    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

  • Martin

    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.

Leave a Reply