String of Pearls

Tap or drag on the screen and watch the string of pearls dangle and wiggle. The first Pearl hangs from the touchPoint, initially the center of the screen. Each subsequent Pearl hangs from the center of its predecessor. I made the above screenshots on my Azpen A727 tablet, because I couldn’t press the Android Studio screenshot button and drag the pearls at the same time.

A Pearl object does no graphics and doesn’t even have a constructor. It’s just a holder for two fields, center and velocity. But it does do some arithmetic based on the second most famous formula in physics: force equals mass times acceleration.

F = ma
The mass is a scalar (a single number, stored as a float), which is why it’s printed in italics. The force and the acceleration are vectors (pairs of numbers, x and y, stored as a PointF), which is why they’re printed in bold. Using this formula, the method dragTowards drags the Pearl towards a point. You can tune the static fields of class Pearl to increase or decrease the gravity, mass, etc.

The BackgroundView creates an array of five Pearls. The onDraw method draws a circle and a line for each Pearl. The infinite loop (the for (;;) {) repositions the Pearls 60 times a second. It is executed by a thread that is not the UI thread.

Class Pearl does its arithmetic in units of dp’s, because I wanted the pearls to be the same size on every device and because I wanted the statement of the classic laws of physics to be uncontaminated with pixels. But class BackgroundView does its arithmetic in units of pixels, because the parameters of onTouch, drawLine, and drawCircle are in pixels. BackgroundView therefore has to convert pixels to dp’s when putting a value into touchPoint for use by Pearl, and it has to convert dp’s back to pixels when using the values that it gets back from Pearl. These simple conversions are performed by division and multiplication by density, which is the number of pixels per dp.

Source code in Pearl.zip

  1. MainActivity.java: onCreate creates a BackgroundView.
  2. BackgroundView.java
  3. Pearl.java
  4. activity_main.xml: unused and ignored.
  5. AndroidManifest.xml
  6. build.gradle (Module: app).

The same program, in HTML5

Point your browser at html5.html and click and drag on the screen. Then select View Source or Page Source. In Safari, it’s the Develop menu. In Chrome, it’s in the View menu.

Things to try

  1. Do you occasionally get the following message from LogCat?
    03-20 08:34:00.772    1382-1382/edu.nyu.scps.pearl I/Choreographer: Skipped 32 frames!
    The application may be doing too much work on its main thread.
    
  2. Is the getCenter method of class Pearl a security risk? It returns a reference to a private field of the class, and the caller could use that reference to change the value of the field. Is there a way to prevent this? Is any other method a risk to the security of this field?

  3. Make the pearls respond to the real gravity in the room and to the acceleration of the device.
    1. The local variable backgroundView in the onCreate method of MainActivity will have to be mentioned in more than one method of this class, so make it a field of the class.
          private BackgroundView backgroundView;
      
      In onCreate, change
              BackgroundView backgroundView = new BackgroundView(this);
      
      to
              backgroundView = new BackgroundView(this);
      
    2. Add two fields to class MainActivity:
          private SensorManager sensorManager;	//Can't initialize them here.
          private Sensor sensor;
      
      Initialize them in onCreate before creating the BackgroundView.
              sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
              sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
      
    3. The MainActivity will be the listener that will be plugged into the sensor.
      public class MainActivity extends AppCompatActivity implements SensorEventListener {
      
      Listening to the sensor is expensive. The MainActivity should listen only when the MainActivity is in the foreground. Add the following two lifecycle methods to class MainActivity. To get Android Studio to type some of the code in for you, click on the word MainActivity in the file MainActivity.java and pull down
      Code → Override Methods…
      Select Methods to Override/Implement
      Press the a–z button for alphabetical order, select onResume, and press OK.
          @Override
          protected void onResume() {
              super.onResume();
              if (sensor != null) {
                  sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);
              }
          }
      
          @Override
          protected void onPause() {
              super.onPause();
              if (sensor != null) {
                  sensorManager.unregisterListener(this);
              }
          }
      
      What should the listener (which is the MainActivity object) do when it hears an acceleration? Add the following two methods to the listener. Can we simplify the switch by calling remapCoordinateSystem? See One Screen Turn Deserves Another. Alternatively, we could have kept the Activity object alive during a change of orientation.
          @Override
          public void onSensorChanged(SensorEvent sensorEvent) {
              //Use only two out of the three values.
              float x = sensorEvent.values[0];
              float y = sensorEvent.values[1];
      
              WindowManager windowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
              Display defaultDisplay = windowManager.getDefaultDisplay();
              int rotation = defaultDisplay.getRotation();
      
              switch (rotation) {
                  case Surface.ROTATION_0:
                      backgroundView.setAcceleration(-x, y);
                      break;
                  case Surface.ROTATION_90:
                      backgroundView.setAcceleration(y, x);
                      break;
                  case Surface.ROTATION_180:
                      backgroundView.setAcceleration(x, -y);
                      break;
                  case Surface.ROTATION_270:
                      backgroundView.setAcceleration(-y, -x);
                      break;
                  default:
                      break;
              }
          }
      
          @Override
          public void onAccuracyChanged(Sensor sensor, int accuracy) {
          }
      
    4. Give class BackgroundView the following field, with a setter and getter.
          private float acceleration[] = {0f, 9.81f};  //Gravity downwards by default.
      
          public void setAcceleration(float x, float y) {
              acceleration[0] = x;
              acceleration[1] = y;
          }
      
          public float getAcceleration(int i) {
              if (i == 0 || i == 1) {
                  return acceleration[i];
              }
              return 0f;
          }
      
    5. Give class Pearl a new field. The class will now need a constructor to initialize the new field. The constructor for class BackgroundView will pass this as an argument to the constructor for class Pearl.
          private BackgroundView backgroundView;
      
          Pearl(BackgroundView backgroundView) {
              this.backgroundView = backgroundView; //this.backgroundView is the field, backgroundView is the argument
          }
      
      The force in drawTowards will now depend on the horizontal and vertical g-forces experienced by the Android device.
              final PointF force = new PointF(
                      (p.x - center.x) * elasticity + backgroundView.getAcceleration(0) * gravity * mass,
                      (p.y - center.y) * elasticity + backgroundView.getAcceleration(1) * gravity * mass
              );
      
      On my Azpen A727 tablet, I tuned the value of the static field Pearl.gravity from 3.0f to .3f.

      Java programmers should note that we should have created an interface containing the methods setAcceleration and getAcceleration. Instead of containing a reference to a BackgroundView, a Pearl should have contained a reference to an object that implements this interface.