|
|||||
|
Measuring Acceleration with AndroidAccelerometerTestStatus Completely functional, but lacking some polish PurposeTo enable the recording and viewing of acceleration over time Features
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 AccelerometerTestEvery 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 GraphViewA 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.
Class MyCountDownTimerBecause 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.xmlAlthough 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.xmlLike 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. |