Light Meter Example

This example shows how to access the light sensor on a device, if present, and read the values it reports.

A screenshot of the application.

Not all Android devices have light sensors, though it may be one of the more common sensors used on mobile devices. The available sensors on a device can be found by running the Sensor List example.

We begin by importing the classes and modules used or referred to in our code. The most relevant to this example are the sensor classes from the android.hardware module.

from java.lang import Math, String
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 LightMeterActivity 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 LightMeterActivity(Activity):

    __interfaces__ = [SensorEventListener]

The class implements the SensorEventListener interface, declaring this by including it in the list of interfaces defined by the __interfaces__ attribute. Implementing this interface involves implementing two methods that are described later.

The initialisation method simply calls the corresponding method in the base class.

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

The onCreate method calls the base class's onCreate method to help set up the activity. We obtain an object that represents the light sensor and set up the user interface.

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

Information about the available sensors is obtained from the device's sensor service. This is obtained using the getSystemService method. The instance we obtain from this method needs to be cast to a suitable type so that we can access its methods.

        sensorManager = self.getSystemService(Context.SENSOR_SERVICE)
        self.sensorManager = CAST(sensorManager, SensorManager)

The light meter is obtained by passing the appropriate constant to the method responsible for returning the default sensor for each type. The value returned may be None, so we must check it before trying to access the sensor.

        self.sensor = self.sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)

We create two labels and a custom view. We use the first TextView as a label to show the name of the sensor, or a message in case no sensor is available. The second TextView is used to report the light level. The custom view is a graphical representation of the light level.

        self.nameLabel = TextView(self)
        self.sensorLabel = TextView(self)
        self.meter = LightMeter(self)

We create a vertical layout to arrange the views, and add them to it.

        layout = LinearLayout(self)
        layout.setLayoutParams(ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.WRAP_CONTENT))
        layout.setOrientation(LinearLayout.VERTICAL)
        
        layout.addView(self.nameLabel)
        layout.addView(self.sensorLabel)
        layout.addView(self.meter)

If an light sensor was not found, we write a message in the name label to indicate that no sensor is available.

        if self.sensor == None:
            self.nameLabel.setText("No suitable sensor found.")

The layout is used as the main content in the activity.

        self.setContentView(layout)

When the activity starts, or the user navigates to it, the onResume method is called. Other than calling the corresponding method in the base class, we check whether we have access to an light meter using the value stored in the above method. If so, we show its name in the name label and register the instance of this class as a listener for it. This requires that this class implements the SensorEventListener interface.

    def onResume(self):
    
        Activity.onResume(self)
        
        if self.sensor != None:
            self.nameLabel.setText(self.sensor.getName())
            self.sensorManager.registerListener(self, self.sensor,
                SensorManager.SENSOR_DELAY_UI)

When the user navigates away from the activity the onPause method is called. We call the corresponding method in the base class and unregister the instance of this class as a listener. This prevents it from receiving updates from the sensor until it is registered again.

    def onPause(self):
    
        Activity.onPause(self)
        if self.sensor != None:
            self.sensorManager.unregisterListener(self)

The following two methods must be implemented because they are part of the SensorEventListener interface whose methods are both abstract.

The onAccuracyChanged method is used to inform the activity about changes to the accuracy of the sensor. For this simple example we ignore this and simply implement an empty method.

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

The onSensorChanged method is used to inform the activity about changes to the values supplied by the sensor. Since this method is only called if we registered the activity as a listener, we can simply read the values for the light detected by the sensor from the supplied event and write them to the labels that were created earlier.

    @args(void, [SensorEvent])
    def onSensorChanged(self, event):
    
        self.sensorLabel.setText(str(event.values[0]) + " Lux "
            "(max = " + str(self.sensor.getMaximumRange()) + ")")
        self.meter.setValue(event.values[0], self.sensor.getMaximumRange())

The LightMeter class is a view that displays a simple meter based on the values and maximum values passed to it. 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 LightMeter(View):

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

We define two Paints that will be used to draw decorations in the view.

        self.background = Paint()
        self.background.setARGB(255, 64, 64, 64)
        self.foreground = Paint()
        self.foreground.setARGB(255, 255, 255, 255)

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
        l = Math.min(width, height)
        self.y = height/2 + l*0.45
        self.r = l * 0.9
        
        x0 = width * 0.05
        x1 = width * 0.95
        
        self.startAngle = Math.acos((x0 - self.x)/self.r)
        self.maxAngle = Math.acos((x1 - self.x)/self.r) - self.startAngle
        self.angle = 0.0

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 drawing the meter's decorations and a needle.

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

We first draw the view's background showing the extent of the needle's range using the paint defined earlier.

        canvas.drawLine(self.x + (self.r * Math.cos(self.startAngle)),
                        self.y - (self.r * Math.sin(self.startAngle)),
                        self.x, self.y, self.background)
        canvas.drawLine(self.x, self.y,
                        self.x + (self.r * Math.cos(self.startAngle + self.maxAngle)),
                        self.y - (self.r * Math.sin(self.startAngle + self.maxAngle)),
                        self.background)

Then, we use the foreground paint to draw the needle.

        x = self.x + (self.r * Math.cos(self.startAngle + self.angle))
        y = self.y - (self.r * Math.sin(self.startAngle + self.angle))
        canvas.drawLine(self.x, self.y, x, y, self.foreground)

The setValue method is called by the onSensorChanged method of the activity so that the meter is updated when the light level changes.

Since we want to be able to easily see changes in the level at low intensities, we restrict the meter to a range of values, divide the level by this value and take approximately the cube root of the result to give additional weight to low values.

    @args(void, [float, float])
    def setValue(self, value, max_value):
    
        value = Math.min(value, max_value)
        self.angle = self.maxAngle * Math.pow(value/max_value, 0.333)
        
        self.invalidate()

Finally, we invalidate the view so that its onDraw method is called again, updating the display.

Files