Friday, August 13, 2010

Implement a Simple Compass using SensorManager and SensorEventListener

Refer to the last exercise "Detect rotation around X, Y & Z axis, using SensorManager and SensorEventListener", Azimuth can be used to implement compass.

Simple Compass SensorManager and SensorEventListener

Create a custom view, MyCompassView, extends View. It display the drawing of our compass.
MyCompassView.java
package com.exercise.AndroidCompass;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class MyCompassView extends View {

private float direction = 0;
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private boolean firstDraw;

public MyCompassView(Context context) {
super(context);
// TODO Auto-generated constructor stub
init();
}

public MyCompassView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
init();
}

public MyCompassView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
init();
}

private void init(){

paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3);
paint.setColor(Color.WHITE);
paint.setTextSize(30);

firstDraw = true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}

@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub

int cxCompass = getMeasuredWidth()/2;
int cyCompass = getMeasuredHeight()/2;
float radiusCompass;

if(cxCompass > cyCompass){
 radiusCompass = (float) (cyCompass * 0.9);
}
else{
 radiusCompass = (float) (cxCompass * 0.9);
}
canvas.drawCircle(cxCompass, cyCompass, radiusCompass, paint);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);

if(!firstDraw){

 canvas.drawLine(cxCompass, cyCompass,
   (float)(cxCompass + radiusCompass * Math.sin((double)(-direction) * 3.14/180)),
   (float)(cyCompass - radiusCompass * Math.cos((double)(-direction) * 3.14/180)),
   paint);

 canvas.drawText(String.valueOf(direction), cxCompass, cyCompass, paint);
}

}

public void updateDirection(float dir)
{
firstDraw = false;
direction = dir;
invalidate();
}

}


Modify the layout file, main.xml, to add MyCompassView.
<?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"
 >
<TextView
 android:layout_width="fill_parent"
 android:layout_height="wrap_content"
 android:text="@string/hello"
 />
<view
class="com.exercise.AndroidCompass.MyCompassView"
android:id="@+id/mycompassview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>


Modify the main code, AndroidCompass.java, to handle SensorManager and SensorEventListener.
package com.exercise.AndroidCompass;

import java.util.List;

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.widget.Toast;

public class AndroidCompass extends Activity {

private static SensorManager mySensorManager;
private boolean sersorrunning;
private MyCompassView myCompassView;

 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.main);
  
     myCompassView = (MyCompassView)findViewById(R.id.mycompassview);
  
     mySensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
     List<Sensor> mySensors = mySensorManager.getSensorList(Sensor.TYPE_ORIENTATION);
  
     if(mySensors.size() > 0){
      mySensorManager.registerListener(mySensorEventListener, mySensors.get(0), SensorManager.SENSOR_DELAY_NORMAL);
      sersorrunning = true;
      Toast.makeText(this, "Start ORIENTATION Sensor", Toast.LENGTH_LONG).show();
    
     }
     else{
      Toast.makeText(this, "No ORIENTATION Sensor", Toast.LENGTH_LONG).show();
      sersorrunning = false;
      finish();
     }
 }

 private SensorEventListener mySensorEventListener = new SensorEventListener(){

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
 // TODO Auto-generated method stub

}

@Override
public void onSensorChanged(SensorEvent event) {
 // TODO Auto-generated method stub
 myCompassView.updateDirection((float)event.values[0]);
}
 };

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();

if(sersorrunning){
 mySensorManager.unregisterListener(mySensorEventListener);
}
}

}


Modify AndroidManifest.xml to disable the auto-rotate feature, otherwise it will point to wrong direction.

download filesDownload the files.

Related Article:
Implement a Simple Horizontal Indicator using SensorManager and SensorEventListener

9 comments:

Denis Volokh said...

Thank you for this example

Pedro Teixeira said...

great tutorial!

Is there anyway to tweek the direction to something I want to use?
I want my direction instead of being north, to be defined by this formula :
(float) Math.toDegrees(Math.atan2(mLongitude - picLongitude, mLatitude - picLatitude));

But im not able to change it, and keep the values working.. picLongitude and picLatitude are static, the others would depend on the user position.

Pedro Teixeira said...

anu ideia for this?

Erik said...

Sorry PedroTeixeira,

I have no idea right now.

Unknown said...

This example is fantastic- I need the compass to work only on Landscape view though- any idea on how to make that happen?

Erik said...

Hello Nick,

Refer here: How to disable the auto-rotate feature?

widhiecyber said...

thank you very much for this sample
this is the great tutorial^^

Unknown said...

After looking thru your code, I'm thinking it's incorrect.

There is this suspiciously simple method:

public void onSensorChanged(SensorEvent event) {
 // TODO Auto-generated method stub
 myCompassView.updateDirection((float)event.values[0]);
}



Basically, it seems like you are using the x component of the force vector as if it were an angle.  Can you look at this and let me know what you think?

Unknown said...

Never mind my last post.

I was using Sensor.TYPE_MAGNETIC_FIELD.