Introduction
This shows how I ported a Java Applet to Android app with no previous experience.
Background
I would like to thank several people instrumental in inspiring me to write this app.
In 1996, John Henckel from IBM posted a Java Applet, Impact.java, that is the heart of this app.
The general purpose of this application is to create 2D ball objects that have attributes of mass, size, color, coefficient of restitution and then place them in a medium with a degree of viscosity. The balls then interact with each other being attracted to each other by gravity and when they collide will rebound as a function of their coefficient of restitution. The user can also 'grab' the balls via the mouse and alter their movement.
I ported this Java Applet to Android and this article discusses the learning steps required and also highlights how fairly common Android features are implemented such as menus, sound, accelerometer reading, notifications, activities, intents, etc. The Android app extends the original applet by adding sound and use of the accelerometer sensors to affect the movement of the balls beyond the gravitational and frictional forces.
Getting Started in Android
There are several good beginning tutorials on how to get started in Android. I recommend http://www.makeuseof.com/tag/write-google-Android-application/ for how to start with a Hello World app and how to install and configure the development tools.
Mike Waddel posted an excellent CodeProject article http://www.codeproject.com/KB/Android/TiltBallWalkthrough.aspx that helped me get started on the details of writing this Android app.
As I thought about writing this article, I was going to replicate much of the above articles but decided that was wasteful since this would be mostly copy and paste anyway. So I recommend reading the above articles on how to construct an Android application from the beginning. I will focus more on the specifics of how I ported and extended this app.
The Original Java Applet
The original applet looks like this. It was written as a single Java file, Impact.java and contained several classes.
In the original applet, there was a MainWindow
that mostly contained the UI elements and a canvas
in which the Animator
would draw Ball
objects. The pace of the animation was metered by the Ticker
. Here is a simple class diagram, don't get hung up on UML purity, this picture is just an overview. The top class Impact
creates one Ticker
and one Animator
instance. The Animator
depends on Ticker
for time pacing. The Animator
creates a set of Ball
objects. During runtime, the Animator
both coordinates the interaction between the Ball
s and repaints the screen.
Original Impact.java class diagram before Android port:
Highlights of Code that Needed Porting Attention
The following code snippets were areas that either I immediately saw as issues in porting to Android or discovered during attempts to import and compile with the Android SDK. The primary porting effort was translating the java.awt.* into the equivalent Android API. The original source code is referenced at the top of this article.
public class Impact extends java.applet.Applet{
...
MainWindow b;
...
}
class MainWindow extends Frame{
...
Dialog db;
Panel p = new Panel();
Label n = new Label("Impact 2.2");
...
public void start()
{
if (anim_thread==null)
{
anim_thread = new Thread(anim);
anim_thread.start();
}
if (tick_thread==null)
{
tick_thread = new Thread(tick);
tick_thread.start();
}
}
public boolean action(Event e, Object arg)
}
class Animator extends Canvas implements Runnable{
...
public void paint(Graphics g) {...}
public void run()
public boolean mouseDown(Event e, int x, int y)
public boolean mouseDrag(Event e, int x, int y)
public boolean mouseUp(Event e, int x, int y)
}
class Ball {...} {
public void draw(Graphics g, boolean sm)
{
g.setColor(Color.black);
g.drawOval((int)(ox-z),(int)(oy-z),(int)(2*z),(int)(2*z));
ox = x; oy = y;
g.setColor(c);
g.drawOval((int)(x-z),(int)(y-z),(int)(2*z),(int)(2*z));
}
class Ticker implements Runnable {...}
Next, Porting this to Android
Besides the obligatory creation of a new Android Eclipse application that will build this app, the first item to tackle was to replace the UI and graphics API from java.awt.* to the equivalent Android API. In the classic manner of (1) I will tell you what I am about to tell you... and (2) I will tell you..., here is the final new class diagram for this Android application after all the changes (again don't get hung up on UML purity).
I will next explain these at various levels.
Screenshots of App
The main activity where balls are drawn and user can move the balls:
The menus:
The "Add balls" custom activity:
Custom Dialog with New Activity and Intent
Android supports a limited set of Dialog boxes. I choose to create a custom screen using a new Activity
and Intent
. This focus shift from the main activity to the GameOptions
activity occurs when the "Options
..." menu item is chosen. The following code in ImpactPhysics.java is called. In this usage, we create an Intent
directed to GameOptions
. Then create a Bundle
that will contain a serialized instance of the gameParams
object. Then the activity is launched via startActivityForResult(myIntent,STATIC_OPTIONS_VALUE);
.
public boolean onOptionsItemSelected(MenuItem item)
{
...
else if (item.getItemId() == OPTION_MENUID)
options();
...
return super.onOptionsItemSelected(item);
}
void options()
{
Intent myIntent = new Intent(this, GameOptions.class);
Bundle b = new Bundle();
b.putSerializable("options", gameParams);
myIntent.putExtras(b);
startActivityForResult(myIntent,STATIC_OPTIONS_VALUE);
}
The new activity will gain user focus...
...and the user will interact with the widgets changing parameters and upon return, onActivityResult
will be called to retrieve the response from the GameOptions
activity. The results will be used to update the current options.
View.OnClickListener quitHandler = new View.OnClickListener() {
public void onClick(View v) {
...
Intent resultIntent = new Intent();
Bundle b = new Bundle();
b.putSerializable("options", gameParams);
resultIntent.putExtras(b);
setResult(Activity.RESULT_OK, resultIntent);
finish();
}
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
switch(requestCode)
{
case (STATIC_OPTIONS_VALUE) :
{
if (resultCode == Activity.RESULT_OK)
{
Bundle b = data.getExtras();
gameParams = (GameOptionParams) b.getSerializable("options");
...
Eliminate Ticker
The original applet used a separate thread for providing a 'ticker
'. That is a valid means of providing a periodic time reference but I did not feel it was needed. In the new AnimatorView
class, OnResumeProxy
is called from ImpactPhysics
OnResume
when the activity becomes visible (see Activity lifecycle).
Within here is an anonymous method of TimerTask
that will get periodically triggered to invalidate the view which in turn will cause onDraw
to be called which will in turn redraw all the balls in their new positions based on the current dynamics.
public void OnResumeProxy()
{
mTmr = new Timer();
mTsk = new TimerTask()
{
public void run()
{
...
RedrawHandler.post(new Runnable()
{
public void run()
{
invalidate();
}
});
}
};
mTmr.schedule(mTsk,speed, speed);
}
Canvas Still Used but a Bit Differently
With the overall refactoring in place, it turns out the draw primitives (at least the android.graphics.Canvas.drawCircle
API) going from AWT to Android are similar enough for easy porting. AnimatorView.onDraw
is called when the screen (or maybe the canvas) is invalidated and thus needs to be redrawn. The drawCircle
used to draw the balls takes 4 parameters in my usage: X,Y, radius and the Paint
(brush).
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
...
for (int i=0; i< currCount; i++ )
{
canvas.drawCircle(ballArray[i].x, ballArray[i].y,
ballArray[i].radius, ballArray[i].mPaint);
}
}
And Just to Show How a Real Dialog Works, Added Color Picker Dialog
I looked at the Android Open Source Project ColorPickerDialog
class (public class ColorPickerDialog extends Dialog
) which extends android.app.Dialog
so I thought it would be fun to play with. I modified it to scale the color circle wheel to the screen size. The following code within ImpactPhysics
shows how to start this dialog. Note that I don't actually use this code in the current release. I instead simply create random sized balls with random colors. The purpose of this code was just to demonstrate how to extend a dialog box.
void chooseColor()
{
showDialog(COLOR_DIALOG_ID);
}
protected Dialog onCreateDialog(int id)
{
switch (id)
{
case COLOR_DIALOG_ID:
return new ColorPickerDialog(this, this, Color.RED);
}
return null;
}
Menus
Adding menus to the bottom of an activity is fairly easy. Basically it is a two step process. First you override onCreateOptionsMenu
which is called one time in your application. Note that there is room only for 6 menus items. If you require more than 6, then the framework creates a "More
" item which then displays additional items for selection. Second, you override onOptionsItemSelected
which is called with the MenuItem
associated with the selection you choose.
public boolean onCreateOptionsMenu(Menu menu)
{
menu.add(Menu.NONE,EXIT_MENUID,Menu.NONE,"Exit");
menu.add(Menu.NONE,CLEAR_MENUID,Menu.NONE,"Clear");
menu.add(Menu.NONE,ADD_MENUID,Menu.NONE,"Add balls");
menu.add(Menu.NONE,OPTION_MENUID,Menu.NONE,"Options...");
menu.add(Menu.NONE,ABOUT_MENUID,Menu.NONE,"About...");
menu.add(Menu.NONE,POP_MENUID,Menu.NONE,"Pop");
menu.add(Menu.NONE,COLOR_MENUID,Menu.NONE,"Choose color");
return super.onCreateOptionsMenu(menu);
}
public boolean onOptionsItemSelected(MenuItem item)
{
if (item.getItemId() == EXIT_MENUID)
finish();
else if (item.getItemId() == CLEAR_MENUID)
clearScreen();
else if (item.getItemId() == ADD_MENUID)
addBallChoice();
else if (item.getItemId() == COLOR_MENUID)
chooseColor();
else if (item.getItemId() == OPTION_MENUID)
options();
else if (item.getItemId() == POP_MENUID)
pop();
else if (item.getItemId() == ABOUT_MENUID)
aboutBox();
return super.onOptionsItemSelected(item);
}
Adding New Features Not in Original
Now that the application is functioning more or less like the original next is to add features using the native Android APIs.
Sound Effects
To play sound, you can use the android.media.MediaPlayer
class. An option in the GameOptions
activity is to play a bubble sound in a looping manner. Assuming you have an external media file in formats like MP3 or WAV (and others), you place that into the /res/raw folder of your project:
Then, you create the MediaPlayer
object and in this case I want looping enabled.
mediaPlayerBubbles = MediaPlayer.create(this, R.raw.bubbles);
mediaPlayerBubbles.setLooping(true);
Then when you want to start/stop the sound do this (I do it upon return from the custom activity).
if (gameParams.bubbleSound == true)
{
float level = (float)gameParams.volumeBarPosition/100;
mediaPlayerBubbles.setVolume(level,level);
if (mediaPlayerBubbles.isPlaying() == false)
{
mediaPlayerBubbles.start();
}
}
else
{
if (mediaPlayerBubbles.isPlaying() == true)
{
mediaPlayerBubbles.pause();
}
}
Accelerometer Input, Shake It Up!
This application reads the accelerometer inputs (just x and y, not z) and applies this to the gravitational force on all the balls. I also wanted to add a "shake" detection by which if you apply a certain level of acceleration (by shaking the phone) it would take some action. I had assumed the Android sensor API would provide this but that does not seem to the case. However the good news is that it is fairly easy to code this up. The following code onSensorChanged
is called from the main activity upon accelerometer change of state. This code does several things. It applies a bit of low pass filtering so that events are not taken too quickly. It will only potentially take action every 100 milliseconds. If the speed difference between samples is greater than SHAKE_THRESHOLD
, then it simply sets a flag stating this case. The animation code mBallView.shakeItUp()
in turn will apply a temporary negative repulsive force to each ball which will cause them to move away from each other. Then this code plays a "sprong" sound <code.mediaplayersprong.start()<>in a non looping manner, i.e., one shot, to indicate this condition. </p>
<pre lang="Java">public void onSensorChanged(SensorEvent event) {
long curTime = System.currentTimeMillis();
boolean shakeDetected = false;
// only allow one update every 100ms.
if ((curTime - lastUpdate) > 100)
{
long diffTime = (curTime - lastUpdate);
lastUpdate = curTime;
x = event.values[0];
y = event.values[1];
z = event.values[2];
float speed = Math.abs(x+y+z - last_x - last_y - last_z) / diffTime * 10000;
if (speed > SHAKE_THRESHOLD)
{
// yes, this is a shake action! Do something about it!
shakeDetected = true;
}
last_x = x;
last_y = y;
last_z = z;
}
if (gameParams.shakeItUp && shakeDetected == true)
{
if (mBallView.bShakeItUpInPlay==false)
{
mBallView.shakeItUp();
float level = (float)gameParams.volumeBarPosition/100;
mediaPlayerSprong.setVolume(level,level); //range 0.0 to 1.0f
mediaPlayerSprong.start();
}
}
mBallView.updateXYgravity(event.values[0], event.values[1]);
} //onSensorChanged</pre>
<h3>Notifications</h3>
<p>You may see lots of applications that post to the notification status bar at the top of the screen or do a popup notification. I did not find any strong use for this but encoded it anyway just to learn how to do it. Note that the actual code provide here does not enable the Toast call, it is kind of annoying but I just wanted to show how it is done. The <code>AnimatorView addRandomBall shows how to display a popup notification via the Toast
API. It also calls showStatus()
to display a notification in the status bar.
public void addRandomBall()
{
...
Context context = getContext();
CharSequence text = "Number of balls = " + currCount;
Toast toast = Toast.makeText(context, text, Toast.LENGTH_SHORT);
toast.show();
if (currCount > 100) showStatus();
}
void showStatus()
{
String ns = Context.NOTIFICATION_SERVICE;
NotificationManager mNotificationManager =
(NotificationManager) pContext.getSystemService(ns);
int icon = R.drawable.notification_icon;
CharSequence tickerText = "balls = "+currCount;
long when = System.currentTimeMillis();
Notification notification = new Notification(icon, tickerText, when);
Context context = pContext.getApplicationContext();
CharSequence contentTitle = "ImpactPhysics";
CharSequence contentText = "Ball status";
Intent notificationIntent = new Intent();
PendingIntent contentIntent = PendingIntent.getActivity
(pContext, 0, notificationIntent, 0);
notification.setLatestEventInfo
(context, contentTitle, contentText, contentIntent);
mNotificationManager.notify(HELLO_ID, notification);
}
Here is what the popup message via the Toast
API looks like:
Here is what the notification bar looks like:
and what the actual notification is when you display the notifications (by down swiping the bar):
WebView Used to Display "About..."
For the "About..." menu item, I used a WebView
in a 3rd activity to load an internal HTML file in /res/assets/about.html mWebView.loadUrl(<a href="file:///android_asset/about.html">file:///android_asset/about.html</a>).
void aboutBox()
{
Intent myIntent = new Intent(this, WebViewHelp.class);
startActivity(myIntent);
}
public class WebViewHelp extends Activity
{
WebView mWebView;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.webview);
mWebView = (WebView) findViewById(R.id.webviewhelp);
if (mWebView == null)
{
System.out.println("Could not load webviewhelp");
}
else
{
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.loadUrl("file:///android_asset/about.html");
}
}
}
This file is then displayed and all hyperlinks can be used to navigate to anchors and external URLs. Standard page scrolling is used to scroll through the multiple pages of the file.
Pinch-zoom
The sensor detection code implements the familiar 'pinch and zoom' two finger gesture. I had assumed that the Android sensor API supplies an event or callback for this 'pinch and zoom' but that does not seem to the case (except maybe for 3 specialized view classes). However the good news is that it is fairly easy to implement 'pinch and zoom' yourself. In AnimatorView
, all the pinch-zoom code is in the onTouch
handler. Event MotionEvent.ACTION_DOWN
occurs when the 1st finger touches, MotionEvent.ACTION_POINTER_DOWN
occurs when the 2nd finger touches and that is used in this code to signal the start of pinch or zoom. A "pinch" is when the MotionEvent.ACTION_MOVE
event occurs and then spacing between the two fingers is getting smaller, conversely if the spacing is getting bigger that is a "zoom".
enum PinchActions {
NONE
,ZOOM
,DRAG
}
public boolean onTouch(android.view.View v, android.view.MotionEvent e)
{
boolean consumed = true;
float x = e.getX();
float y = e.getY();
switch (e.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
pinchMode = PinchActions.DRAG;
currentBall = nearestBall(x,y,currCount);
mx = x; my = y;
break;
case MotionEvent.ACTION_UP:
pinchMode = PinchActions.NONE;
break;
case MotionEvent.ACTION_POINTER_DOWN:
oldDist = spacing(e);
if (oldDist > 10f) {
pinchMode = PinchActions.ZOOM;
}
break;
case MotionEvent.ACTION_POINTER_UP:
pinchMode = PinchActions.DRAG;
break;
case MotionEvent.ACTION_MOVE:
if (pinchMode == PinchActions.ZOOM)
{
newDist = spacing(e);
if (newDist > 10f) {
float scale = newDist / oldDist;
reScaleBalls(scale);
}
}
else if (pinchMode == PinchActions.DRAG)
{
mx = x; my = y;
}
break;
default:
consumed = false;
break;
}
return consumed;
}
Limitations of Current Code and Future Improvements
This code was my first endeavor with Android and thus I assume that expert Android writers could find fault and room for improvement. Please provide feedback on areas you think can be improved!
Persistent Storage
The next version I would like to add persistent storage to the option menu.
Parsing of options.xml
Another aspect of this code I do not like is that while the XML layout of the options activity (/res/layout/options.xml) works great from within GameOptions
activity, I could not find an easy way to extract the attributes of the widgets from the main ImpactPhysics
activity short of manually parsing the XML file (no thanks). I suspect there is an easy way to do this but it was not apparent to me. In the next version, I would like to read the defaults from the options.xml file upon startup.
Some Strange Bugs
I did run into a strange bug. I have two media files I obtained from the same site as the others which behave badly and in different ways between the emulator and the real phone. The file pop.mp3 does not work on my phone (MediaPlayer.create(this, R.raw.pop)
returns null
but works on the emulator. Then in a different failure mode, pop.wav does not crash but produces no sound on phone but does on emulator. I never did figure out what that was about. But that was the only anomaly I ran into.
History