|
|||||
Measuring Acceleration with Android
AccelerometerTest is an Android app that records the acceleration of the phone, using the built-in accelerometer. It shows the
instantaneous acceleration for the X, Y, and Z axises, as well as the total acceration. In addition, it allows you to record the
acceleration of the phone over a period of time and obtain the highest acceleration in that interval, as well as a graph of the
acceleration.
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. 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 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> |