Compass Example

This example shows how to access sensors in a device in order to display a compass.

A screenshot of the application.

We import the classes and modules that will be needed by the application. The most relevant are the classes from the android.hardware module.

from java.lang import Math
from android.app import Activity
from android.content import Context
from android.graphics import Canvas, Paint, Path
from android.hardware import Sensor, SensorEvent, SensorEventListener, SensorManager
import android.os
from android.view import View, ViewGroup
from android.widget import LinearLayout, TextView

The CompassActivity is derived from the standard Activity class and represents the application. Android will create an instance of this class when the user runs it.

class CompassActivity(Activity):

    __interfaces__ = [SensorEventListener]

We declare that the class implements the SensorEventListener interface. This means that we need to implement the onAccuracyChanged and onSensorChanged methods.

    def __init__(self):
    
        Activity.__init__(self)

The initialisation method calls the corresponding method in the base class before requesting access to sensors and setting up the user interface.

    @args(void, [android.os.Bundle])
    def onCreate(self, bundle):
    
        Activity.onCreate(self, bundle)

We call the getSystemService method to obtain a sensor manager object which we cast to the correct type so that we can call its methods. From this object, we obtain objects that represent the magnetometer and accelerometer.

        sensorManager = self.getSystemService(Context.SENSOR_SERVICE)
        self.sensorManager = CAST(sensorManager, SensorManager)
        self.magnetometer = self.sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
        self.accelerometer = self.sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

We create a layout and two views to display information about the sensors.

        layout = LinearLayout(self)
        layout.setLayoutParams(ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.WRAP_CONTENT))
        layout.setOrientation(LinearLayout.VERTICAL)
        
        self.infoViewA = TextView(self)
        layout.addView(self.infoViewA)
        self.infoViewM = TextView(self)
        layout.addView(self.infoViewM)

If either the accelerometer or the magnetometer was not found, we write a message in the corresponding label to indicate this.

        if self.accelerometer == None:
            self.infoViewA.setText("No accelerometer found.")
        if self.magnetometer == None:
            self.infoViewM.setText("No magnetometer found.")

Before making the layout the main view in the activity, we create an instance of a custom view class and add it to the layout.

        self.compass = CompassView(self)
        layout.addView(self.compass)
        
        self.setContentView(layout)

When the activity starts or is navigated to by the user, the onResume method is called. We call the corresponding method in the base class before showing the name of each sensor in the corresponding label, and we register the activity as a listener for the two sensors if both sensors are available.

    def onResume(self):
    
        Activity.onResume(self)
        
        if self.accelerometer != None and self.magnetometer != None:
            self.infoViewA.setText(self.accelerometer.getName())
            self.sensorManager.registerListener(self, self.magnetometer,
                SensorManager.SENSOR_DELAY_UI)
            self.infoViewM.setText(self.magnetometer.getName())
            self.sensorManager.registerListener(self, self.accelerometer,
                SensorManager.SENSOR_DELAY_UI)

When the user navigates away from the activity, the onPause method is called. We call the onPause method in the base class and unregister the activity as a listener with the sensors if both sensors are available.

    def onPause(self):
    
        Activity.onPause(self)
        
        if self.accelerometer != None and self.magnetometer != None:
            self.sensorManager.unregisterListener(self)

The following two methods are required to implement the SensorEventListener interface. Although we do not handle the first one in this example, we still need to provide an implementation.

    @args(void, [Sensor, int])
    def onAccuracyChanged(self, sensor, accuracy):
        pass

The onSensorChanged method is called when either of the two sensors notifies the activity that a change has occurred. We respond to this by passing on information about the relevant sensor's new values to the custom view we created earlier. Both sensors must be available for this to occur.

    @args(void, [SensorEvent])
    def onSensorChanged(self, event):
    
        if self.accelerometer != None and self.magnetometer != None:
            if event.sensor == self.magnetometer:
                self.compass.updateField(event.values)
            elif event.sensor == self.accelerometer:
                self.compass.updateAcceleration(event.values)

The CompassView class is derived from the standard View class and is used to display the representation of a compass in the activity.

As with other views, the initialisation method accepts a Context as its parameter and initialises itself by calling the initialisation method of the View class.

class CompassView(View):

    @args(void, [Context])
    def __init__(self, context):
    
        View.__init__(self, context)

We define a paint that defines how lines will be drawn and a path that will be used to display a compass needle.

        self.paint = Paint()
        self.paint.setStrokeWidth(float(2))
        self.paint.setFlags(Paint.ANTI_ALIAS_FLAG)
        self.paint.setStyle(Paint.Style.STROKE)
        self.path = Path()

We also define and initialise some attributes that we use to hold information about the position and size of the graphics in the view, the orientation of the compass needle, and the sensor information from the accelerometer and magnetometer.

        self.x = self.y = self.r = float(0)
        self.angle = float(0)
        self.acceleration = array(float, 3)
        self.field = array(float, 3)
        self.hasAcceleration = False
        self.hasField = False

The onSizeChanged method is called when the view is first shown and whenever it changes size afterwards.

    @args(void, [int, int, int, int])
    def onSizeChanged(self, width, height, oldWidth, oldHeight):
    
        self.x = width/2
        self.y = height/2
        self.r = Math.min(self.x, self.y) * 0.8

We record the coordinates of the centre of the view and calculate a suitable radius of a circle that will fit inside it.

We also create a path to depict the compass needle that will fit inside the circle. The path has its own local coordinate system, so we add elements to it around the origin point. This will be translated and rotated into place within the view's coordinate system when we draw it.

        L = self.r * 0.9
        l = self.r * 0.1
        
        self.path.rewind()
        self.path.moveTo(0, -L)
        self.path.lineTo(l, 0)
        self.path.lineTo(0, -l)
        self.path.lineTo(-l, 0)
        self.path.close()

The updateField method is called by our activity's onSensorChanged method in response to a change in the magnetic field.

    @args(void, [[float]])
    def updateField(self, values):
    
        self.field[0] = values[0]
        self.field[1] = values[1]
        self.field[2] = values[2]
        self.hasField = True
        if self.hasAcceleration:
            self.updateNeedle()

We record the components of the field in an array and set a flag to indicate that we have valid data. If we also have a valid acceleration then we call a method to update the needle.

The updateField method is called by our activity's onSensorChanged method in response to a change in the acceleration.

    @args(void, [[float]])
    def updateAcceleration(self, values):
    
        self.acceleration[0] = values[0]
        self.acceleration[1] = values[1]
        self.acceleration[2] = values[2]
        self.hasAcceleration = True
        if self.hasField:
            self.updateNeedle()

We record the components of the acceleration in an array and set a flag to indicate that we have valid data. If we also have a valid magnetic field then we call a method to update the needle.

When we have both magnetic field and acceleration information, we call this method to update the orientation of the compass needle.

    def updateNeedle(self):
    
        R = array(float, 9)
        I = array(float, 9)

We define two arrays to hold matrices that we need for calculations.

Using a static method in the SensorManager class, we obtain the rotation and inclination matrices from the acceleration and magnetic field, assuming that the only acceleration is due to gravity.

        r = SensorManager.getRotationMatrix(R, I, self.acceleration, self.field)
        if not r:
            return

We use another static method to obtain the orientation of the device from the rotation matrix. The array we supply is filled in with angles of rotation, the first of which is the one we need.

        v = array(float, 3)
        SensorManager.getOrientation(R, v)

Finally, we convert the angle to degrees and invalidate the view, causing the onDraw method to be called.

        self.angle = 180.0 * v[0] / Math.PI
        self.invalidate()

The onDraw method is called when the view needs to be displayed. The parameter is a Canvas object that we draw onto. We call the onDraw method in the base class before adding our own decorations.

    @args(void, [Canvas])
    def onDraw(self, canvas):
    
        android.view.View.onDraw(self, canvas)

We draw a circle in the position calculated in the onSizeChanged method.

        self.paint.setARGB(255, 192, 192, 192)
        canvas.drawCircle(self.x, self.y, self.r, self.paint)

Then we translate and rotate the origin of the canvas before drawing the path depicting the compass needle, which is now rotated about the centre of the canvas.

        canvas.translate(self.x, self.y)
        canvas.rotate(-self.angle)
        self.paint.setARGB(255, 255, 192, 192)
        canvas.drawPath(self.path, self.paint)

Files