Previous  | Next  | Home

Animator Demo I


 

For many applications, particularly those that are scientific or educational in nature, it is important to be able to simulate precise motion of objects governed by mathematical equations. In this project we introduce the basics of how to use a Java thread to implement motion of objects on the screen governed by mathematical models. For an overview of graphics methods discussed here, see the Android Graphics document.

 

Methods for 2D Animation

We have several options for 2D graphical animation in Android, varying in complexity and speed:

  1. The simplest but least powerful and least flexible option is to draw graphics or animations into a View object from your layout. In this approach you only have to define the graphics that go into the View and the drawing (and possible simple animation) of your graphics can be handled by Android's normal View hierarchy drawing process. This method is only adequate for fixed images, or simple pre-defined animations. We won't discuss it further here, but for a basic introduction see Simple Graphics inside a View and 2D Graphics. For specific discussion of simple fixed animation that can be implemented in this way see Tween Animation and Frame Animation.

    A tweened animation can perform a series of simple position, size, rotation, and transparency transformations on the contents of a View object. A frame animation is a "traditional" animation in that is corresponds to displaying a sequence of related images ("flip-book animation"). Both of these kinds of animation can be implemented either in code or in XML resources, but often it is simplest to do it entirely within XML.


  2. For any animation that regularly needs to redraw itself a much better animation choice is to draw directly to a Canvas. Games, and the sort of scientific animation we are addressing here, are likely to fall into this category. A Canvas serves as an interface to the actual surface upon which your graphics will be drawn. It holds all of your draw calls, but display of the drawing is performed (through the agency of the Canvas) upon an underlying Bitmap that is placed into the window by the system at the appropriate time. There are several ways to do this, differing primarily in whether the Canvas is obtained and managed by you or by the system, and whether the drawing operations take place on the main thread holding the View or on a separate thread.

Let's now implement these ideas in some basic 2D animation. The example that we choose is to animate the motion of a planet around the Sun, assuming the planet to be in a circular orbit around a fixed Sun. The method that we shall use for this illustration is to implement the animation update on a dedicated thread and use postInvalidate() from that thread to force periodic screen updates.

 

Creating the Initial Project

Create a new project in Eclipse with the following specification:

 

Thread Options

As discussed in the Progress Bar Example, there are two basic ways to implement threading:

  1. Create a new class that extends Thread and override its run() method.

  2. Provide a new Thread instance with a Runnable object using the Runnable interface during its creation.

In the Progress Bar Example we used the first approach and created a new class extending Thread. In this example we shall use the second approach and implement thread-based animation by creating a class that implements the Runnable interface.

 

Implementing an Animation Thread Using Runnable

Create a new public class MotionRunner.java, having it subclass View and implement Runnable: in Eclipse, File > New > Class, and then fill out the resulting screen as follows (substituting your namespace for com.lightcone)



and click Finish. Open MotionRunner.java in the Eclipse editor, which should indicate an error: Eclipse underlines MotionRunner and hovering over it with the mouse gives the error message "Implicit super constructor View() is undefined for default constructor. Must define explicit constructor." Let Eclipse fix it by choosing "Add constructor 'MotionRunner(Context)'" from the options in the popup window. Then MotionRunner.java should read


package com.lightcone.animatordemo;
import android.content.Context;
import android.view.View;

public class MotionRunner extends View implements Runnable {

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

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

This should compile with no errors. Now edit MotionRunner.java so that it reads


    package com.lightcone.animatordemo;
    
    import android.content.Context;
    import android.util.Log;
    import android.view.View;
    
    public class MotionRunner extends View implements Runnable {
           
        private Thread animator = null;      // The thread that will hold the animation
        private long delay;                  // Delay in ms controlling speed of thread looping
        private boolean please_stop = false; // Boolean controlling whether thread loop is running
        
        public MotionRunner(Context context) {
            super(context);
        }
    
        @Override
        public void run() {
            while(!please_stop) {
                Log.i("ANIMATOR","  ..... LOOPED");
                // Wait then execute it again
                try { Thread.sleep(delay); } catch (InterruptedException e) { ; }
            }	
        }
        
        // Method to start animation loop
        public void startIt(long delay) {
            this.delay = delay;
            animator = new Thread(this);
            animator.start();
        }
        
        // Method to stop animation loop
        public void stopLooper(){
            please_stop = true;
        }
        
        // Method to resume animation loop
        public void startLooper(long delay){
            please_stop = false;
            if(animator == null) {
                startIt(delay);
            }
        }    
    }

where changes are highlighted in red. Let's describe briefly the functionality of this code.

Now we modify AnimatorDemo.java to run the animation thread.

 

Running the Animation Thread

Open AnimatorDemo.java in the Eclipse editor and modify it to read


    package com.lightcone.animatordemo;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.ViewGroup;
    
    public class AnimatorDemo extends Activity  {
    
        private long delay = 40;      // Delay in ms controlling speed of thread looping
        MotionRunner 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 MotionRunner(this);
            mrunner.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                    ViewGroup.LayoutParams.FILL_PARENT));
            setContentView(mrunner);
            mrunner.startIt(delay);
        }
    
        @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(delay);
        }
    }

where changes are highlighted in red. This segment of code

If you now execute this app on a phone or emulator you should see a blank screen (since we haven't told it to display anything yet), but in the logcat output there should be a sequence of "LOOPED" strings indicating that the while-loop in the thread is executing. By changing the value of the variable delay you should be able to approximately control how often this string is output. You also should find that the loop stops if you send the app to the background by hitting the back button or the home button.

 

Adding Animated Motion

We now have a basic animation thread running, so lets animate something with it by using the methods that mrunner inherits from View to implement the motion of objects on the screen.

 

Geometry of the Planetary Orbit

The geometry that we shall assume for the planetary orbit is illustrated in the following figure,



where we shall position the Sun at the center of the display screen and take it as the origin of the coordinate system. The animation will consist of moving the planet by a small increment Δθ on the circular orbit each time through the while-loop of the run() method, with the size of the increment &Delta&theta controlled by the integer steps and the clockwise direction corresponding to positive &Delta&theta increments. We implement this through the following additions and modifications.

 

New Imports and Variables

First, add the imports and new variables indicated in red in the following listing to MotionRunner.java.


    package com.lightcone.animatordemo;
    import android.content.Context;
    import android.util.Log;
    import android.view.View;
    
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.graphics.drawable.ShapeDrawable;
    import android.graphics.drawable.shapes.OvalShape;
    
    public class MotionRunner extends View implements Runnable {
        private Thread animator = null;              // The thread that will hold the animation
        private long delay;                          // Delay in ms controlling speed of thread looping
        private boolean please_stop = false;         // Boolean controlling whether thread loop is running
    	
        static final int ORBIT_COLOR = Color.argb(255, 66, 66, 66);
        static final double RAD_CIRCLE = 2*Math.PI;    // Number radians in a circle
        private Paint paint;                           // Paint object controlling format of screen draws
        private ShapeDrawable planet;                  // Planet symbol
        private int planetRadius = 7;                  // Radius of spherical planet (pixels)
        private int sunRadius = 12;                    // Radius of Sun (pixels)
        private float X0 = 0;                          // X offset from center (pixels)
        private float Y0 = 0;                          // Y offset from center (pixels)
        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 circular orbit (pixels)
        private int nsteps = 600;                      // Number animation steps around circle
        private double theta;                          // Angle around orbit (radians)
        private double dTheta;                         // Angular increment each step (radians)
        private double direction = -1;                 // Direction: counter-clockwise -1; clockwise +1


where the purpose of each variable is defined in the comments.

 

Setup and Initialization in the Constructor

Next, add to the constructor of MotionRunner.java the statements in red in the following listing.


    public MotionRunner(Context context) {
        super(context);
        // Initialize angle and angle step (in radians) 
        theta = 0;
        dTheta = RAD_CIRCLE/((double) nsteps);     // Angle increment in radians
    
        // 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);
    }

where the purpose of the statements should be clear from the comments and the documentation for the methods of the classes ShapeDrawable and Paint.

 

Getting the Device Screen Size

Since we will need to know the display size of the view to calculate geometries, let's get the height and width of the full display. We do that by overriding the View method onSizeChanged(int w, int h, int oldw, int oldh), adding to MotionRunner


    /*
    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
        R0 = (float) (0.90*Math.min(centerX, centerY));
        // Set the initial position of the planet (translate by planetRadius so center of planet
        // is at this position)
        X = centerX - planetRadius ;
        Y = centerY - R0 - planetRadius;
    }

You might wonder why we use this method to get the view height and width. Why not just call the View methods getWidth() and getHeight() in the constructor, for example? Recall that we are generally using layouts that adapt to the device in use, so the geometry is known only at runtime. Furthermore, the screen geometry may change during execution if, for example, the phone is switched from portrait to landscape display by sliding out a keyboard.


The qualifier protected required for the method onSizeChanged is an access-level modifier in Java (an alternative to public, private, or the default package-level access if no qualifier is specified). A protected method is visible to all classes in the package and to classes outside the package that inherit the class. In writing your own classes, there are various (often strongly-held) views on declaring access levels. There is fairly common agreement that it is best to restrict access to methods and fields as much as possible. One approach to this is to
  1. Use the private modifier (not accessible in subclasses or other classes of the package) if the method or field is needed only within the class. Things that should not be changed if the class is subclassed should be declared private, for example.

  2. Use the protected modifier (accessible in subclasses and other classes of the package) if there are cooperating classes within a single package that may need access to it, or you expect that the methods and variables of the class may be useful to someone extending the class.

  3. Otherwise use the public modifier (accessible by all classes) for methods and fields that may be of direct interest to users of the class.
The preceding considerations are motivated primarily by principles of good object-oriented design. However, note that in Java generally, and Android in particular, there also may be some performance issues associated with choice of access qualifiers, as described in Designing for Performance. Of course we have no choice in this particular example: onSizeChanged is an Android method that must be declared protected or it won't compile.

 

Method to Increment the Angle and Compute New Position Coordinates

Next we add a method newXY() that will increment θ and compute the corresponding screen coordinates X and Y for the planet. Add to MotionRunner.java the following code:


    // Method to increment theta and compute the new X and Y .
    private void newXY(){
        theta += dTheta;     
        if(theta > RAD_CIRCLE) theta -= RAD_CIRCLE;  // For convenience, keep angle 0-2pi
        X =  (float)(R0*Math.sin(direction*theta)) + centerX - planetRadius;
        Y =  centerY - (float)(R0*Math.cos(direction*theta)) - planetRadius;
        Log.i("ANIMATOR", "X="+X+" Y="+Y);
    }

Notice the use of the variable direction to control whether the motion is clockwise or counterclockwise, and that for convenience we have implemented some logic so that at any time in the animation the angle (which is specified in radians in the calculation, since the trigonometric methods expect radians for the angles) lies in the interval 0 to 2π

 

Modifications of the run() Method

Now we modify run() to invoke the newXY() method that increments the position of the planet. The required changes are indicated in red in the following listing.


@Override
public void run() {
    while(!please_stop) {
        // Move planet by dTheta and compute new X and Y
        newXY();
        // Must use postInvalidate() rather than invalidate() to request redraw since
        // this is invoked from different thread than the one that created the View
        postInvalidate();
        // Wait, then execute it again
        try { Thread.sleep(delay); } catch (InterruptedException e) { ; }
    }	
}

Note that the View method postInvalidate(), which causes the invalidate to happen on a subsequent cycle through the event loop, must be used rather than invalidate() to request a redraw from a thread other than the one that created the View (see the discussion above and in the Android Graphics document).

 

Overriding the onDraw(Canvas canvas) Method

Finally, we override the onDraw(Canvas canvas) method inherited from View to reflect the updated position of the planet when the screen is redrawn in response to the postInvalidate() request in run(). To do so, add the following two methods to MotionRunner.java.


    /*
    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() or postInvalidate() inherited from the View superclass.
    In this case we must use postInvalidate(), since we are updating on a thread separate
    from the main UI thread.
    */
    
    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBackground(paint, canvas);
        canvas.save();
        canvas.translate(X + X0, Y + 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);
        canvas.drawCircle(centerX + X0, centerY + Y0, R0, paint);
    }

The techniques used here are documented under the methods for the Android classes Canvas, Paint, and Color, and are similar to those already discussed in conjunction with the onDraw method in the DraggableSymbols project. Notice that the Canvas on which were are drawing is supplied to us as the argument canvas, so we don't have to manage the Canvas; we only have to draw on it. The formatting of our drawing is controlled by the Paint object paint that we initialized in the MotionRunner constructor.

 

Run Planet, Run!

If you now compile this code and run it on an emulator or phone you should see the planet moving around the Sun in a circle, as in the following figure (a screenshot taken on a Samsung Galaxy S phone in horizontal mode).





If you look at the logcat output you should see a steady stream of X and Y position updates because of the Log.i() statement that we inserted, and if you hit the back or home buttons the screen animation and this stream of position updates should halt, indicating that the onPause() method of AnimatorDemo has stopped execution of the update thread by invoking the stopLooper() method of MotionRunner.


With the integer direction set to -1, the motion is counterclockwise; change it to +1 to give clockwise motion. The speed and smoothness of the animation are controlled by the parameters nsteps, which specifies the number of animation steps to take each time around the circle, and delay, which specifies the delay in milliseconds for each animation step.


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


Previous  | Next  | Home