Android Logo MathCS.org - Android

Animating a View with a Thread

java -> android -> Animating a View ...

In this section we will see how to use a thread to animate the circle we created in our previous section. First, here is the code we will start with:

Class Game

public class Game extends Activity 
{
   private GameArena canvas;

   public void onCreate(Bundle savedInstanceState) 
   {
      super.onCreate(savedInstanceState);
      requestWindowFeature(Window.FEATURE_NO_TITLE);

      canvas = new GameArena(this);
      setContentView(canvas);
   }
}

Class GameArena

public class GameArena extends View
{
   private MyCircle circle = null;

   public GameArena(Context context)
   {
      super(context);
      circle = new MyCircle(100, 150);
   }

   public void onDraw(Canvas canvas) 
   {
   canvas.drawColor(Color.WHITE);
   circle.draw(canvas);
   }
}

Class MyCircle

public class MyCircle
{
   private int x;
   private int y;
   private int radius;

   private Paint paintBrushBlue = null;
   private Paint paintBrushGreen = null;

   public MyCircle(int x, int y)
   {
      super();

      this.x = x;
      this.y = y;
      this.radius = 50;

      paintBrushBlue = new Paint();
      paintBrushBlue.setColor(Color.BLUE);
      paintBrushBlue.setAntiAlias(true);
      paintBrushBlue.setDither(true);

      paintBrushGreen = new Paint();
      paintBrushGreen.setColor(Color.GREEN);
      paintBrushGreen.setAntiAlias(true);
      paintBrushGreen.setDither(true);
   }

   public void setCenterTo(int x, int y)
   {
      this.x = x;
      this.y = y;
   }

   public void draw(Canvas canvas)
   {
      canvas.drawCircle(x, y, radius, paintBrushBlue);
      canvas.drawCircle(x, y, radius/2, paintBrushGreen);
   }
}

To animate the circle, we will use a thread that changes the (x,y) coordinates of the circle and causes the onDraw method to execute. Since the coordinates of the circle have changed, it will appear at a new location, appearing to move.

We could use the Runnable interface in the GameArena but for simplicity - and for job separation - we create a new class that extends Thread. Recall that the Thread class start a new execution thread that runs parallel to any other activity. The main method of a thread is its run method, which executes once the thread is instantiated and its start method is called. You generally override the run method, placing the code you want the thread to execute into the overridden method, often in a loop. To properly start and stop a thread, you should add appropriate methods. Here is a possible framework for most classes that extend Thread:

public class GameThread extends Thread
{
   private final static int SLEEP_TIME = 100;

   private boolean running = false;

   public GameThread()
   {
      super();
   }

   public void startThread()
   {
      running = true;
      super.start();
   }

   public void stopThread()
   {
      running = false;
   }

   public void run()
   {
      while (running)
      {
         try
         {
            // code to execute goes here
            sleep(SLEEP_TIME);
         }
         catch(InterruptedException ie)
         { 
         }
      }
   }
}

Note that programming with multiple threads is, generally, pretty tricky. You need to ensure that a thread only executes when necessary and stops when possible to avoid unnecessary power consumption, which could be especially bad for mobile apps. For example, whenever an app is put into the background and its screen is invisible, any threads should be put on hold: it would be foolish to have a thread update an animation that nobody can see. Also, you need to be careful not to manipulate data via a user interface item at the same time a thread is accessing the same object, otherwise your app might crash due to synchronization problems. For example, if a thread needs to iterate over an array of objects to display, you need to be careful not to add or delete items from the list as the thread is iterating over it.

But we will take care of these problems so let's return to our code. Generally, a thread needs to call methods of the object it needs to control, so we need to add a reference to that object as the thread is constructed. In our case, we add a field to the thread class and initialize it as follows:

 private final static int SLEEP_TIME = 100;

private boolean running = false;
private GameArena canvas = null;

public GameThread(GameArena canvas)
{
   super();
   this.canvas = canvas;
}

Now our thread could utilize methods of the GameArena to move the circle, except there are none such methods (yet)! Thus, we first add a simple move method to the GameArena as follows:

public void moveCircle()
{
   circle.setCenterTo(circle.getX()+1, circle.getY()+1);
}

It requires us to implement getX and getY methods in the MyCircle class as follows:

public int getX()
{
   return x;
}

public int getY()
{
   return y;
}

With these in place we can modify the run method of GameThread, which just got a reference to the arena a few minutes ago:

public void run()
{
   while (running)
   {
      try
      {
         canvas.moveCircle();
         sleep(SLEEP_TIME);
      }
      catch(InterruptedException ie)
      { 
      } 
   }
}

All that's left to do, it seems, is to start the thread. Once started, it will by itself through its run method, change the coordinates of the circle, causing it to move. We therefore modify the constructor of GameArena as follows:

public GameArena(Context context)
{
   super(context);
   circle = new MyCircle(100, 150);

   thread = new GameThread(this);
   thread.startThread();
}

Everything should be in place now. Yet when the app executes, the circle does not move at all - what is wrong?

What' wrong is that everything is actually going according to plan, except nobody is calling the onDraw method! Thus, while the thread diligently updates the circle's coordinates, the circle never gets redrawn because nobody calls onDraw! We are wasting computing cycles - and battery power - by running a thread that causes no screen updates! It seems easy to fix: just add a call to onDraw to the thread's run method. However, the onDraw method requires a Canvas as input and the thread does not have access to one!

The solution is that every view inherits a postInvalidate() method. That method causes a cascade of internal calls, including a call to the onDraw method with an appropriate canvas as input. Thus, we call postInvalidate() in the thread's run method, which will cause, in turn, the onDraw method to be called. A good side effect of postInvalidate is that it automatically takes care of potential synchronization problems (a bad - and potentially prohibitive - side effect is that using this method is slow):

public void run()
{
   while (running)
   {
      try
      {
         canvas.moveCircle();
         canvas.postInvalidate();
         sleep(SLEEP_TIME);
      }
      catch(InterruptedException ie)
      { 
      }
   }
}

All seems good. When we run the app, the circle slowly moves towards the lower right. It eventually disappears, which we will fix by adding some code to test when it reaches the screen boundaries, but for now we have another problem we need to address first. Try this:

Recall that tapping HOME switches the current app to the background but does not stop it. The circle has moved while our app was in the background! In fact, we started a thread but we're never stopping it. Thus it will continue executing even if our app has moved to the background - not good!

Once we realize the problem we can fix it pretty quickly. Recall that when an app moves to the background, the onPause method is called, and when it is brought back to the front, onResume is called. Thus, our strategy is as follows:

For completeness, here is the full code listing. Everything should work perfectly now (we hope). Note that we removed the call to start the thread from the GameArena constructor. Instead, when the activity starts, even initially, its onResume method is called, which in turn calls startGame, which in turn starts the thread, which in turn moves the circle.

class Game

public class Game extends Activity 
{
    private GameArena canvas;
	
    public void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
           
        canvas = new GameArena(this);
        setContentView(canvas);
    }
    
    public void onPause()
    {
    	super.onPause();
    	canvas.stopGame();
    }

    public void onResume()
    {
    	super.onResume();
    	canvas.startGame();    	
    }
}
		

class GameArena

public class GameArena extends View
{
	private MyCircle circle = null;
	private GameThread thread = null;
	
	public GameArena(Context context)
	{
		super(context);
		circle = new MyCircle(100, 150);
	}

	public void startGame()
	{
		if (thread == null)
		{
			thread = new GameThread(this);
			thread.startThread();
		}
	}
	
	public void stopGame()
	{
		if (thread != null)
		{
			thread.stopThread();

			// Waiting for the thread to die by calling thread.join,
			// repeatedly if necessary
			boolean retry = true;
			while (retry)
			{
				try
				{
					thread.join();
					retry = false;
				} 
				catch (InterruptedException e)
				{
				}
			}
			thread = null;
		}
	}
	
	public void moveCircle()
	{
		circle.setCenterTo(circle.getX()+1, circle.getY()+1);
	}
	
	public void onDraw(Canvas canvas) 
    	{
		canvas.drawColor(Color.WHITE);
	   	circle.draw(canvas);
    	}
}		
		

class GameThread

public class GameThread extends Thread
{
	private final static int SLEEP_TIME = 100;
	
	private boolean running = false;
	private GameArena canvas = null;

	public GameThread(GameArena canvas)
	{
		super();
		this.canvas = canvas;
	}
	
	public void startThread()
	{
		running = true;
		super.start();
	}
	
	public void stopThread()
	{
		running = false;
	}

	public void run()
	{
		while (running)
		{
			try
			{
				canvas.moveCircle();
				canvas.postInvalidate();
				sleep(SLEEP_TIME);
			}
			catch(InterruptedException ie)
			{			
			}
		}
	}
}		
		

class myCircle

public class MyCircle
{
	private int x;
	private int y;
	private int radius;
	
	private Paint paintBrushBlue = null;
	private Paint paintBrushGreen = null;
	
	public MyCircle(int x, int y)
	{
		super();

		this.x = x;
		this.y = y;
		this.radius = 50;

		paintBrushBlue = new Paint();
		paintBrushBlue.setColor(Color.BLUE);
		paintBrushBlue.setAntiAlias(true);
		paintBrushBlue.setDither(true);

		paintBrushGreen = new Paint();
		paintBrushGreen.setColor(Color.GREEN);
		paintBrushGreen.setAntiAlias(true);
		paintBrushGreen.setDither(true);
	}

	public void setCenterTo(int x, int y)
	{
		this.x = x;
		this.y = y;
	}
	
	public int getX()
	{
		return x;
	}
	
	public int getY()
	{
		return y;
	}
	
	public void draw(Canvas canvas)
	{
		canvas.drawCircle(x, y, radius, paintBrushBlue);
		canvas.drawCircle(x, y, radius/2, paintBrushGreen);
	}
}

Now our animation runs fine and stops properly when the app is moved to the background. The last thing to fix is to make our circle stay inside its window by bouncing off the walls. We move the actual calculations into the MyCircle class by adding variables for the x and y direction and speed. It is all pretty straight-forward, so here is our new and improved class:

Improved class MyCircle

public class MyCircle
{
   private int x;
   private int y;
   private int dx = 1;
   private int dy = 1;
   private int speedX = 1;
   private int speedY = 1;

   private int radius;

   private Paint paintBrushBlue = null;
   private Paint paintBrushGreen = null;

   public MyCircle(int x, int y)
   {
      super();

      this.x = x;
      this.y = y;
      this.radius = 50;

      paintBrushBlue = new Paint();
      paintBrushBlue.setColor(Color.BLUE);
      paintBrushBlue.setAntiAlias(true);
      paintBrushBlue.setDither(true);

      paintBrushGreen = new Paint();
      paintBrushGreen.setColor(Color.GREEN);
      paintBrushGreen.setAntiAlias(true);
      paintBrushGreen.setDither(true);
   }

   public void setCenterTo(int x, int y)
   {
      this.x = x;
      this.y = y;
   }

   public int getX()
   {
      return x;
   }

   public int getY()
   {
      return y;
   }

   public void checkBounds(int width, int height)
   {
      if ((x - radius <= 0) || (x + radius >= width))
         dx *= (-1);
      if ((y - radius <= 0) || (y + radius >= height))
         dy *= (-1);
   }

   public void move()
   {
      x = x + dx * speedX;
      y = y + dy * speedY;
   }

   public void draw(Canvas canvas)
   {
      canvas.drawCircle(x, y, radius, paintBrushBlue);
      canvas.drawCircle(x, y, radius/2, paintBrushGreen);
   }
}

To utilize the class' new capabilities, we'll change the moveCircle method in the GameArena class to the following:

public void moveCircle()
{
   circle.checkBounds(getWidth(), getHeight());
   circle.move();
}

Now our class should be perfect. Slow and boring perhaps, but working properly. We could fix the boring part by adding more objects, collisions, touch controls etc. but before we go all out we should think about the "slow" problem.

We could speed the app up by either decreasing the thread sleep time (or removing it entirely, or replace it by calling yield()instead) or by increasing the speedX and speedY values in MyCircle. However, we have a more basic problem: our approach to use postInvalidate to regenerate the drawing has too much overhead and is inherently too slow for smooth, professional animations.

Thus, before we go and add more objects to our "game" we need to fix this problem by using a SurfaceView instead of a View as our basic drawing class. That is a little more complicated and will be the subject of our next section.