Mr

 Mr. Rogers' IB Computer Science -- Android SmartPhone Programming Project
Help boost K-12 science, math, and computer education. Donate your obsolete Android phone to us (see below)! We promise to put it to good use in a classroom.

 

Mr Rogers Android Project Menu

Home

Projects

2010-2011


   3D Compass

   3D Accelerometer

   Friction Tester

   Drum

   Spherical Game

   Level

 


Greenville, SC

Measuring Acceleration with Android

AccelerometerTest

Status

Completely functional, but lacking some polish

Purpose

To enable the recording and viewing of acceleration over time

Features
  • Records acceleration on the X, Y, and Z axises, as well as total acceleration.
  • Displays instantaneous X, Y, Z, and total acceleration, as well as the highest recorded total acceleration.
  • The accerometer can be zero-calibrated via a menu option
  • Graphs the total acceleration on a acceleration vs. time graph, also displays the mean acceleration
  • The graph can be touched to display the acceleration at the y-value of the touch
Potential Features
  • A more effective method of calibration could be used
  • The phone could relay acceleration data to a PC program for more advanced analysis and visualization of data, storage of data, and viewing multiple sets of data from multiple phones simultaneously
  • Some of the aforementioned features could also be added to the phone application
  • The graph could be optimized for better display on different devices
Instructions

When the AccelerometerTest application is opened, it will display the X, Y, and Z acceleration, and total (T) acceleration, which is equal to √X2+Y2+Z2 - g (so it should read close to zero when the phone is stationary). To begin recording acceleration, press the "Start Recording" button; press it again to stop. Alternately, if the phone undergoes an acceleration of more than 10g while not recording, it will record acceleration for the next second (good for measuring the force of punches). While recording, the H label will display the highest total acceleration recorded thus far in that recording. The application can hold/view one recording at a time.

The accelerometer can be calibrated using the "Calibrate..." option in the application menu. Selected, this will bring up another menu from which one can choose the calibrate either the X, Y, or Z axises. The selected axis will be zero-calibrated, i.e. adjusted so that the acceleration reads zero at the current acceleration of the phone on that axis. It unfortunately will not read *exactly* zero, due to the limitations of the accelerometer itself. The calibration of the axises can be reset by selecting "Reset" from the application menu.

The third option on the application menu, "Switch View", will switch between the initial view and the graph view. The graph view displays an acceleration vs. time graph for the last acceleration recording. The horizontal red line on the graph marks the mean acceleration. When touched, the graph will display a horizontal black line and label that marks the y (acceleration) value at that point. This is useful for determining acceleration values that aren't easy to measure with the labels on the edges of the graph.

Class AccelerometerTest

Every Android app has at least one Activity class, i.e. a class that extends android.app.Activity. One of those activity classes is started when the application is launched. In this application, the Activity class is AccelerometerTest. It handles application lifecycle events (onCreate(), onStop(), etc.) as well as creating and managing menus, among other things. In this case, android.hardware.SensorEventListener is implemented so the application can also recieve sensor events - such as changes in acceleration from the accelerometer.
package com.test.accel;

import android.app.Activity;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.util.FloatMath;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.TextView;
import android.widget.ViewFlipper;

/**
 * The primary class for this application
 * @author Stephan Williams
 *
 */
public class AccelerometerTest extends Activity implements SensorEventListener {

//      These are the values used for calibration.
	private float dx = 0;
	private float dy = 0;
	private float dz = 0;

//      Keeps track of the recording start times so
//      the origin of the graph can be kept at t=0
	private long timeStart = 0;

//      holds the last sensor event, used for calibration
	SensorEvent lastEvent;

//      This is my subclass of CountDownTimer, which adds some convenience
//      methods for checking if the timer is finished
	MyCountDownTimer timer = new MyCountDownTimer(0, 0);

	private float highAccel = 0;

    @Override
    public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	requestWindowFeature(Window.FEATURE_NO_TITLE);
	setContentView(R.layout.main);

//      Sets up this class (which implements SensorEventListener) to recieve
//      sensor events, specifically from the accelerometer.
	SensorManager manager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
	Sensor accel = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
	manager.registerListener(this, accel, SensorManager.SENSOR_DELAY_GAME);

//      Sets up the actions for the "Start Recording" button
	((Button)findViewById(R.id.startTimer)).setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
//                              When clicked, if the timer is not running (meaning the app is not recording),
//                              clear the graph, change the button text to "Stop Recording", and reset the 
//                              timer and highest acceleration values.
				if (!timer.isRunning()) {
					((GraphView)findViewById(R.id.graph)).clearPoints();
					((Button)findViewById(R.id.startTimer)).setText(R.string.stopTimer);
					timeStart = 0;
					highAccel = 0;
					timer = new MyCountDownTimer(Long.MAX_VALUE, 100);
					timer.start();
				} else {
					((Button)findViewById(R.id.startTimer)).setText(R.string.startTimer);
					timer.cancelTimer();
				}
			}
		});
    }

	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) {

	}

	public void onStop() {
		super.onStop();
		((Button)findViewById(R.id.startTimer)).setText(R.string.startTimer);
		timer.cancelTimer();
	}

	@Override
	public void onSensorChanged(SensorEvent event) {
		lastEvent = event;
		TextView xAccel = ((TextView)findViewById(R.id.x_accel));
		TextView yAccel = ((TextView)findViewById(R.id.y_accel));
		TextView zAccel = ((TextView)findViewById(R.id.z_accel));
		TextView tAccel = ((TextView)findViewById(R.id.all_accel));
		TextView hAccel = ((TextView)findViewById(R.id.high_accel));

		xAccel.setText("X: " + (event.values[0] - dx));
		yAccel.setText("Y: " + (event.values[1] - dy));
		zAccel.setText("Z: " + (event.values[2] - dz));

		float totalAccel = FloatMath.sqrt((event.values[0] - dx) * (event.values[0] - dx) +
						  (event.values[1] - dy) * (event.values[1] - dy) +
						  (event.values[2] - dz) * (event.values[2] - dz)) - SensorManager.GRAVITY_EARTH;

		tAccel.setText("T: " + totalAccel);
		if (timer.isRunning()) {
//                      sets the start time of recording
			if (timeStart == 0) timeStart = event.timestamp;
			if (totalAccel > highAccel) highAccel = totalAccel;
			hAccel.setText("H: " + highAccel);
//                      add the point to the graph, converting the nanoseconds from the timer to seconds
			((GraphView)findViewById(R.id.graph)).addPoint((float)((event.timestamp - timeStart) / 1.0E9), totalAccel);
		} else if (totalAccel > 10) {
//                      for the "punch detector" feature, records high accelerations
//                      if not already recording.
			((GraphView)findViewById(R.id.graph)).clearPoints();
			timeStart = 0;
			highAccel = 0;
			timer = new MyCountDownTimer(1000, 100);
			timer.start();
		}
	}

	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.menu, menu);
		return true;
	}

	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
//              gets which menu item was pressed to calibrate,
//              uses the last recorded accelerometer value to zero
//              the display value
		case (R.id.calX):
			dx = lastEvent.values[0];
			break;
		case (R.id.calY):
			dy = lastEvent.values[1];
			break;
		case (R.id.calZ):
			dz = lastEvent.values[2];
			break;
//              reset and view switching buttons
		case (R.id.reset):
			dx = dy = dz = 0;
			break;
		case (R.id.switchView):
			((ViewFlipper)findViewById(R.id.details)).showNext();
			break;
                default:
			break;
		}
		return super.onOptionsItemSelected(item);
	}
}
	    

Class GraphView

A class extending android.view.View, the base class for UI components. Displays a graph. The graph is currently not resolution independent, though it would definitely need to be made so should this app ever be released to the Android market.
Making this class was the first time I can remember having to optimize a program for speed issues; the graph originally had a ~1 second lag redrawing. It turned out that the slowdown was caused by String.format(), which I had been using to constrain the labels to a certain number of decimal places. This was eventually fixed by simply rounding the number down a few decimal places (see the round() method at the end), which had a similar effect, and was substantially faster.
package com.test.accel;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnTouchListener;

/**
 * A basic graph view. Can be set to either fit the graph bounds to
 * all the given points, or use user-defined bounds. It displays the
 * mean of the points, as well as
 * @author Stephan Williams
 *
 */
public class GraphView extends View implements OnTouchListener {
        private List<PointF> points;
        private float yInt;
        private float xInt;
        private RectF window;
        private boolean fitToScreen;
        private float touchAt;

        public GraphView(Context context, AttributeSet attrs) {
                super(context, attrs);
                this.setOnTouchListener(this);
                points = new ArrayList<PointF>();
                yInt = 1;
                xInt = 1;
                window = new RectF(-10, -10, 10, 10);
                fitToScreen = true;
                touchAt = 1000;
        }
        @Override
        public void onDraw(Canvas canvas) {
                Paint white = new Paint() {{ setColor(0xFFFFFFFF); setTextSize(15); setAntiAlias(true); }};
                Paint gray = new Paint() {{ setColor(0xFFDDDDDD); }};
                Paint redAA = new Paint() {{ setColor(0xFFFF0000); setTextSize(15); setAntiAlias(true); }};
                Paint blackAA = new Paint() {{ setColor(0xFF000000); setTextSize(15); setAntiAlias(true); }};
                Paint red = new Paint() {{ setColor(0xFFFF0000); setTextSize(15); }};
                Paint black = new Paint() {{ setColor(0xFF000000); setTextSize(15); }};

//              bounds represents the coordinate system of the graph,
//              clip represents the graph relative to the screen.
                RectF bounds = fitToScreen ? createBoundsRect(points, 1) : window;
                RectF clip = new RectF(50, 50, 320, 455);

                canvas.drawRect(clip, white);
//              prevent NullPointerExceptions
                if (points.size() == 0) return;

//              draw y axis labels
                canvas.rotate(90, 160, 160);
                for (float i = Math.min(bounds.top, bounds.bottom);
   i <= Math.max(bounds.top, bounds.bottom);
   i += fitToScreen ? (Math.abs(bounds.top - bounds.bottom) / 15f) : Math.abs(yInt)) {
                        float off = mapPoint(new PointF(0, i), bounds, clip).x;
                        canvas.drawText("" + round(i), 10, 325 - off, white);
                        canvas.drawLine(48, 320 - off, 455, 320 - off, gray);
                }

                canvas.rotate(-90, 160, 160);

//              draw x axis labels
                for (float i = Math.min(bounds.left, bounds.right);
   i <= Math.max(bounds.left, bounds.right);
   i += fitToScreen ? (Math.abs(bounds.left - bounds.right) / 25) : Math.abs(xInt)) {
                        float off = mapPoint(new PointF(i, 0), bounds, clip).y;
                        canvas.drawText("" + round(i), 10, off + 5, white);
                        canvas.drawLine(48, off, 320, off, gray);
                }

                canvas.clipRect(clip);
//              draw mean
                float temp = mapPoint(new PointF(0, getAverageY()), bounds, clip).x;
                canvas.drawLine((int)temp, clip.top, (int)temp, clip.bottom, red);
                canvas.rotate(90, 160, 160);
                canvas.drawText("" + round(getAverageY()), clip.top + 2, clip.right - temp - 2, redAA);
                canvas.rotate(-90, 160, 160);

//              draw graph
                for (int i = 1; i < points.size(); i++) {
                        PointF p1 = mapPoint(points.get(i - 1), bounds, clip);
                        PointF p2 = mapPoint(points.get(i), bounds, clip);
                        canvas.drawLine(p1.x, p1.y, p2.x, p2.y, new Paint() {{ setColor(0xFF0000FF); }});
                }

//              draw trace line
                temp = mapPoint(new PointF(touchAt, 0), clip, bounds).y;
                canvas.drawLine(touchAt, clip.top, touchAt, clip.bottom, black);
                canvas.rotate(90, 160, 160);
                canvas.drawText(String.format("%3.2f", temp), clip.top + 2, clip.right - touchAt - 2, blackAA);
                canvas.rotate(-90, 160, 160);

        }
        /**
         * Replaces the existing points in the graph with
         * ones from a list
         * @param p the list of points
         */
        public void setPoints(List<PointF> p) {
                points = p;
                sortPoints();
        }
        /**
         * Adds a list of points to the existing points
         * @param p this list of points to add
         */
        public void addPoints(List<PointF> p) {
                points.addAll(p);
                sortPoints();
        }
        /**
         * Adds a single point
         * @param p the point to add
         */
        public void addPoint(PointF p) {
                points.add(p);
                sortPoints();
        }
        /**
         * Add a point from x and y coordinates, instead
         * of from a PointF object
         * @param x
         * @param y
         */
        public void addPoint(float x, float y) {
                points.add(new PointF(x, y));
                sortPoints();
        }
        /**
         * Removes all points from the graph
         */
        public void clearPoints() {
                points.clear();
        }
        /**
         * Sets the user-defined x interval for the graph.
         * @see #setFitToScreen(boolean)
         * @param xint
         */
        public void setXInterval(float xint) {
                xInt = Math.abs(xint);
        }
        /**
         * @see #setXInterval(float)
         * @see #setFitToScreen(boolean)
         * @return the user-defined x interval for the graph
         */
        public float getXInterval() {
                return xInt;
        }
        /**
         * Sets the user-defined y interval for the graph.
         * @see #setFitToScreen(boolean)
         * @param yint
         */
        public void setYInterval(float yint) {
                yInt = Math.abs(yint);
        }
        /**
         * @see #setYInterval(float)
         * @see #setFitToScreen(boolean)
         * @return the user-defined y interval for the graph
         */
        public float getYInterval() {
                return yInt;
        }
        /**
         * Sorts the list of points in ascending order, by x value
         */
        private void sortPoints() {
                Collections.sort(points, new Comparator<PointF>() {
                        @Override
                        public int compare(PointF p1, PointF p2) {
                                return Float.compare(p1.x, p2.x);
                        }
                });
        }
        /**
         * Creates a rectangle that completely encloses all points
         * in the given list, with the specified border around them.
         * The border is taken as the same units as the graph.
         * @param points a list of points
         * @param border the width of the border to put around the points
         * @return a rectangle enclosing all given points
         */
        private RectF createBoundsRect(List<PointF> points, float border) {
                if (points.size() == 0) return new RectF();
                float ymin = points.get(0).y;
                float ymax = points.get(0).y;
                for (PointF p : points) {
                        if (p.y < ymin) ymin = p.y;
                        if (p.y > ymax) ymax = p.y;
                }
                return new RectF(points.get(0).x - border,
                                                 ymin - 2 *border,
                                                 points.get(points.size() - 1).x + border,
                                                 ymax + 2 * border);
        }
        /**
         * Maps a point from one rectangle to another, so it appears in
         * the same relative spot
         * @param p the point to map
         * @param srcRect
         * @param destRect
         * @return the mapped point
         */
        private PointF mapPoint(PointF p, RectF srcRect, RectF destRect) {
                return new PointF((p.y - srcRect.top) / (srcRect.bottom - srcRect.top) *
                                (destRect.right - destRect.left) + destRect.left,
                                (p.x - srcRect.left) / (srcRect.right - srcRect.left) *
                                (destRect.bottom - destRect.top) + destRect.top);
        }
        /**
         *
         * @return the average y value of the points in the graph
         */
        public float getAverageY() {
                if (points.size() == 0) return 0;
                float temp = 0;
                for (PointF pf : points) temp += pf.y;
                return temp / points.size();
        }
        /**
         * Sets whether the graph should fit all the points on the
         * graph, or use the user-defined bounds and intervals.
         * @param fit
         */
        public void setFitToScreen(boolean fit) {
                fitToScreen = fit;
        }
        /**
         * @see #setFitToScreen(boolean)
         * @return
         */
        public boolean getFitToScreen() {
                return fitToScreen;
        }
        /**
         * Sets the user-defined viewing window for the graph.
         * @see #setFitToScreen(boolean)
         * @param rect
         */
        public void setWindow(RectF rect) {
                window = rect;
                window.sort();
        }
        /**
         * @see #setFitToScreen(boolean)
         * @see #setWindow(RectF)
         * @return the user-defined window for the graph
         */
        public RectF getWindow() {
                return window;
        }
        @Override
        public boolean onTouch(View view, MotionEvent e) {
                touchAt = e.getX();
                postInvalidate();

                return true;
        }
        private float round(float f) {
                return (int)(f * 100f) / 100f;
        }
}

Class MyCountDownTimer

Because of the way this program is structured in AccelerationTest, a timer was needed that could be asked whether or not it had finished. Because no such timer with that functionality existed in the Android API, I subclassed the closest available - android.os.CountDownTimer - and added a few simple methods.
package com.test.accel;

import android.os.CountDownTimer;
import android.widget.ViewFlipper;
/**
 * Subclass of CountDownTimer with methods added for 
 * telling if the timer has stopped.
 * @author Stephan Williams
 *
 */
public class MyCountDownTimer extends CountDownTimer {

        private boolean running = false;

        public MyCountDownTimer(long millisInFuture, long countDownInterval) {
                super(millisInFuture, countDownInterval);
        }

        @Override
        public void onFinish() {
                running = false;
        }

        @Override
        public void onTick(long millisUntilFinished) {
                running = true;
        }

        public boolean isRunning() {
                return running;
        }

        public void cancelTimer() {
                running = false;
                cancel();
        }

}

/res/layout/main.xml

Although UIs in Android apps can be created programmatically, it is considered better practice to design them using XML, where they can then be referenced (and instantiated) using the autogenerated resource class, R. This xml file lays out the entire UI for this program (sans menu), using a ViewFlipper to switch between the primary and graph views.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <ViewFlipper android:id="@+id/details"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
                >
                <LinearLayout android:orientation="vertical"
                        android:layout_width="fill_parent"
                        android:layout_height="fill_parent"
                        >
                        <TextView  
                            android:layout_width="fill_parent" 
                            android:layout_height="wrap_content" 
                            android:textSize="20pt"
                            android:id="@+id/x_accel"
                            android:singleLine="true"
                            />
                        <TextView  
                            android:layout_width="fill_parent" 
                            android:layout_height="wrap_content" 
                            android:textSize="20pt"
                            android:id="@+id/y_accel"
                            android:singleLine="true"
                            />
                        <TextView  
                            android:layout_width="fill_parent" 
                            android:layout_height="wrap_content" 
                            android:textSize="20pt"
                            android:id="@+id/z_accel"
                            android:singleLine="true"
                            />
                        <TextView  
                            android:layout_width="fill_parent" 
                            android:layout_height="wrap_content" 
                            android:textSize="20pt"
                            android:id="@+id/all_accel"
                            android:singleLine="true"
                            />
                        <TextView  
                            android:layout_width="fill_parent" 
                            android:layout_height="wrap_content" 
                            android:textSize="20pt"
                            android:id="@+id/high_accel"
                            android:singleLine="true"
                            android:text="H:"
                            />
                        <Button
                                android:layout_width="fill_parent"
                                android:layout_height="fill_parent"
                                android:id="@+id/startTimer"
                                android:text="@string/startTimer"
                                android:textSize="14pt"
                                />
                </LinearLayout>
                <LinearLayout android:orientation="vertical"
                        android:layout_width="fill_parent"
                        android:layout_height="fill_parent"
                        >
                        <com.test.accel.GraphView
                                android:layout_width="fill_parent"
                                android:layout_height="fill_parent"
                                android:id="@+id/graph"
                                />
                </LinearLayout>
        </ViewFlipper>
</LinearLayout>

/res/menu/menu.xml

Like the rest of the UI, the application menu can also be created with XML, with the code behind it going in the Activity class.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:title="Calibrate..."
              android:id="@+id/calibrate">
                <menu>
                        <item android:title="X"
                              android:id="@+id/calX"/>
                        <item android:title="Y"
                              android:id="@+id/calY"/>
                        <item android:title="Z"
                              android:id="@+id/calZ"/>
                </menu>
        </item>
        <item android:title="Reset"
              android:id="@+id/reset"/>
        <item android:title="Switch View"
              android:id="@+id/switchView"/>
</menu>

Stephan Williams is a junior at Southside High School in Greenville, South Carolina.