JDroidLib
 
  The source code of all examples is included in the JDroidLib distribution.

Implementing Android's Button Look-And-Feel

With JDroidLib the complexity of the Android lifecycle with the onCreate(), onStart(), onResume(), onPause(), onStop(), onRestart(), onDestroy() callbacks is hidden to the developer. This is achieved by deriving the application class from GameGrid and overriding the main() method where most of the code is placed. Each Android app is running in its own Linux process that normally does not terminate when the user hits the [HOME]. The apps activity is still running, but the display is owned by another activity.

As default the JDroidLib app process terminates when the [HOME] or [BACK] button is hit (by a crude-force killing of the process). This unusual behavior ensures that all resources are released and all network connections are closed when one of these buttons are hit. (This design stragedy has been chosen for security reasons in an educational environment.) You can easily overcome these restrictions with a few additional code. We show how to do it by an example.

If you want to implement your own action when the [BACK] button is hit, you register a GGNavigationListener. Now the callback navigationEvent() is called when one of the buttons [BACK], [MENU], [VOLUME+], [VOLUME-] are used.

To prevent the termination of your app when the [HOME] button is hit or when another app takes your screen (e.g. on a incoming phone call), you disable the brute-force killing with enableKill(false). Now the app cedes the screen to another activity, but continues to run in the background. It is up to you to ensure that it does not waste system resources.

When your app resumes after it was paused, the main thread is started again and executes main(). An automatic cleanup is done that removes all actors from the game grid. If you want to disable this cleanup you set setCleanupEnabled(false).

We put all together in an example that can be used in every day's life: A spirit level indicator (germ. Wasserwaage, Libelle) is simulated using the Android's orientation sensor that reports the pitch and roll angles (forward and sideward tilt). First we show a simple implementation without taking care of the [HOME] and [BACK] buttons.

To get data from the orientation sensor we reclaim a singleton reference sensor from the GGComboSensor class. In act() we poll the sensor by calling getOrientation() and determine a displacement with coordinates proportional to the pitch and roll angles. Because the origin of the pixel coordinate system is at the upper left corner of the window, we must add the center coordinates (offset, offset) to place the bubble.

It is nice to limit the bubble location to the periphery of the spirit gear. Several solutions are conceivable. We opt for the following: We do not restrict the bubble coordinates at all, but set its apparent location to the periphery of the gear. The simple geometric calculation is performed in the method reduce() that takes the real location P(x, y) and returns the reduced location P'(x', y'). As you see from the figure, two proportions based on similarity are evident:

spiritlevel

windowZoom(600) enables the automatic zoom of the gear and bubble images depending on the current device resolution (600 is the size of the virtual coordinate system corresponding to the size of the current screen window).


package ch.aplu.spiritlevelbase;

import
 ch.aplu.android.*;
import
 android.graphics.*;
import
 android.hardware.SensorManager;

public
 class SpiritLevelBase extends GameGrid
{
  
private int offset;
  
private Actor bubble;
  
private GGComboSensor sensor;
  
private int rLimit;

  
public SpiritLevelBase()
  {
    
super("gear", windowZoom(600));
  }

  
public void main()
  {
    offset 
= getNbVertCells() / 2;
    rLimit 
= getNbVertCells() / 4;
    sensor 
= GGComboSensor.init(this, SensorManager.SENSOR_DELAY_FASTEST);
    bubble 
= new Actor("bubble");
    addActor(bubble, 
new Location(offset, offset));
    setSimulationPeriod(30);
    doRun();
  }

  
public void act()
  {
    
// origin of x, y in center of window
    
double[] a = sensor.getOrientation(3);
    
Point p = new Point(
      (
int)(-0.02 * a[5] * offset), // Roll
      (
int)(0.02 * a[4] * offset)); // Pitch
    p 
= reduce(p);
    bubble.setLocation(
new Location(offset + p.x, offset + p.y));
  }

  
private Point reduce(Point p)
  {
    
// Reduce point to limiting circle
    
if (p.x * p.x + p.y * p.y > rLimit * rLimit)
      
return new Point(
        (
int)(rLimit * p.x / Math.sqrt(p.x * p.x + p.y * p.y)),
        (
int)(rLimit * p.y / Math.sqrt(p.x * p.x + p.y * p.y)));
    
else
      
return p;
  }
}

 

Download SpiritLevelBase app for installation on a smartphone

Create QR code to download Android app to your smartphone.

Download sources (SpiritLevelBase.zip).

jdroidlib11

 

In the full version we implement the standard Android button look-and-feel:

When the [BACK] button is hit, a message is displayed that says to press [BACK] again within a short time to really exit the process. To implement this feature we launch a thread to check the timeout by resetting the isBackPressed flag. finish() is called to terminate the process.

If the user hits the [MENU] button an "about" message is displayed. Here some more useful action could take place.

When the [HOME] button is pressed or another activity interrupts our app (e.g. an incoming phone call or when the display goes to sleep), the system invokes GameGrid.onPause(). Here we find the somewhat malicious code that kills our app. To prevent this code to execute, we override onPause(). (Android claims to invoke Activity.onPause(), we fullfill this demand by calling superOnPause().) To avoid wasting processor time we also stop the simulation cycling by calling doPause(), so act() is not called anymore. When the user starts the app again or if the interrupting app terminates, main() is called again where we skip the initilization code by testing the isInitDone flag.


package ch.aplu.spiritlevel;

import ch.aplu.android.*;
import android.graphics.*;
import android.hardware.SensorManager;

public class SpiritLevel extends GameGrid
  implements GGNavigationListener
{
  private int offset;
  private Actor bubble;
  private GGComboSensor sensor;
  private int rLimit;
  private boolean isInitDone = false;
  private boolean isBackPressed;

  public SpiritLevel()
  {
    super("gear", windowZoom(600));
  }

  public void main()
  {
    if (isInitDone)
    {
      showToast("SpiritLevel up again");
      doRun();
      return;
    }
    isInitDone = true;

    offset = getNbVertCells() / 2;
    rLimit = getNbVertCells() / 4;
    sensor = GGComboSensor.init(this, SensorManager.SENSOR_DELAY_FASTEST);
    bubble = new Actor("bubble");
    addActor(bubble, new Location(offset, offset));
    setSimulationPeriod(30);

    // Remove the standard behavior 
    // when the [BACK], [MENU] or [VOLUME] button is hit
    addNavigationListener(this);
    
    // Prevent actors to be removed from GameGrid
    setCleanupEnabled(false);

    doRun();
    showToast("SpiritLevel ready");
  }

  public void act()
  {
    // origin of x, y in center of window
    double[] a = sensor.getOrientation(3);
    Point p = new Point(
      (int)(-0.02 * a[5] * getNbVertCells()), // Roll
      (int)(0.02 * a[4] * getNbVertCells())); // Pitch
    p = reduce(p);
    bubble.setLocation(new Location(offset + p.x, offset + p.y));
  }

  private Point reduce(Point p)
  {
    // Reduce point to limiting circle
    if (p.x * p.x + p.y * p.y > rLimit * rLimit)
      return new Point(
        (int)(rLimit * p.x / Math.sqrt(p.x * p.x + p.y * p.y)),
        (int)(rLimit * p.y / Math.sqrt(p.x * p.x + p.y * p.y)));
    else
      return p;
  }

  // Prevents killing of process
  public void onPause()
  {
    superOnPause();
    doPause();  // Must pause simulation cycling
  }

   // Callback when a device button is hit
  public void navigationEvent(GGNavigationEvent event)
  {
    switch (event)
    {
      case BACK_DOWN:
        if (!isBackPressed)
        {
          isBackPressed = true;
          showToast("Click back once more to exit");
          new Thread()
          {
            public void run()
            {
              delay(2000);
              isBackPressed = false;
            }

          }.start();
        }
        else
          finish();
        break;

      case MENU_DOWN:
        showToast("SpiritLevel by www.aplu.ch");
        break;
    }
  }
}

Download SpiritLevel app for installation on a smartphone

Create QR code to download Android app to your smartphone.

Download sources (SpiritLevel.zip).