Click here to Skip to main content
Click here to Skip to main content

An Adventure in Porting a Java Applet to Android with no Previous Experience with Android

, 17 Oct 2011
Rate this:
Please Sign up or sign in to vote.
Impact Physics walkthrough
Main play screen

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.

AppletViewer

Main play screen

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 Balls and repaints the screen.

Original Impact.java class diagram before Android port:

UML class diagram

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.

//
// The main applet code
public class Impact extends java.applet.Applet{
...
    MainWindow b;   //a AWT feature
...
}

// This class controls the main window
class MainWindow extends Frame{	//Frame is also AWT
...
    Dialog db;          		// Dialog is AWT
    //Note the widgets (Panel, Button, Label, Slider, etc) all have to change
    Panel p = new Panel();      	// control panel
    Label n = new Label("Impact 2.2");
    ...
    
    //uses Java threads, these should port fairly easy unless there is a better way 
    //to implement the functionality
    public void start() 
    {
        if (anim_thread==null)
        {
            anim_thread = new Thread(anim);
            anim_thread.start(); 	// start new thread
        }
        if (tick_thread==null)
        {
            tick_thread = new Thread(tick);
            tick_thread.start();  	// start new thread
        }
    }
    
    // This handles user input events, this should be roughly similar to Android
    // but the details of the API will differ
    public boolean action(Event e, Object arg)
}

// This class performs the animation in the main canvas.
class Animator extends Canvas implements Runnable{
...
    //paint is from Canvas, this will definitely be different in Android
    public void paint(Graphics g) {...}

    // from "implements Runnable", will need porting
    public void run()
    
    //these should port easily
    //The following mouse methods allow you to drag balls
    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) 
}

//  The Ball class
class Ball {...} {
    //the Graphics api will need to be changed to Android equivalent
    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;           // save new location
        g.setColor(c);
        g.drawOval((int)(x-z),(int)(y-z),(int)(2*z),(int)(2*z));
    }

// Class that serves as a pacemaker for the animator
// It turns out I eliminated this class in the port, there was an easier way to do this
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).

New UML

I will next explain these at various levels.

Screenshots of App

The main activity where balls are drawn and user can move the balls:

Main play screen

The menus:

Main options

The "Add balls" custom activity:

Game options

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) 
    {
        // Handle item selection 
...
        else if (item.getItemId() == OPTION_MENUID)	//user clicked Options...
            options();
...
           return super.onOptionsItemSelected(item);    
    }
    
    /**
     * invoke the 2nd activity which contains the main program options
     */
    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); //this will start 
			//the activity and tell framework to expect a response
    }

The new activity will gain user focus...

Options

...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.

// In GameOptions when the "Apply and quit" button is pressed, 
// a new Intent and Bundle are created
// and the updated gameParams are serialized in the bundle and returned setResult
View.OnClickListener quitHandler = new View.OnClickListener() {
    public void onClick(View v) {
        // read widget values
        ...
            
        // prepare to send results back to invoker
        Intent resultIntent = new Intent();
        Bundle b = new Bundle();
        b.putSerializable("options", gameParams);
        resultIntent.putExtras(b);
        setResult(Activity.RESULT_OK, resultIntent);
        finish();
    }  //onClick

// Back in ImpactPhysics
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) 
          { 
              //retrieve intended options
              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.

activityLifecycle

public void OnResumeProxy()
{
    mTmr = new Timer(); 
    mTsk = new TimerTask() 
    {
        //anonymous method  
        public void run() 
        {
            ...
            //redraw ball. Must run in background thread to prevent thread lock.
            RedrawHandler.post(new Runnable() 
            {
                public void run() 
                {
                    invalidate();
                }
            });
        }
    }; // TimerTask

    //task, long delay, long period
    mTmr.schedule(mTsk,speed, speed); //start timer
} //OnResumeProxy

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);
    }
} //onDraw

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.

Options

/**
 * Menu action item to open ColorPickerDialog to choose new color for added balls
 */
void chooseColor()
{
    showDialog(COLOR_DIALOG_ID);	//step 1, call framework to load/show this dialog 
				// (in step 2)
} 
    
//called by framework as a result of showDialog(COLOR_DIALOG_ID);
protected Dialog onCreateDialog(int id)
{
    switch (id) 
    {
        case COLOR_DIALOG_ID:
            return new ColorPickerDialog(this, this, Color.RED); //step 2, 
					// create and show the dialog
    }
    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.

//step 1, create the menus (1 time call)
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...");
    //the following menu items get lumped into the "More" category
    menu.add(Menu.NONE,POP_MENUID,Menu.NONE,"Pop");
    menu.add(Menu.NONE,COLOR_MENUID,Menu.NONE,"Choose color");
    return super.onCreateOptionsMenu(menu);
}
    
//step 2, callback when menu button pressed
public boolean onOptionsItemSelected(MenuItem item) 
{
    // Handle item selection 
    if (item.getItemId() == EXIT_MENUID)		//user clicked Exit
        finish(); //will exit the activity
    else if (item.getItemId() == CLEAR_MENUID)	//user clicked Clear
        clearScreen();
    else if (item.getItemId() == ADD_MENUID)		//user clicked Add balls
        addBallChoice();
    else if (item.getItemId() == COLOR_MENUID)	//user clicked choose color
        chooseColor();
    else if (item.getItemId() == OPTION_MENUID)	//user clicked Options...
        options();
    else if (item.getItemId() == POP_MENUID)		//user clicked Pop
        pop();
    else if (item.getItemId() == ABOUT_MENUID)	//user clicked About...
        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:

Options

Then, you create the MediaPlayer object and in this case I want looping enabled.

mediaPlayerBubbles = MediaPlayer.create(this, R.raw.bubbles); //note the R.raw.bubbles 
						//refers to /res/raw/bubbles.wav
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);  	//range 0.0 to 1.0f
      if (mediaPlayerBubbles.isPlaying() == false)
      {
          mediaPlayerBubbles.start(); 		// no need to call prepare(); 
						// create() does that for you
      }
}
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 in a non looping manner, i.e., one shot, to indicate this condition.

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

Notifications

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 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()
    {
...
        //for fun, add toast message
        Context context = getContext();
        CharSequence text = "Number of balls = " + currCount;
        Toast toast = Toast.makeText(context, text, Toast.LENGTH_SHORT);
        toast.show();  
        
        //for fun lets try the status bar notification
        if (currCount > 100) showStatus();
    }	//addRandomBall
    
    void showStatus()
    {
        //get ref to NotificationManager
        String ns = Context.NOTIFICATION_SERVICE;
        NotificationManager mNotificationManager = 
			(NotificationManager) pContext.getSystemService(ns);
        
        //instantiate it
        int icon = R.drawable.notification_icon;
        CharSequence tickerText = "balls = "+currCount;
        long when = System.currentTimeMillis();

        Notification notification = new Notification(icon, tickerText, when);
        
        //Define the notification's message and PendingIntent: 
        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);
        
        //Pass the Notification to the NotificationManager: 
        mNotificationManager.notify(HELLO_ID, notification);
    }

Here is what the popup message via the Toast API looks like:

Options

Here is what the notification bar looks like:

Options

and what the actual notification is when you display the notifications (by down swiping the bar):

Options

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(file:///android_asset/about.html).

void aboutBox()
{
    Intent myIntent = new Intent(this, WebViewHelp.class);
    startActivity(myIntent); //step 1, start the WebView activity
} 
    
//The WebView
public class WebViewHelp extends Activity 
{
    WebView mWebView;

    // Called when the activity is first created.
    @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 loads 
				//the browser and view the file
        }
    }	//onCreate        
}	//WebViewHelp  

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.

Options

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 {
    /**
     * user not touching the screen
     */
    NONE
    /**
     * Pinch or Zoom action in progress , 2 fingers in play
     */
    ,ZOOM

    /**
     * single finger drag in progress
     */
    ,DRAG
}
    
public boolean onTouch(android.view.View v, android.view.MotionEvent e) 
{       
    boolean consumed = true;	//assume we consumed this event
    float x = e.getX();
    float y = e.getY(); 
        
    switch (e.getAction() & MotionEvent.ACTION_MASK)
    {
        case MotionEvent.ACTION_DOWN:
            //ignore
            pinchMode = PinchActions.DRAG;
            currentBall = nearestBall(x,y,currCount);
            mx = x; my = y;
            break;
            
        case MotionEvent.ACTION_UP:
            // this magic number means that the mouse is up
            pinchMode = PinchActions.NONE;
            break;
            
        case MotionEvent.ACTION_POINTER_DOWN:
            //this action occurs when 2nd finger is down
            oldDist = spacing(e);
            if (oldDist > 10f) {
                pinchMode = PinchActions.ZOOM;
            }
            break;
            
        case MotionEvent.ACTION_POINTER_UP:
            //I think this means the 2nd finger was raised
            pinchMode = PinchActions.DRAG;
            break;

        case MotionEvent.ACTION_MOVE:
            if (pinchMode == PinchActions.ZOOM) 
            {
                //these moves happen frequently, apply a kind of low pass filter 
	       //to slow it down
                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 True if the listener has consumed the event, false otherwise.
        return consumed;
}	//onTouch

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

  • Version 1.0, Oct 2011

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Ed Korsberg
Software Developer (Senior)
United States United States
During working hours is a real time firmware developer for custom ASIC's in embedded systems. Writes C/C++/C# code in his sleep and is moderately proficient in Java, Python, hardware description languages and other obscure domain specific languages. Deals with nano/microsecond data propagation delay paths, cache line hit/miss ratios, multicore asic design, etc.
However for fun likes to dabble in the new technologies of the day and deals at higher level software engineering aspects.
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberskrajeshwar13-Sep-12 20:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140709.1 | Last Updated 17 Oct 2011
Article Copyright 2011 by Ed Korsberg
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid