Previous  | Next  | Home

Animator Demo II


 

In this project we shall illustrate a second method of running an animation on a thread separate from the main UI thread. Again we will animate an idealized Solar System with circular orbits for planets as in the Animator Demo example, but now we will allow the motion of all 8 planets (Pluto is no longer considered the 9th planet). Much of the basic technique will be similar to the Animator Demo example, except that

  1. In this case we will subclass Thread to define our animation thread (creating an inner class within our main display class), rather than invoking the Runnable interface.

  2. We will update the view by sending messages from our animation thread to a Handler on the UI thread that will issue an invalidate() to cause the screen to redraw.

In this project, as in the previous Animation Demo, the screen display will be created entirely by Java classes that we will write, so it makes sense to lay the display out entirely in code (main.xml will not play a role in this project).

 

Setting Up the Project

Open Eclipse and set up the following project:

Now we create the Java class that will run the animation, and then modify AnimatorDemo2.java to use this new class as the screen layout.

 

The Main Animation Class

Create a new class file MotionRunner2.java, having it extend View. Then edit this file to read as follows.


package com.lightcone.animatordemo2;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;

public class MotionRunner2 extends View {
	
    // Class constants defining state of the thread
    static final int DONE = 0;
    static final int RUNNING = 1;

    static final int ORBIT_COLOR = Color.argb(255, 66, 66, 66);
    static final int nsteps = 600;                 // Number animation steps around circle
    static final int planetRadius = 3;             // Radius of spherical planet (pixels)
    static final int sunRadius = 4;                // Radius of Sun (pixels)
    static final float X0 = 0;                     // X offset from center (pixels)
    static final float Y0 = 0;                     // Y offset from center (pixels)
    static final long delay = 20;                  // Milliseconds of delay in the update loop
    static final double RAD_CIRCLE = 2*Math.PI;    // Number radians in a circle
    static final double direction = -1;            // Orbit direction: counter-clockwise -1; clockwise +1
    static final String anim = "ANIM +++++++++++++++++++++++";    // Diagnostic
    static final double fracWidth = 0.97;          // Fraction of screen width to use for display

    
    /* Data for planets.  Note that Pluto is no longer considered a planet, so there are only
      eight planets in the list.  The semimajor elliptical axis is denoted by a and is 
      in units of astronomical units (AU), the eccentricity epsilon is dimensionless, the period is 
      in years, and theta0 (initial angle) is in radians (there are 57.3 degrees per radian), with 
      clockwise positive and measured from the 12-o'clock position.  For circular orbits (the
      approximation we will use in this example) the radius of the planetary orbit is equal to the
      semimajor axis a and the eccentricity epsilon plays no role.*/    
    
    static final double epsilon[] = {0.206,0.007,0.017,0.093,0.048,0.056,0.047,0.009};
    static final double  a[] = {0.387,0.723,1.0,1.524,5.203,9.54,19.18,30.06};
    static final double period[] = {0.241,0.615,1.0,1.881,11.86,29.46,84.01,164.8};
    static final double theta0[] = {5.2, 1.8, 1.4, 3.6, 1.6,4.5,1.6,2.4};
    
    private int numPlanets;                     // Number of planets to include
    private AnimationThread animThread;         // The animation thread
    private Paint paint;                        // Paint object controlling format of screen draws
    private ShapeDrawable planet;               // Planet symbol
    private float X[];                          // Current X position of planet (pixels)
    private float Y[];                          // Current Y position of planet (pixels)
    private float centerX;                      // X for center of display (pixels)
    private float centerY;                      // Y for center of display (pixels)
    private float R0[];                         // Radius of planetary orbit in pixels
    private double theta[];                     // Planet angle (radians clockwise from 12 o'clock)
    private double dTheta[];                    // Angular increment each step (radians)
    private double dTheta0;                     //  Base angular increment each step (radians)
    private double pixelScale;                  // Scale factor: number of pixels per AU
    private double zoomFac = 1.0;               // Zoom factor (relative to 1) for display
	

    public MotionRunner2(Context context) {
        super(context);
        
        numPlanets = 5;                             // Number of planets to animate
        
        // Initialize angle and angle step (in radians) 
        dTheta0 = RAD_CIRCLE/((double) nsteps);     // Angle increment in radians
        
        X = new float[numPlanets];
        Y = new float[numPlanets];
        theta = new double[numPlanets];
        dTheta = new double[numPlanets];
        R0 = new float[numPlanets];
        
        for(int i=0; i<numPlanets; i++){
            dTheta[i] = direction*dTheta0/period[i];
            theta[i] = -direction*theta0[i];
        }
    
        // Define the planet as circular shape
        planet = new ShapeDrawable(new OvalShape());
        planet.getPaint().setColor(Color.WHITE);
        planet.setBounds(0, 0, 2*planetRadius, 2*planetRadius);	
        
        // Set up the Paint object that will control format of screen draws
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setTextSize(14);
        paint.setStrokeWidth(1);
        
        // Create the animation thread
        animThread = new AnimationThread(handler);
        animThread.start();
            
    }
	
    /* Handler on the main (UI) thread that will receive messages from the 
    animation thread and force a redraw of the screen by issuing invalidate().  Note
    that invalidate() can only be called from the thread holding the View.  It can't be
    called directly from the animation thread. */    
	
    final Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            // When we receive a message from the animation thread indicating one
            // time through the animation loop, invalidate the main UI view to force a redraw.
            invalidate();
        }
    };
    
    
    /* The View display size is only available after a certain stage of the layout.  Before then
    the width and height are by default set to zero.  The onSizeChanged method of View
    is called when the size is changed and its arguments give the new and old dimensions.  
    Thus this can be used to get the sizes of the View after it has been laid out (or if the 
    layout changes, as in a switch from portrait to landscape mode, for example). */
	
    @Override
    protected void onSizeChanged  (int w, int h, int oldw, int oldh){
        // Coordinates for center of screen
        centerX = w/2;
        centerY = h/2;
        // Make orbital radius a fraction of minimum of width and height of display and scale
        // by zoomFac
        pixelScale= zoomFac*fracWidth*Math.min(centerX, centerY)/a[numPlanets-1];
        // Set the initial position of the planet (translate by planetRadius so center of planet
        // is at this position)
        for(int i=0; i<numPlanets; i++){
            // Compute R0[] in pixels
            R0[i] = (float)(pixelScale*a[i]);
            X[i] = centerX - R0[i]*(float)Math.sin(theta[i]) - planetRadius ;
            Y[i] = centerY - R0[i]*(float)Math.cos(theta[i]) - planetRadius;
        }
    }
	
	
    /* This method will be called each time the screen is redrawn. The draw is
    on the Canvas object, with formatting controlled by the Paint object. 
    When to redraw is under Android control, but we can request a redraw 
    using the method invalidate() inherited from the View superclass. */
   
   @Override
   public void onDraw(Canvas canvas) {
       super.onDraw(canvas);
       drawBackground(paint, canvas);
       for(int i=0; i<numPlanets; i++){
            canvas.save();
            canvas.translate(X[i] + X0, Y[i] + Y0);
            planet.draw(canvas);
            canvas.restore();
       }
   }
   
   // Called by onDraw to draw the background
   private void drawBackground(Paint paint, Canvas canvas){
        paint.setColor(Color.YELLOW);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(centerX + X0, centerY + Y0, sunRadius, paint);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(ORBIT_COLOR);
        for(int i=0; i<numPlanets; i++){
            canvas.drawCircle(centerX + X0, centerY + Y0, R0[i], paint);
        }
   }
   
   // Stop the thread loop
   public void stopLooper(){
        animThread.setState(DONE);
   }
   
   // Start the thread loop
   public void startLooper(){
        animThread.setState(RUNNING);
   }
    
    
    /*  Inner class that performs animation calculations on a second thread.  Implement
     the thread by subclassing Thread and overriding its run() method.  Also provide
     a setState(state) method to stop the thread gracefully. */    
   
    private class AnimationThread extends Thread {	
        
        Handler mHandler;
        int mState;
       
        // Constructor with an argument that specifies Handler on main thread
        // to which messages will be sent by this thread.
        
        AnimationThread(Handler h) {
            mHandler = h;
        }
        
        /* Override the run() method that will be invoked automatically when 
         the Thread starts.  Do the work required to update the animation on this
         thread but send a message to the Handler on the main UI thread to actually
         change the visual representation by calling invalidate() on the View.  */ 
        
        @Override
        public void run() {
            mState = RUNNING;   
            while (mState == RUNNING) {
            	newXY();
            	// The method Thread.sleep throws an InterruptedException if Thread.interrupt() 
            	// were to be issued while thread is sleeping; the exception must be caught.
                try {
                    // Control speed of update (but precision of delay not guaranteed)
                    Thread.sleep(delay);
                } catch (InterruptedException e) {
                    Log.e("ERROR", "Thread was Interrupted");
                }
                
                /*  Send message to Handler on UI thread so that it can update the screen display.
                 We could also send a data bundle with the message (see the ProgressBarExample)
                 but there is no need in this case since the coordinates to be plotted (X and Y) are 
                 defined in the enclosing class and thus can be changed directly from this inner 
                 class. */ 
                
                Message msg = mHandler.obtainMessage();
                mHandler.sendMessage(msg);
            }
        }
    	
    	/* Method to increment angle theta and compute the new X and Y .  The orbits of the
         planets are actually ellipses with the Sun at one focus, but for this example we
         approximate them as circles with the Sun at the center but with the correct periods.
         The constant distance from the Sun is set to the semimajor axis a[i], which is the
         average separation of the planet from the Sun.  Only Mercury has  significant 
         eccentricity; the orbits for the other planets are very nearly circles with the Sun at 
         the center. */     
        
    	private void newXY(){
            for(int i=0; i<numPlanets; i++){
                theta[i] += dTheta[i];     
                X[i] =  (float)(R0[i]*Math.sin(theta[i])) + centerX - planetRadius;
                Y[i] =  centerY - (float)(R0[i]*Math.cos(theta[i])) - planetRadius;
            }
    	}
        
        // Set current state of thread (use state=AnimationThread.DONE to stop thread)
        public void setState(int state) {
            mState = state;
        }
    }
}

Then edit the file AnimatorDemo2.java to create an instance mrunner of the MotionRunner2 class and set it as the display for the main screen (the only screen for this example).


package com.lightcone.animatordemo2;

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

public class AnimatorDemo2 extends Activity {
	
    MotionRunner2 mrunner;
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Instantiate the class MotionRunner to define the entry screen display
        mrunner = new MotionRunner2(this);
        mrunner.setLayoutParams(new ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT));
        setContentView(mrunner);
    }
    
    @Override
	public void onPause() {
            super.onPause();
            // Stop animation loop if going into background
            mrunner.stopLooper();
    }
    
    @Override
	public void onResume() {
            super.onResume();
            // Resume animation loop
            mrunner.startLooper();
    }  
}

In the following section we describe how this code works.

 

Discussion of Code Functionality

Much of the preceding code is similar to that for Animator Demo and similar explanation applies. The essential new things are

  1. Since we are animating the motion of as many as 8 objects (the number is set by the variable numPlanets), we have to define arrays and loop over them to move each individual object in each timestep. But for each object the procedure is similar to that for the single object in Animator Demo.

  2. We define a variable pixelScale as the scaling factor from physical distances (astronomical units or AU) to pixels on the screen, and we set its value automatically so that the orbits of all planets chosen to animate will fit on the screen.

  3. The inner class AnimationThread, which subclasses Thread, overriding its run() method to create a thread with an animation loop very similar to that in Animator Demo (where did essentially the same thing by overriding the run() method of the Runnable interface).

  4. As for the Animator Demo example, the animation thread is separate from the main thread from which the view was launched, so we cannot call invalidate() on the view directly. In this case, instead of calling postInvalidate(), which requests a screen redraw at a later time, we define a Handler instance handler, send messages to it from the animation thread, and have handler issue the invalidation() request on the UI thread. We already used this approach in the Progress Bar Example, so it should be familiar. In the present example, since the inner class has access to the methods and fields (even private ones) of the enclosing class and can update them directly, we don't even need to send any data with the message.

Let's now see what this program does.

 

Run Planet(s), Run!

The following figure shows a snapshot of the motion for the 5 innermost planets of the Solar System assuming circular motion.



In the Solar System project we shall extend this program to become much more realistic, putting the planets on elliptical orbits governed by Kepler's laws, adding orbits for some asteroids, comets, and dwarf planets, and permitting retrograde (opposite sense of Earth) orbital motion.


The complete project for the application described above is archived at the link AnimatorDemo2.


Previous  | Next  | Home