Previous  | Next  | Home

Solar System


 

In this project we extend the Solar System animation introduced in Animator Demo II to a much more realistic model of the Solar System. We will use a similar animation technique but

  1. We use Kepler's laws to describe the motion of the objects in the Solar System (in the limit where the Sun is considered to be much more massive than any other body). Thus the orbits of objects in the Solar system are not circles but are ellipses with the Sun at one focus of the ellipse.

  2. In addition to the planets, we include for variety the dwarf planet Pluto, two Apollo asteroids (ones that cross the Earth's orbit), 2008 VB4, and 2009 FG, and Halley's comet. All of these latter objects have elliptical orbits deviating substantially from that of a circle.

  3. We allow for the possibility of both direct (in the same sense as that of the Earth) and retrograde (opposite sense of that of Earth) orbital motion.

  4. We assume all orbits to lie in the plane of the ecliptic (plane of the Earth's orbit) for animation purposes, but we allow the long axes of the ellipses to be oriented with respect to each other by physically realistic amounts.

  5. We build some user controls in (buttons and touch-screen gestures) to allow some control of animation speed, size scale, and so on.

A significant part of this project uses techniques that have been used and explained in the Progress Bar Example, Animator Demo, and Animation Demo II projects, just in more sophisticated ways. Thus, we will show the full code listing and concentrate our explanation on those things that are new in this implementation.

 

Setting Up the Project

In Eclipse, set up the following project:

where, as usual, you should substitute your namespace for com.lightcone.

 

The Code

The entire project will consist of a single screen that animates motion in the Solar System and the motion on this screen will be implemented by a Java class. Thus, we lay this screen out entirely in Java code (we won't be using main.xml for layout). Create a new class file KeplerRunner.java (extending View) and edit it to read as follows.


    package com.lightcone.solarsystem;

    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;
    import android.view.View.OnLongClickListener;
    import android.view.View.OnClickListener;
    import android.widget.Toast;
    
    public class KeplerRunner extends View implements OnClickListener, OnLongClickListener {
            
        // 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 numpoints = 50;           // number points used to draw orbit as line segments
        // angular increment for orbit straight-line segment
        static final float dphi = (float) (2*math.pi/(float)numpoints);   
        static final double third = 1.0/3.0;
        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 double direction = -1;        // Orbit direction: counter-clockwise -1; clockwise +1
        static final String anim = "ANIM +++++++++++++++++++";    // Diagnostic label
        static final double fracWidth = 0.95;      // Fraction of screen width to use for display
    
        /* Data for 8 planets, dwarf planet Pluto, 2 Apollo (Earth-crossing) asteroids,  and Halley's
        Comet. (See http://neo.jpl.nasa.gov/orbits/ for asteroid and comet orbits.) The semimajor 
        elliptical axis a is in astronomical units (AU),  eccentricity epsilon is dimensionless,  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.  orientDeg[] is the relative
        orientation of the ellipse in degrees.  The variable retroFac controls whether the motion is
        direct (+1) or retrograde (-1). The relative orientations of the ellipses were eyeballed 
        from a plot, so are only approximately correct.  Likewise, the initial orientation angles theta0 
        were eyeballed from plots and are approximately correct for the date October 6, 2010. 
        The period and semimajor axis length are not independent, being related by Kepler's 3rd
        law P = a^{3/2} in these units.  We include them as separate static final arrays for
        computational efficiency. */    
        
        static final String planetName[] = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn",
            "Uranus", "Neptune", "Pluto", "2008 VB4", "2009 FG", "Halley"};   
        static final double epsilon[] = {0.206, 0.007, 0.017, 0.093, 0.048, 0.056,
                                                            0.047, 0.009, 0.248, 0.617, 0.529, 0.967};
        static final double  a[] = {0.387, 0.723, 1.0, 1.524, 5.203, 9.54,
                                                            19.18, 30.06, 39.53, 2.35, 1.97, 17.83};
        static final double period[] = {0.241, 0.615, 1.0, 1.881, 11.86, 29.46,
                                                            84.01,164.8, 248.5, 3.61, 2.76, 75.32};
        static final double theta0[] = {5.1, 1.4, 1.2, 1.6, 1.2, 4.4, 1.2, 2.0, 5.6, 3.1, 3.1, 3.1};
        static final float orientDeg[] = {0f, 0.0f, 0.0f, 100f, 0.0f, 0.0f, 0.0f, 0.0f, 200f, 
                                         70f, -45f, 115f};
        static final double retroFac[] = {1,1,1,1,1,1,1,1,1,1,1,-1};  // +1 for direct; -1 for retrograde
        
        private int numObjects;                        // Number of bodies to include (max = 12)
        private AnimationThread animThread;            // The animation thread
        private Paint paint;                           // Paint object controlling format of screen draws
        private ShapeDrawable planet;                  // Planet symbol
        float X[];                                     // Current X position of planet (pixels)
        float Y[];                                     // Current Y position of planet (pixels)
        float centerX;                                 // X for center of display (pixels)
        float centerY;                                 // Y for center of display (pixels)
        float R0[];                                    // Radius of planetary orbit in pixels
        double theta[];                                // Planet angle (radians clockwise from 12 o'clock)
        double dTheta[];                               // Angular increment each step (radians)
        private double pixelScale;                     // Scale factor: number of pixels per AU
        double c1[];                                   // The constant distance scale factor a*(1+epsilon^2)
        double c2[];                                   // Constant used to computer dTheta[i] from dt
        private double dt;                             // Animation timestep (years)
        long delay = 20;                               // Milliseconds of delay in the update loop
        private double zoomFac = 1.0;                  // Zoom factor (relative to 1) for display
        boolean showLabels = false;                    // Whether to show planet labels
        int mState = DONE;                             // Whether thread runs
        boolean isAnimating = true;                    // Whether planet motion is updated on screen
        private boolean showOrbits = true;             // Whether to show the orbital paths as curves
        private boolean showToast1 = true;             // Whether to Toast indicating short-press action
        private boolean showToast2 = true;             // Whether to Toast indicating long-press action
            
            
        public KeplerRunner(Context context) {
            super(context);	
            numObjects = 12;
            X = new float[numObjects];
            Y = new float[numObjects];
            theta = new double[numObjects];
            dTheta = new double[numObjects];
            R0 = new float[numObjects];
            c1 = new double[numObjects];
            c2 = new double[numObjects];
            dt = 1/(double)nsteps;
            
            // Add click and long click listeners
            setOnClickListener(this);
            setOnLongClickListener(this);
            
            for(int i=0; i<numObjects; 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(12);
            paint.setStrokeWidth(0);
            
            // Create the animation thread.  Don't start it though until the geometry
            // of the screen is set in onSizeChanged
            
            animThread = new AnimationThread(handler);
        }
            
        /* 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 (X0 and Y0 permit arbitrary offset of center)
            centerX = w/2 + X0;
            centerY = h/2 + Y0;
            
            // 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[4];
            
            // Set the initial position of the planet (translate by planetRadius so center of planet
            // is at this position)
            for(int i=0; i<numObjects; i++){
                // Compute scales c1[] and c2[] carrying distance units in pixels
                c1[i] = pixelScale*a[i]*(1-epsilon[i]*epsilon[i]);
                c2[i] = direction*2*Math.PI*Math.sqrt(1-epsilon[i]*epsilon[i])
                                        *dt*(pixelScale*a[i])*(pixelScale*a[i])/period[i];
                R0[i] = (float) distanceFromFocus(c1[i], epsilon[i], theta[i]);
                // The change in theta consistent with Kepler's 2nd law (equal areas in equal time)
                dTheta[i] = c2[i]/R0[i]/R0[i];
                // New values of X and Y for planet
                X[i] = centerX - R0[i]*(float)Math.sin(theta[i]) - planetRadius ;
                Y[i] = centerY - R0[i]*(float)Math.cos(theta[i]) - planetRadius;
            }
            
            // Start the animation thread now that we have the screen geometry
            animThread.start();
        }
            
        // Method to change the zoom factor
        void setZoom(double scale){
            if(!isAnimating) return;
            zoomFac *= scale;
            pixelScale= zoomFac*fracWidth*Math.min(centerX, centerY)/a[4];
            for(int i=0; i<numObjects; i++){
                c1[i] = pixelScale*a[i]*(1-epsilon[i]*epsilon[i]);
                c2[i] = direction*2*Math.PI*Math.sqrt(1-epsilon[i]*epsilon[i])
                                        *dt*(pixelScale*a[i])*(pixelScale*a[i])/period[i];
            }
        }
        
        // Method to change the speed of the animation.  Returns int code indicating action taken.
        // If negative, no change.
        
        long setDelay(double factor){
            if(!isAnimating) return -1;
            if(delay == 1 && factor < 1) return -2;
            // Logic below because delay is long int, so keep it from getting less than 1 and also
            // allow it to increase from small values.
            delay = Math.max((long) (delay * factor), 1);
            if(delay <10 && factor >1) delay += 2;
            return delay;
        }
                
                
        /* 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.  In this
        case the method handler() calls invalidate() when it receives a message from
        the animation thread that it has completed one pass through the animation loop. */
        
        @Override
        public void onDraw(Canvas canvas) {

            super.onDraw(canvas);
            
            /* The equations we are solving for Kepler's laws define elliptical motion for a planet,
            comet, or asteroid about the Sun at one focus of the ellipse.  But the objects in the
            Solar System generally have different orientations for the long axis of the ellipse, so
            to plot the correct relative orientation of different elliptical orbits on the same plot we
            must rotate each solution by a specific amount (given in the array orientDeg[]). 
            This rotation can be implemented in one of two ways.  (1) We can define 
            our own 2-dimensional rotation matrix and use it to rotate the coordinates for
            the instantaneous position of each planet, and the shape defining its orbit, before 
            plotting it.  (2) We can use the translate(dx, dy) and rotate(angle) methods of the
            Canvas class to transform the canvas appropriately for each object before plotting it.
            Both approaches are complicated by the fact that the origin of the computer graphics
            coordinate system is at the upper left corner, but we are executing elliptical motion 
            about a point at the center of the screen, so these transformation involve both 
            translations and rotations.  In the following example we employ the 2nd approach and
            use the rotate and translate methods of Canvas to rotate orbits. Notice also that 
            we are adopting the fiction that all bodies being considered have the same plane for
            their ellipses.  Except for Pluto and Comet Halley, this is almost true for the objects
            considered here.  Pluto's orbit is tilted about 17 degrees out of the plane of the ecliptic
            (plane defined by the Earth's orbit), Halley's orbit by about 70 degrees, and Mercury's
            orbit by 7 degrees.  All others are within several degrees of the ecliptic plane.  Thus, 
            the model in this example is an idealized but almost correct one where the realistic 
            elliptical orbits have been rotated when necessary to coincide with the ecliptic plane,
            but their relative orientations within the ecliptic plane are approximately correct. 
            To treat the orbits more correctly we need 3D graphics.*/
            
            // First draw the background (Sun and orbital paths)
            drawBackground(paint, canvas);
            paint.setColor(Color.LTGRAY);  
                        
            // Now loop over the planets, asteroids, dwarf planets, and comets, placing the 
            // corresponding symbol at the appropriate position.
            
            for(int i=0; i<numObjects; i++){
                
                // The nested sets of save() .. restore() below keep the matrix transformations
                // (translations and rotations in this case) from affecting the drawing on the canvas
                // outside of the save() .. restore() blocks.  Note: for each save() there is
                // a matching restore().
                
                canvas.save();
                canvas.translate(centerX, centerY);
                canvas.rotate(orientDeg[i]);
                canvas.translate(X[i] -centerX, Y[i] -centerY); 	   
                planet.draw(canvas);
                
                // Rotate the canvas back before drawing label so it will be horizontal instead of 
                // having the orientation of the ellipse. This save() .. restore() block is nested inside
                // the outer one, so this inverse rotation affects only the orientation of the label.
                
                canvas.save();
                canvas.rotate(-orientDeg[i]);
                if(showLabels) canvas.drawText(planetName[i], 10, 0, paint);
                canvas.restore();  
                canvas.restore();
            }
        }
        
        // Called by onDraw to draw the background
        private void drawBackground(Paint paint, Canvas canvas){
                
            // Draw the Sun
            paint.setColor(Color.YELLOW);
            paint.setStyle(Paint.Style.FILL);
            canvas.drawCircle(centerX, centerY, sunRadius, paint);
            
            // Orbits drawn with line segments if showOrbits is true
            if(showOrbits){
                paint.setStyle(Paint.Style.STROKE);
                paint.setColor(ORBIT_COLOR);
                double phi=0;
                    
                // Loop over each object, drawing its orbit as a sequence of numpoints line segments
                for(int i=0; i<numObjects; i++){
                        
                    // Starting points to draw orbit.  Note that the sign of the y coordinate is flipped
                    float lastxx = 0;
                    float lastyy = -(float) (distanceFromFocus(c1[i], epsilon[i], phi)*Math.cos(phi));
                    
                    canvas.save();
                    canvas.translate(centerX, centerY);
                    canvas.rotate(orientDeg[i]);
                    phi = 0;
    
                    // Increase density of plot points for very elliptical orbits to resolve their narrow shapes
                    int plotpoints = numpoints;
                    double delphi = dphi;
                    if(epsilon[i] > 0.7){
                        plotpoints *= 3;
                        delphi *= THIRD;
                    }
                    // Draw the orbit for object i
                    for(int j=0; j<plotpoints; j++){
                        phi += delphi;
                        float rr = (float) distanceFromFocus(c1[i], epsilon[i], phi);
                        float xx =  (float)(rr*Math.sin(phi));
                        float yy =  -(float)(rr*Math.cos(phi));  // Sign flipped
                        canvas.drawLine(lastxx, lastyy, xx, yy, paint);
                        lastxx = xx;
                        lastyy = yy;
                    }
                    canvas.restore();
                } 
            }
        }
        
        // Return distance from focus for elliptical orbit (in units of c1)
        private double distanceFromFocus(double c1, double epsilon, double theta){
            return (c1/(1+epsilon*Math.cos(theta)));
        }
        
        // Stop the thread loop
        public void stopLooper(){
            animThread.setState(DONE);
        }
        
        // Start the thread loop
        public void startLooper(){
            animThread.setState(RUNNING);
        }
        
        // Use long-press to toggle motion on and off.  The thread animThread and its while-loop
        // continue to run but motion update of view is suppressed when toggled off.
        
        @Override
        public boolean onLongClick(View v) {
            String ts = "Long-press toggles planet motion on/off";
            isAnimating = !isAnimating;
            if(showToast2) Toast.makeText(this.getContext(), ts, Toast.LENGTH_LONG).show();
            showToast2 = false;   // Show only the first time
            return true;    // Return true so long-press doesn't also trigger onClick action
        }
    
        // Use short press to toggle visibility of orbits
        
        @Override
        public void onClick(View v) {
            String ts = "Short-press toggles orbit visibility";
            showOrbits = !showOrbits;
            if(showToast1) Toast.makeText(this.getContext(), ts, Toast.LENGTH_LONG).show();
            showToast1 = false;   // Show only the first time
        }
        
            
            
        /*  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.  Since this is an inner class, it has
        access to the methods and fields defined in the enclosing class. */    
        
        private class AnimationThread extends Thread {	
            
            Handler mHandler;
            
            // 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) {
                    if(isAnimating) 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 ellipses with the Sun at one focus.   */     
        
            private void newXY(){
                for(int i=0; i<numObjects; i++){
                    dTheta[i] = retroFac[i]*c2[i]/R0[i]/R0[i];
                    theta[i] += dTheta[i];     
                    R0[i] = (float) distanceFromFocus(c1[i], epsilon[i], theta[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;
            }
        }  // End of inner class AnimationThread
    }


Now we instantiate KeplerRunner.java and lay out the corresponding animation screen and five control buttons. Open the file edit SolarSystem.java and edit it so that it reads


    package com.lightcone.solarsystem;
    
    import android.app.Activity;
    import android.graphics.Color;
    import android.os.Bundle;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.view.ViewGroup.LayoutParams;
    import android.widget.LinearLayout;
    import android.widget.Button;
    import android.widget.Toast;
    
    public class SolarSystem extends Activity implements OnClickListener {
        KeplerRunner krunner;
            
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            
            /* In the following we lay out the screen entirely in code (main.xml isn't used).  We
            wish to lay out a stage for planetary motion and five buttons at the bottom using
            LinearLayout.  The instance krunner of KeplerRunner is added to a LinearLayout LL1
            using addView.  Then the buttons are added 5 across in a LinearLayout LL2 using
            addView, and finally we use addView to add LL2 to LL1 and then use setContent
            to set the content view to LL1 and its children. The formatting of these layouts is
            controlled using the LinearLayout.LayoutParams lp1 and lp2. */
            
            /* First define some LayoutParams that we will use to control layout format.  Note
            that we use the LinearLayout.LayoutParams(int width, int height, float weight) form
            of the constructor, which allows us to assign weights (corresponding to the XML
            attribute android:layout_weight).  Assigning weights is essential to distributing
            screen space among the View and 5 buttons. (We also could have used the
            LinearLayout.LayoutParams(int width, int height) form of the constructor and then
            populated the public weight field with lp1.weight = float, etc.) */ 
            
            LinearLayout.LayoutParams lp1 = new LinearLayout.LayoutParams(
                            LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT, 20);
            LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(
                            LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT, 80);
            
            LinearLayout LL1 = new LinearLayout(this);
            LL1.setOrientation(LinearLayout.VERTICAL);
            
            LinearLayout LL2 = new LinearLayout(this);
            LL2.setOrientation(LinearLayout.HORIZONTAL);
            
            // Instantiate the class MotionRunner to define the entry screen display
            krunner = new KeplerRunner(this);
            krunner.setLayoutParams(lp2);
            
            krunner.setBackgroundColor(Color.BLACK);
            LL1.addView(krunner);
            
            // Add five buttons to the bottom of the screen in the LinearLayout LL2 with 
            // LinearLayout.LayoutParams lp1.  Since the layout orientation of LL2 was set to 
            // LinearLayout.HORIZONTAL above and the weight of lp1 was set to 20 in its
            // constructor, this will lay the five buttons out horizontally with equal spacing.
            
            Button button1 = new Button(this);
            button1.setLayoutParams(lp1);
            button1.setText("fast");
            button1.setId(1);
            button1.setOnClickListener(this);
            LL2.addView(button1);
    
            Button button2 = new Button(this);
            button2.setLayoutParams(lp1);
            button2.setText("slow");
            button2.setId(2);
            button2.setOnClickListener(this);
            LL2.addView(button2);
            
            Button button3 = new Button(this);
            button3.setLayoutParams(lp1);
            button3.setText("big");
            button3.setId(3);
            button3.setOnClickListener(this);
            LL2.addView(button3);
            
            Button button4 = new Button(this);
            button4.setLayoutParams(lp1);
            button4.setText("small");
            button4.setId(4);
            button4.setOnClickListener(this);
            LL2.addView(button4);
            
            Button button5 = new Button(this);
            button5.setLayoutParams(lp1);
            button5.setText("label");
            button5.setId(5);
            button5.setOnClickListener(this);
            LL2.addView(button5);
            
            LL2.setBackgroundColor(Color.BLACK);
            
            // Now add the five buttons to the bottom of the LinearLayout LL1 that already contains
            // the animation view defined by krunner, and finally set the content view for the screen 
            // to the LinearLayout LL1.
            
            LL1.addView(LL2);
            setContentView(LL1);
        }
        
        @Override
        public void onPause() {
            super.onPause();
            // Stop animation loop if going into background
            krunner.stopLooper();
        }
        
        @Override
        public void onResume() {
            super.onResume();
            // Resume animation loop
            krunner.startLooper();
        }
    
        // Process the button clicks
        @Override
        public void onClick(View v) {
                
            double delayScaler = 1.2;
            double zoomScaler = 1.1;
            
            switch(v.getId()){
            
                case 1:
                    long test = krunner.setDelay(1/delayScaler);    // Run faster
                    if(test == -2){
                        Toast.makeText(this, "Maximum speed. Can't increase further", 
                                        Toast.LENGTH_SHORT).show();
                    }
                    break;
                
                case 2:
                    krunner.setDelay(delayScaler);       // Run slower
                    break;
                
                case 3:
                    krunner.setZoom(zoomScaler);      // Zoom in
                    break;
                        
                case 4:
                    krunner.setZoom(1/zoomScaler);   // Zoom out
                    break;
                        
                case 5:
                    krunner.showLabels = !krunner.showLabels;   // Toggle planet labels
                    break;	
    
            }	
        }  
    }

This code is rather heavily commented, so much of its functionality should not be too hard to grasp if the reader is already familiar with the earlier simpler animation examples. However, in the next two sections we outline some of the most important functionality, and things that are new in this more complex animation.

 

Layout and Button Events

In preceding animation examples we have laid out the screen entirely in code, but the layout for this example is more complex, with a custom View and five Buttons. The layout is accomplished entirely by the code in SolarSystem.java, as now described.

  1. The class SolarSystem.java extends Activity and implements the OnClickListener interface, since we are going to listen for button clicks.

  2. First we define some LinearLayout.LayoutParams that we will use to control layout format. Note that we use the LinearLayout.LayoutParams(int width, int height, float weight) form of the constructor, which allows us to assign layout weights (corresponding to the XML attribute android:layout_weight. Assigning layout weights is essential to distributing screen space among the View and 5 Buttons. (If you don't assign explicit weights you will likely not have all widgets displayed on the screen.)
    
        LinearLayout.LayoutParams lp1 = new LinearLayout.LayoutParams(
                LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT, 20);
        LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(
                LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT, 80);
    
    
    Alternatively, we could have used the LinearLayout.LayoutParams(int width, int height) form of the constructor to instantiate the layout parameters, and then assign values to the public field weight, like so:
    
        LinearLayout.LayoutParams lp1 = new LinearLayout.LayoutParams(
            LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
        LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(
            LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
        lp2.weight = 80;
        lp1.weight = 20;
    
    


  3. Then we instantiate the KeplerRunner object krunner to define the entry screen display and add it to the LinearLayout LL1 using the method addView that LinearLayout inherits from ViewGroup

  4. Then the Buttons are added five across in a LinearLayout LL2. For example,
    
        Button button1 = new Button(this);
        button1.setLayoutParams(lp1);
        button1.setText("fast");
        button1.setId(1);
        button1.setOnClickListener(this);
        LL2.addView(button1);
    
    
    where the Button constructor is used to create a new button, then the methods setLayoutParams, setID, and setOnClickListener inherited from View are used to set layout parameters, widget ID, and an OnClickListener, respectively, the method setText inherited by Button from TextView is used to set the displayed Button label, and finally the Button view is added to the LinearLayout LL2 using the method addView that LinearLayout inherits from ViewGroup. Since the layout orientation of LL2 was set to LinearLayout.HORIZONTAL and the weight of lp1 was set to 20 in its constructor, this will lay the five buttons out horizontally with equal spacing.

  5. We complete the layout by using addView to add LL2 to LL1 and then use the method setContentView(View view) that SolarSystem inherits from Activity to set the content view to LL1 and its children. The formatting of all these layouts is controlled using the LinearLayout.LayoutParams lp1 and lp2.

  6. In a manner that should be familiar from earlier examples, we override the onClick method of the OnClickEvent interface, using a switch statement and the IDs assigned for the buttons to decide which was pushed and implement an appropriate action: scale the zoom up or down, scale the animation speed up or down, and toggle labels on and off for objects.

Finally, in the onPause() and onResume() methods we implement some standard life-cycle management to stop the animation thread when the app is in the background. The corresponding layout should look like that in the following figure




 

The Class KeplerRunner

The class KeplerRunner implements the entire initial display and subsequent animation. Most of its features should be familiar from the preceding Animator Demo 2 project:

  1. The animation is run on a separate thread, created by subclassing Thread.

  2. The class implementing the animation thread is implemented as an inner class AnimationThread of the main class KeplerRunner.

  3. Update of the main View is affected by sending messages from the animation thread to a Handler object defined on the UI thread, with this Handler requesting a screen redraw through the invalidate() method.

Most of our specific discussion will center on new features required because of the increased reality and thus complexity of the animation

  1. The motion of objects is no longer circular but involves solving Kepler's equations for motion in the Solar System, which define elliptical orbits with the Sun at one focus. Thus the method newXY() now calls the method distanceFromFocus() to determine the distance from the Sun as a function of time for elliptical orbits, and then converts that to new screen coordinates X and Y. (In the limit of zero eccentricity epsilon for orbits, the orbit becomes a circle with the Sun at the center, so one recovers our earlier Animator Demo 2 example.)

    An overview of Kepler's laws and the equations that we are using for this project may be found in the Wikipedia articles Kepler's Laws and Ellipses. Data for orbits of objects in the Solar System used here (and many additional ones) can be obtained from the NASA sites Near Earth Objects and Solar System Dynamics.


  2. Additional orbital data arrays orientDeg[], which describes the relative orientation of the different elliptical orbits with respect to each other, and retroFac[], which defines the relative sense of orbital motion (clockwise or counterclockwise in our display) are required.

  3. The class KeplerRunner implements both the OnClickListener and the OnLongClickListener interfaces, since we implement event handling so that a short press toggles the view of the orbits on and off and a long press toggles motion on and off. (These click listeners detect touches on the animation view itself and are separate from the click listener defined in the class SolarSystem that listens for clicks on the buttons at the bottom of the screen.)

  4. We implement a method setZoom(zoomFactor) that allows the zoom factor for the display to be toggled up and down by the corresponding buttons. We implement a method setDelay(factor) that controls the delay in the animation loop and thus the speed of the animation. It is invoked by the corresponding buttons to control the speed of the animation, but we place a practical limit on how fast the animation can be (this limit corresponds to no less than 1 ms delay in the animation loop). We use the label button defined in the class SolarSystem to toggle the value of the KeplerRunner boolean showLabels to control whether labels are displayed.

    The left figure below shows the display after using the buttons to zoom out far enough to see the outermost Solar System (the three outermost objects are Neptune, Pluto, and Halley's Comet). The right figure below shows the inner Solar System with the labels toggled on. (These are instantaneous screen captures for continuously-moving objects.)



  5. The equations we are solving for Kepler's laws define elliptical motion for a planet, comet, or asteroid about the Sun at one focus of the ellipse. But the objects in the Solar System generally have different orientations for the long axis of the ellipse, so to plot the correct relative orientation of different elliptical orbits on the same plot we must rotate each solution by a specific amount (given in the array orientDeg[]). That rotation can be implemented in one of two ways Both approaches are complicated by the fact that the origin of the computer graphics coordinate system is at the upper left corner, but we are executing elliptical motion about a point at the center of the screen, so these transformations involve both translations and rotations. In the following example we employ the 2nd approach and use the rotate and translate methods of Canvas to rotate orbits. In Exercise 1, we will implement the other method.

    In this exercise we are adopting the fiction that all bodies being considered have the same plane for their ellipses. Except for Pluto, Mercury, and Comet Halley, this is almost true for the objects considered here. Pluto's orbit is tilted about 17 degrees out of the plane of the ecliptic (plane defined by the Earth's orbit), Halley's orbit by about 70 degrees, and Mercury's orbit by 7 degrees. All others are within several degrees of the ecliptic plane. Thus, the model in this example is an idealized but almost correct one where the realistic elliptical orbits have been rotated when necessary to coincide with the ecliptic plane, but their relative orientations within the ecliptic plane are approximately correct. To treat the orbits correctly we need 3D graphics, which is beyond the scope of the present project illustrating 2D graphical techniques.

    The relevant Canvas rotate and translate statements may be found in the methods onDraw(Canvas canvas) and drawBackground(Paint paint, Canvas canvas), along with a number of comments explaining their implementation.

  6. In the method drawBackground(Paint paint, Canvas canvas), special logic has been implemented to increase the number of straight-line segments used to draw an orbit if it is highly elliptical, since otherwise it may not be well resolved by the default number of segments.

  7. In the method newXY() the variable retroFac[] is used to reverse the sense of the orbit if the object has retrograde orbital motion. For the cases considered here, only Halley's Comet exhibits retrograde motion; all other objects have direct motion.

  8. The onClick method has been overriden to cause short presses to toggle the orbit visibility on and off; the onLongClick method has been overriden to cause long presses to toggle the orbital motion on and off. In both cases we use a Toast to notify the user of the action the first time they do it.

 

Performance Versus Object-Oriented Design

In KeplerRunner.java we have declared a number of variables defined in the outer class to have default (package) scope rather than private (class only) scope. This is because, according to the Android document Designing for Performance, when variables of the outer class are accessed by an inner class the efficiency is improved (because of the way the compiler handles the special case of a class being permitted to access private methods and fields of another because it is an inner class of the other class) if those variables have package rather than private scope.

However, this also means that the fields of the outer class that are declared to have package scope could be accessed directly by other classes in the same package, which violates standard object-oriented practice of making all fields private if possible. You as a programmer must decide how to trade off good programming practice against performance. The choice may depend on the context: if you must have every ounce of performance for a limited device, you may need such optimizations, but if you are designing an API for public consumption you might wish to forego such optimizations in the interest of clarity and consistency for the API.


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



Exercises

1. In rotating the elliptical orbits to the correct relative orientation of their axes in this example we used the method discussed above of translating and rotating the canvas before plotting. Implement the same animation, but instead of rotating the canvas, define your own 2-dimensional rotation matrix and use it to rotate the coordinates before plotting them. Hint: the formula for a 2-D rotation matrix may be found in the Wikipedia article on Rotation Matrices. However, remember that in Android (as in many implementations of computer graphics) the y axis typically points downward if the x axis points to the right, which is opposite that of the usual mathematical discussion, so you will have to switch some signs for Y to get things to come out right in your rotations if you start with the formulas in the above article. [Solution]

2. Add some more comets and asteroids to the above animation of the Solar System. [Solution]


Previous  | Next  | Home