Pages

Tuesday, January 31, 2012

Android Canvas - A Beginner's Tutorial


So you want to make a game, do you? Do you want to compete with the likes of Angry Birds? Well, learning the Android Canvas is the best place to start.

Many people want to make Android games. I would assume that is why you are at my page. I hope that my tutorial will help you grasp a beginner's understanding of the SurfaceView class and the underlying Canvas class. That way, you'll be making your game or custom view in no time! So, let's start.


Step 1 - Make A Class
In order to start working with the SurfaceView and all of it's functions, you are going to have to extend from it. Create a new class file, and define it as:

    class DrawingPanel extends SurfaceView implements SurfaceHolder.Callback {


    }

Extending the SurfaceView class will allow you to create a view based on SurfaceView and place it into your Activity. Implementing the callback interface will allow you to be notified when the surface is created and destroyed, starting and stopping any current tasks or threads that you may be running when the Activity is created or ended, respectively.

Step 2 - Construct the Class
The next step is to construct the class with many of the important parts. We're going to need a constructor:


    public DrawingPanel(Context context) { 
        super(context);
        getHolder().addCallback(this);

    }


    @Override 
    public void onDraw(Canvas canvas) { 
        //do drawing stuff here.
    } 


    @Override 
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
        int height) { 


    }

As you can see in this code, we are setting up the new SurfaceView. The call to getHolder().addCallback(this) allows us to set up the methods in step 3. The onDraw(Canvas canvas) method will be where you construct what you are going to draw on the canvas. To draw on the canvas, you can use many methods, including:


  • canvas.drawColor(color) //Draws a solid color
  • canvas.drawLine(params) //Draws a line
  • canvas.drawRect(params) //Draws a rectangle
  • canvas.drawBitmap(params) //Draws a bitmap image
  • other methods available at the Android reference site
Step 3 - Create the Drawing Thread
In this step, we will create a thread that will draw whatever we put into the onDraw() method, as it won't call itself.



class PanelThread extends Thread {
        private SurfaceHolder _surfaceHolder;
        private DrawingPanel _panel;
        private boolean _run = false;


        public PanelThread(SurfaceHolder surfaceHolder, DrawingPanel panel) {
            _surfaceHolder = surfaceHolder;
            _panel = panel;
        }


        public void setRunning(boolean run) { //Allow us to stop the thread
            _run = run;
        }


        @Override
        public void run() {
            Canvas c;
            while (_run) {     //When setRunning(false) occurs, _run is 
                c = null;      //set to false and loop ends, stopping thread


                try {


                    c = _surfaceHolder.lockCanvas(null);
                    synchronized (_surfaceHolder) {


                    //Insert methods to modify positions of items in onDraw()
                    postInvalidate();


                    }
} finally {
                    if (c != null) {
                        _surfaceHolder.unlockCanvasAndPost(c);
                    }
                }
            }
        }
    }


Something to note is that this tutorial will use postInvalidate() instead of a direct call to onDraw(). This is because, in my experience, I have found this method to be better for the system and for performance.

The thread code is pretty much copy-paste-able, however, if you want to change positions of any of the drawings, you will have to modify their positions in the section above the call to postInvalidate(). This is also pretty straight-forward, and (usually) requires you to use class-wide variables, stated in the original DrawingPanel class.

Next, we will start with implementing the onSurfaceCreated and onSurfaceDestroyed methods supplyed by the implemented callback in order to stop and start our thread:



    @Override
    public void surfaceCreated(SurfaceHolder holder) {


    setWillNotDraw(false); //Allows us to use invalidate() to call onDraw()


    _thread = new PanelThread(getHolder(), this); //Start the thread that
        _thread.setRunning(true);                     //will make calls to 
        _thread.start();                              //onDraw()
    }


    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    try {
        _thread.setRunning(false);                //Tells thread to stop
_thread.join();                           //Removes thread from mem.
} catch (InterruptedException e) {}
    }


Step 4 - Implement the SurfaceView
This is the easiest step, by far. Simply create a new instance of your View in your activity, and add it. Easy! Now you're done! Go out there and make some good games!

10 comments:

  1. Great, thanks. A working example would be nice.

    ReplyDelete
  2. it is a working example, you gotta paste like 3 things? r u that lazy

    ReplyDelete
  3. I have started implementing this, but there are a few things that could use some clarification.

    First, where is PanelThread defined? Is it in its own file, or embedded in the DrawingPanel object, or defined after the DrawingPanel class in the same file?

    Next, how does the DrawingPanel get access to the _thread in the surfaceCreated/surfaceDestroyed methods? This may end up being obvious after the answer to the first question, but right now it isn't apparent.

    The last thing I noticed was that the @Override tag was not needed by the surfaceChanged, surfaceCreated and surfaceDestroyed methods. This is because the methods in the base are declared as abstract, not virtual (or an indicator I'm inheriting from the wrong thing/version?).

    Any pointers would be appreciated.

    ReplyDelete
    Replies
    1. It should be in the DrawingPanel object. In doing this, it has a direct way to reference the _thread.

      The override is only there because I'm lazy and had eclipse do it for me :p

      Delete
    2. This comment has been removed by the author.

      Delete
    3. Using threads is not a beginners example is it ?

      Delete
    4. Personally, I believe that most of the time, people will be looking for Canvas tutorials because they want to animate something, or make something move in their custom view... They can't do that without the threading. While normally I'd agree with you, the nature of the Canvas almost requires people have a general idea of how to use threads.

      Delete
    5. Man it's unbelievable how people act as spoiled little bitches. It's almost like you have to apologize for the helping.

      Delete
  4. Not sure where to copy stuff to. Could we get a download link to the full source.

    ReplyDelete
  5. this is working code---

    package com.example.surfaceviewexample;

    import android.app.Activity;
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.os.Bundle;
    import android.view.SurfaceHolder;
    import android.view.SurfaceView;

    public class First extends Activity {

    DrawingPanel dp;
    PanelThread _thread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //setContentView(R.layout.first);

    dp = new DrawingPanel(this);
    setContentView(dp);

    }// end of oncreate fns



    // new class start
    class DrawingPanel extends SurfaceView implements SurfaceHolder.Callback {

    public DrawingPanel(Context context) {
    super(context);
    getHolder().addCallback(this);

    }

    @Override
    public void onDraw(Canvas canvas) {

    int x = 0;
    int y = 0;
    Paint paint = new Paint();
    paint.setStyle(Paint.Style.FILL);

    // make the entire canvas white
    paint.setColor(Color.WHITE);
    canvas.drawPaint(paint);
    // another way to do this is to use:
    // canvas.drawColor(Color.WHITE);

    // draw a solid blue circle
    paint.setColor(Color.BLUE);
    canvas.drawCircle(20, 20, 15, paint);

    // draw blue circle with antialiasing turned on
    paint.setAntiAlias(true);
    paint.setColor(Color.BLUE);
    canvas.drawCircle(60, 20, 15, paint);

    }

    @Override
    public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2,int arg3) {

    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {


    setWillNotDraw(false); //Allows us to use invalidate() to call onDraw()


    _thread = new PanelThread(getHolder(), this); //Start the thread that
    _thread.setRunning(true); //will make calls to
    _thread.start(); //onDraw()
    }


    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    try {
    _thread.setRunning(false); //Tells thread to stop
    _thread.join(); //Removes thread from mem.
    } catch (InterruptedException e) {}
    }



    } // end of drawing panel class


    // start of thread class
    class PanelThread extends Thread {
    private SurfaceHolder _surfaceHolder;
    private DrawingPanel _panel;
    private boolean _run = false;


    public PanelThread(SurfaceHolder surfaceHolder, DrawingPanel panel) {
    _surfaceHolder = surfaceHolder;
    _panel = panel;
    }


    public void setRunning(boolean run) { //Allow us to stop the thread
    _run = run;
    }


    @Override
    public void run() {
    Canvas c;
    while (_run) { //When setRunning(false) occurs, _run is
    c = null; //set to false and loop ends, stopping thread


    try {


    c = _surfaceHolder.lockCanvas(null);
    synchronized (_surfaceHolder) {


    //Insert methods to modify positions of items in onDraw()
    postInvalidate();


    }
    } finally {
    if (c != null) {
    _surfaceHolder.unlockCanvasAndPost(c);
    }
    }
    }
    }


    private void postInvalidate() {

    }


    }// end of thread class


    } // end of main class

    ReplyDelete