Building an Android App that Displays Live Accelerometer Data

Most mobile devices today have built-in sensors such as accelerometers, compasses, GPS and even sensors for temperature, audio and light. These sensors are used in a variety of apps from games to weather and travel apps. The availability of these sensors in mass-marketed mobile devices creates exciting new opportunities for data mining applications.

In this article, we’ll walk through how to display live accelerometer data in a simple Android seismograph app. To that end we’ll use the Telerik Chart control for Android and we’ll test how the control behaves when handling large amount of streaming data. We’ll build the app using Android Studio. Below is a video demonstrating the result we’ll get at the end.

Once you’ve set up a new project in Android Studio, you’ll need to ensure that you have the Telerik UI for Android downloaded and set up. For detailed instructions on doing this, click here. This article will focus mainly on the workflow and core mechanics of building the application.

Receiving Sensor Notifications

When working with sensors in an Android application, you must first implement the SensorEventListener interface. This interface is used for receiving notifications from the SensorManager when sensor values have changed. You will need to override a aouple of methods – onSensorChanged and onAccuracyChanged. As you may have guesssed, onAccuracyChanged is called when the accuracy of a sensor has changed and onSensorChanged is called when a sensor’s values have changed. In the case of this seismograph simulator, all the magic will happen in the onSensorChanged override, which is where the accelerometer sensor reports its data.

Let’s start by obtaining a reference to the SensorManager and the accelerometer. In order to get an instance of the default accelerometer sensor, we need to do the following:

private SensorManager mSensorManager;
private Sensor mSensor;
  ...
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

With the above in mind, any similar accelerometer app would inevitably boil down to the following basic workflow:

this is how it looks like in code.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Get an instance to the accelerometer
    this.sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
    this.accelerometer = this.sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}

@Override
public void onSensorChanged(SensorEvent event) {

    // get the new point based on the readings from the accelerometer
    SeismicDataPoint point = new SeismicDataPoint(this.framesCount++, event.values[this.currentAxisIndex]);

    // add the point to a collection of all the points that should be visible on the screen
    this.seismicActivityBuffer.add(point);

    // draw the chart with all the points that should be visible*
    this.chart = createChart(seismicActivityBuffer);

    // keep the point in another collection, for historic purposes
    this.allSeismicActivity.add(point);
}

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
    // Do nothing here.
}

The above code is simplified for example purposes.

Creating the Chart

Next let’s look at the code to create the chart.

private RadCartesianChartView createChart(Iterable dataPoints) {
    RadCartesianChartView chart = new RadCartesianChartView(this);

    LineSeries series = new LineSeries(this);
    // create category binding with the X coordinate of the accelerometer point
    DataPointBinding categoryBinding = new DataPointBinding() {
        @Override
        public Object getValue(Object o) throws IllegalArgumentException {
            return ((SeismicDataPoint) o).x;
        }
    };
    series.setCategoryBinding(categoryBinding);

    // create value binding with the Y coordinate of the accelerometer point
    DataPointBinding valueBinding = new DataPointBinding() {
        @Override
        public Object getValue(Object o) throws IllegalArgumentException {
            return ((SeismicDataPoint) o).y;
        }
    };
    series.setValueBinding(valueBinding);
    chart.getSeries().add(series);

    // feed the data to the chart
    series.setData(dataPoints);

    // configure the vertical axis
    LinearAxis vAxis = new LinearAxis(this);
    // The maximum value of the accelerometer is 20 and the minimum -20, so give a bonus 10 to the vertical axis.
    vAxis.setMaximum(30);
    vAxis.setMinimum(-30);
    chart.setVerticalAxis(vAxis);

    // configure the horizontal axis
    CategoricalAxis hAxis = new CategoricalAxis(this);
    hAxis.setShowLabels(false);
    chart.setHorizontalAxis(hAxis);

    return chart;
}

The above code illustrates how you can display data from a smartphone sensor (in this case the accelerometer) inside the Chart component. You can download the source code for this first version of the app from GitHub.

Adding the Needle

Next let’s look at how to add a “needle“. The needle will be synchronized with the Y coordinate of each incoming accelerometer (“seismic”) data point, which will create the following effect (note that the needle is the red triangle on the right of the graph):

Taking into consideration our prior workflow diagram, we can assume that the position of the needle will be updated in the onSensorChanged method.

@Override
public void onSensorChanged(SensorEvent event) {

    // get the new point based on the readings from the accelerometer
    SeismicDataPoint point = new SeismicDataPoint(this.framesCount++, event.values[this.currentAxisIndex]);

    // add the point to a collection of all the points that should be visible on the screen
    this.seismicActivityBuffer.add(point);

    // draw the chart with all the points that should be visible*
    this.chart = createChart(seismicActivityBuffer);

    // keep the point in another collection, for historic purposes
    this.allSeismicActivity.add(point);

    // update the vertical position of the needle
    this.needle.updatePosition(point.y);
}

The only thing left is to do is implement the updatePosition method referenced above that will, you guessed it, update the needle’s position. Updating the position of a view in Android can be done in several ways. In this case I’ve chosen to update the position of the needle by overriding its onDraw method. In the code snippet below, as soon as the updatePosition method is called, it invalidates the layout of the needle which subsequently triggers onDraw.

public class Needle extends View {

    public Needle(Context context, float offsetRight) {
        super(context);

        this.needlePaint = new Paint();
        this.needlePaint.setColor(Color.RED);
        this.needlePaint.setStyle(Paint.Style.FILL);

        this.needleShape = new Path();
    }

    public void updatePosition(float y) {
        invalidate();
        this.currentY = y;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 20 is the max acceleration, 30 is the max value for the vertical axis, so 2/3 of 30 is 66.6%
        float y = (float) (this.halfHeight + (((this.halfHeight * (this.currentY / MAX_ACCELERATION)) * .666) * -1));

        this.needleShape.reset();
        this.needleShape.moveTo(this.width, y - this.halfNeedleHeight);
        this.needleShape.lineTo(this.pointerLeft, y);
        this.needleShape.lineTo(this.width, y + this.halfNeedleHeight);
        this.needleShape.close();

        canvas.drawPath(this.needleShape, this.needlePaint);
    }
}

This is how you can synchronize the position of a visual element with the position of a new data point.

Conclusion

I hope you’ve seen how fun and easy it is to create complex visualizations that leverage device sensors using the Telerik UI for Android. You can download the the full source code for the example via GitHub. To get started with the Telerik Chart for Android, you can download it here or read more about UI for Android here.

Comments