Sunday, April 28, 2013

Android Game Case Study 1: Getting an image to display using Android Canvas class

Introduction


Using Android's Canvas to display images on the screen is crucial for anyone wanting to program games or similar content.  In this tutorial, I'll take you through some of the basics. 
Assumptions:
  • I assume you have Eclipse downloaded and ready to use.
  • I also assume you have the Android SDKs installed - click here to see the instructions for installing the Eclipse plugin.
  • You have the emulator ready to go (See here for more details.)
It would be very helpful if you've had some experience with Eclipse before attempting this tutorial, but if you follow along carefully, you should be okay.

The version of Eclipse I am using might be slightly different from yours.  I'm using Indigo Service Release 2.  (Build ID 20120216-1857)

Creating a New Android Application

Well, first things first - we need to create a new Android application.
  1. Go to File
  2. Go to New
  3. Click Android Application Project
 
The next thing you do is name your project, and change the package name to something meaningful.  You should have something similar to what I have below:
 


 
 
I chose to set the minimum, target, and compile with to API 10, which is Gingerbread (specifically, Android 2.3.3)  Note that if you use an earlier version of Android like I am, you must set the Theme to None.
 
Click Next, and continue to do so, accepting the default options.  Then, finally click Finish.  You could rename your Main_Activity, but for this example, I chose not to.
 
Once you create the project, you may have an XML file pop up.  You can close it.  Expand the src directory so you can see the package, and then expand the package so you can see the Main_Activity java file.
 
The code should look similar to the following:
 
package com.profjpbaugh.gametest1;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;

public class MainActivity extends Activity {

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

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }
    
}
 
 

Creating a SurfaceView that Implements Runnable

In order to correctly and efficiently draw to the canvas, we have to create a class that extends (inherits from) Android's SurfaceView class.  Additionally, we will implement threading in our application.
 
To create a new class:
  1. Right click the com.profjpbaugh.gametest1 package (or whatever you named yours)
  2. Name the class CustomView
  3. Click Finish
 
 
 
 
 
Once the class is created, you should:
 
  1. Add the code extends SurfaceView implements Runnable after the class name
  2. Once you do this, you'll receive errors, which you can fix automatically by hovering over the errors and selecting the appropriate fix.  This would include the missing class references and method and constructor that are to be implemented. 


The code should look something like the following:

package com.profjpbaugh.gametest1;

import android.content.Context;
import android.view.SurfaceView;

public class CustomView extends SurfaceView implements Runnable
{

 public CustomView(Context context) {
  super(context);
  // TODO Auto-generated constructor stub
 }

 @Override
 public void run() {
  // TODO Auto-generated method stub
  
 }

}
CustomView.java

Adding a Bitmap Asset and Implementing the getBitmapFromFile() method

The major point of this tutorial is to display an image on the canvas.  We can do this in a number of ways.  However, putting images in the drawable directories is not always the best idea.  You cannot make subdirectories in the drawable directories for one, which can cause organizational nightmares.  In my own applications, I typically leave the basic UI components and icons in drawable directories, and put sprites and other game-related objects in the assets directory (or subdirectories.)

But, getting the images out of the assets directory is not as nice as just being able to reference them easily, as with drawables they are indexed and given identifiers in the R.java file.

First thing is to add the bitmap to the assets directory.
  1. Create an image in a paint program
  2. Save it to the assets directory inside of your Eclipse project
I created a file named mr_stickman.png in Adobe Photoshop.  I gave it transparency by creating a new Layer, and deleting the background.  When I saved it as a .png file, it was automatically created with the transparency information in it.



mr_stickman.png - my glorious work of art


Then refresh your navigation pane from inside Eclipse
  1. From inside Eclipse, right click assets directory
  2. Choose Refresh
  3. Expand the assets directory and you should see the image


The above screenshot shows our mr_stickman.png file sitting in the folder.

The next thing we need to do is implement a method that can read the data from the assets directory, and create a Bitmap from it, and allow us to capture the image.  The entire code is as follows:

package com.profjpbaugh.gametest1;

import java.io.IOException;
import java.io.InputStream;

import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.view.SurfaceView;



public class CustomView extends SurfaceView implements Runnable
{
 private AssetManager assets = null;
 
 public CustomView(Context context) {
  super(context);
  assets = context.getAssets();
 }

 @Override
 public void run() {
  // TODO Auto-generated method stub
  
 }
 
 
 
 private Bitmap getBitmapFromFile(String fileName)
 {
  Bitmap bmp = null;
  
  Config config = Config.ARGB_8888;
  
  Options options = new Options();
  options.inPreferredConfig = config;
  
  InputStream inputStream = null;
  
  try
  {
   inputStream = assets.open(fileName);
   bmp = BitmapFactory.decodeStream(inputStream);
   if(bmp == null)
   {
    throw new RuntimeException("Couldn't load bitmap from asset '" + fileName + "'");
   }
   
  }
  catch(IOException e)
  {
   throw new RuntimeException("Couldn't load bitmap from asset '" + fileName + "'");
  }
  finally
  {
   if(inputStream != null)
   {
    try
    {
     inputStream.close();
    }
    catch(IOException e)
    {
     //do nothing
    }
   }
  }
  return bmp;
 }//end getBitmapFromFile

}
If you just cut and paste the code without the imports at the top, you're going to get a bunch of unresolved errors.  Just hover over each of them and correct them.  For example, you will need to include (import) the package references for the following:

  • Bitmap
  • Config
  • Options
  • InputStream
  • BitmapFactory
  • IOException
If you copy and paste the entire code above, then the only thing you'll need to change is the package name at the top.  For example, change com.profjpbaugh.gametest1 to com.somename.gametest1 or whatever you chose to name it.

So, what does the code do?  First, pay attention that I added an AssetManager variable assets to the top and initialized it in the constructor.

As far as the getBitmapFromFile( ) method is concerned, in a nutshell, the inputStream variable reads a byte stream from file (it doesn't really have knowledge of what the data in the fileName represents.  We take that data, and we create a bitmap using the BitmapFactory method decodeStream( ) on the inputStream variable.  If we are successful, we have a beautiful bitmap, which we return to the caller.

So basically, from a "black box" perspective - you give the method getBitmapFromFile( ) the name of the bitmap, and it returns that bitmap in the appropriate Bitmap class format.

Implementing the resume() and pause() methods

Okay, so now we've got our method to load data from a file and convert it to the appropriate format so that we can use it.  Now we have to add a little bit more code to our CustomView class so that it can paint our image to the screen.
 
Firstly, we'll add the following variable declarations to the top of the CustomView class:
  • private SurfaceHolder holder = null;
  • private boolean running = false;
  • private Thread renderThread = null;
  • private Bitmap stickman = null;
The SurfaceHolder is an interface that we will use to gain access to draw to the screen.
The boolean variable running is to keep track as to whether or not the thread of execution is running or not.
The Thread class allows us to create actual threads and start their execution.
The Bitmap class allows us to create variables to hold a compatible image.
 
Once we have the variables declared (and the appropriate imports placed at the top of the file), we can make use of them, initializing them in the constructor.
 
 public CustomView(Context context) {
  super(context);
  
  this.holder = getHolder();
  this.assets = context.getAssets();
  this.stickman = getBitmapFromFile("mr_stickman.png");
 }
 
 Once we've initialized these variables, we can continue to work by adding the resume() and pause() methods, which will be called from our MainActivity's onResume( ) and onPause( ) methods, respectively.

Below the run( ) method, add the following code:
 
 
public void resume()
 {
  running = true;
  renderThread = new Thread(this);
  renderThread.start();
 }
 

 public void pause()
 {
  running = false;
  while(true)
  {
   try
   {
    renderThread.join();
    break;
   }
   catch(InterruptedException e)
   {
    
   }
  }//end while(true)
 }
 
The resume( ) method essentially just sets the running variable to true, then creates and starts a new thread (renderThread).
The pause( ) method sets the running to false, and then attempts to perform a join( ) and continue execution.   
 
Once these are implemented, we can move back to our run ( ) method and implement that.
 

Implementing the run( ) method

 Now, we see where the real magic happens.  The run( ) method is called when the thread starts.  As long as our running variable is true, we continue running the thread.

 @Override
 public void run() {
  while(running)
  {
   synchronized(this)
   {
    if(!holder.getSurface().isValid())
    {
     continue;
    }//end if isValid() call
    
    Canvas canvas = holder.lockCanvas();
    
    canvas.drawBitmap(stickman, 0, 0, null);
    holder.unlockCanvasAndPost(canvas);
   }
  }//end while
  
 }


Notice that here, we test to see if the surface is valid, and if it is not we continue (in other words, go to the next iteration of the loop, skipping what is below the continue statement.)  As long as the surface is valid, we lock the canvas for drawing using our SurfaceHolder interface, then draw the bitmap using the drawBitmap( ) method.  In this particular version of drawBitmap( ), the parameters are as follows:

  • Bitmap bitmap - the bitmap to be drawn
  • int x - x coordinate of the upper left corner of the image
  • int y - y coordinate of the upper left corner of the image
  • Paint paint - the paint object to use to paint the bitmap.  We can just leave this null.
When we're done drawing, we unlock the canvas and post a message indicating this fact.


The MainActivity Class updates

In the MainActivity class, we must:

  • Create an instance of the CustomView class
  • Set that instance as the Content View for this activity
  • Call the pause() method of the CustomView class from onPause()
  • Call the resume() method of the CustomView class from onResume()
The full code for the final MainActivity class is as follows:

package com.profjpbaugh.gametest1;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;

public class MainActivity extends Activity {

 private CustomView cv = null;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        cv = new CustomView(this);
        setContentView(cv);
    }

    @Override
    public void onResume()
    {
     super.onResume();
     cv.resume();
     
    }
    
    @Override
    public void onPause()
    {
     super.onPause();
     cv.pause();
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }
      
}





And the full code for the CustomView class is as follows:

package com.profjpbaugh.gametest1;

import java.io.IOException;
import java.io.InputStream;

import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Canvas;
import android.view.SurfaceHolder;
import android.view.SurfaceView;



public class CustomView extends SurfaceView implements Runnable
{
 private AssetManager assets = null;
 private SurfaceHolder holder = null;
 private boolean running = false;
 private Thread renderThread = null;
 private Bitmap stickman = null;
 
 public CustomView(Context context) {
  super(context);
  
  this.holder = getHolder();
  this.assets = context.getAssets();
  this.stickman = getBitmapFromFile("mr_stickman.png");
 }

 @Override
 public void run() {
  while(running)
  {
   synchronized(this)
   {
    if(!holder.getSurface().isValid())
    {
     continue;
    }//end if isValid() call
    
    Canvas canvas = holder.lockCanvas();
    
    canvas.drawBitmap(stickman, 0, 0, null);
    holder.unlockCanvasAndPost(canvas);
   }
  }//end while
  
 }

 public void resume()
 {
  running = true;
  renderThread = new Thread(this);
  renderThread.start();
 }
 
 public void pause()
 {
  running = false;
  while(true)
  {
   try
   {
    renderThread.join();
    break;
   }
   catch(InterruptedException e)
   {
    
   }
  }//end while(true)
 }
 
 
 private Bitmap getBitmapFromFile(String fileName)
 {
  Bitmap bmp = null;
  
  Config config = Config.ARGB_8888;
  
  Options options = new Options();
  options.inPreferredConfig = config;
  
  InputStream inputStream = null;
  
  try
  {
   inputStream = assets.open(fileName);
   bmp = BitmapFactory.decodeStream(inputStream);
   if(bmp == null)
   {
    throw new RuntimeException("Couldn't load bitmap from asset '" + fileName + "'");
   }
   
  }
  catch(IOException e)
  {
   throw new RuntimeException("Couldn't load bitmap from asset '" + fileName + "'");
  }
  finally
  {
   if(inputStream != null)
   {
    try
    {
     inputStream.close();
    }
    catch(IOException e)
    {
     //do nothing
    }
   }
  }
  return bmp;
 }//end getBitmapFromFile

}


If you run this project as an Android Application, you should see something like the following:



Closing Remarks

In this tutorial, we went over the basics of displaying an image using the canvas.  In brief, we implemented a custom called CustomView, which inherits from (extends) the SurfaceView class.  This class also implements the Runnable interface, which is involved in threading.

Once we created the CustomView class, we created an instance of it in the MainActivity class, and set it as the content View of the MainActivity.  Additionally, we called the CustomView's pause() and resume() methods from the MainActivity's onPause( ) and onResume( ) activities, respectively.

No comments:

Post a Comment