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
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).
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.
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.
Much of the preceding code is similar to that for Animator Demo and similar explanation applies. The essential new things are
Let's now see what this program does.
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. |