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
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.
In Eclipse, set up the following project:
where, as usual, you should substitute your namespace for com.lightcone.
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.
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.
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;
|
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.
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 implements the entire initial display and subsequent animation. Most of its features should be familiar from the preceding Animator Demo 2 project:
Most of our specific discussion will center on new features required because of the increased reality and thus complexity of the animation
| 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. |
![]() |
![]() |
| 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. |
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. |