Orientation

Let's set up the layout for our app! activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.tiltorientation.MainActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/label_group"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <TextView
            android:id="@+id/label_azimuth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/label_azimuth_string"
            style="@style/Label"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="@+id/label_group"/>

        <TextView
            android:id="@+id/label_pitch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/label_pitch_string"
            style="@style/Label"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/label_azimuth"/>

        <TextView
            android:id="@+id/label_roll"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/label_roll_string"
            style="@style/Label"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/label_pitch"/>

        <TextView
            android:id="@+id/value_azimuth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:layout_marginStart="8dp"
            android:text="@string/value_format"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            app:layout_constraintLeft_toRightOf="@id/label_azimuth"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/label_group"/>

        <TextView
            android:id="@+id/value_pitch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/value_format"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/value_azimuth"/>

        <TextView
            android:id="@+id/value_roll"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/value_format"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/value_pitch"/>
    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

Add the following code in styles.xml

<style name="Label" parent="Base.TextAppearance.AppCompat.Medium">
    <item name="android:textStyle">bold</item>
</style>

This line locks the activity in portrait mode, to prevent the app from automatically rotating the activity as you tilt the device.

setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

Declaring our Sensors ,SensorManagersand our TextView

in top of MainActivity,class

private SensorManager mSensorManager;
private Sensor accelerometer;
private Sensor magneticField;
private TextView mTextSensorAzimuth;
private TextView mTextSensorPitch;
private TextView mTextSensorRoll;

We will also add float arrays so we can have copies of our sensor data

private float[] mAccelerometerData = new float[3];
private float[] mMagnetometerData = new float[3];

in onCreate()

mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
accelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
magneticField = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
mTextSensorAzimuth = (TextView) findViewById(R.id.value_azimuth);
mTextSensorPitch = (TextView) findViewById(R.id.value_pitch);
mTextSensorRoll = (TextView) findViewById(R.id.value_roll);

Registering Listeners on our device if we can find them

@Override
    protected void onStart() {
        super.onStart();

        if (mSensorAccelerometer != null) {
            mSensorManager.registerListener(this, mSensorAccelerometer,
                    SensorManager.SENSOR_DELAY_NORMAL);
        }
        if (mSensorMagnetometer != null) {
            mSensorManager.registerListener(this, mSensorMagnetometer,
                    SensorManager.SENSOR_DELAY_NORMAL);
        }
    }

Unregistering Listeners on our device when we close the app (for Battery Life!)

@Override
    protected void onStop() {
        super.onStop();

        // Unregister all sensor listeners in this callback so they don't
        // continue to use resources when the app is stopped.
        mSensorManager.unregisterListener(this);
    }

finally implement the SensorEventListener so we can register changes

public class MainActivity extends AppCompatActivity implements SensorEventListener {

We want to generate a rotation matrix from the raw accelerometer and magnetometer data. which helps to use in the next step to get the device orientation.

Rotation Matrix

A rotation matrix is a linear algebra term that translates the sensor data from one coordinate system to another—in this case, from the device's coordinate system to the Earth's coordinate system. That matrix is an array of nine float values, because each point (on all three axes) is expressed as a 3D vector.

The device-coordinate system is a standard 3-axis (x, y, z) coordinate system relative to the device's screen when it is held in the default or natural orientation. Most sensors use this coordinate system. In this orientation:

  • The x-axis is horizontal and points to the right edge of the device.

  • The y-axis is vertical and points to the top edge of the device.

  • The z-axis extends up from the surface of the screen. Negative z values are behind the screen.

The Earth's coordinate system is also a 3-axis system, but relative to the surface of the Earth itself. In the Earth's coordinate system:

  • The y-axis points to magnetic north along the surface of the Earth.

  • The x-axis is 90 degrees from y, pointing approximately east.

  • The z-axis extends up into space. Negative z extends down into the ground.

A reference to the array for the rotation matrix is passed into the getRotationMatrix() method and modified in place. The second argument to getRotationMatrix() is an inclination matrix, which you don't need for this app. You can use null for this argument.

The getRotationMatrix() method returns a boolean (the rotationOK variable), which is true if the rotation was successful. The boolean might be false if the device is free-falling (meaning that the force of gravity is close to 0), or if the device is pointed very close to magnetic north. The incoming sensor data is unreliable in these cases, and the matrix can't be calculated. Although the boolean value is almost always true, it's good practice to check that value anyhow.

  • Azimuth: The direction (north/south/east/west) the device is pointing. 0 is magnetic north.

  • Pitch: The top-to-bottom tilt of the device. 0 is flat.

  • Roll: The left-to-right tilt of the device. 0 is flat.

All three angles are measured in radians, and range from -π (-3.141) to π.

@Override
public void onSensorChanged(SensorEvent event) {
    int sensorType = event.sensor.getType();
    switch (sensorType) {
        case Sensor.TYPE_ACCELEROMETER:
            mAccelerometerData = event.values.clone();
            break;
        case Sensor.TYPE_MAGNETIC_FIELD:
            mMagnetometerData = event.values.clone();
            break;
        default:
            return;
    }
    float[] rotationMatrix = new float[9];
    boolean rotationOK = SensorManager.getRotationMatrix(rotationMatrix, null, mAccelerometerData, mMagnetometerData)
    float orientationValues[] = new float[3];
    if (rotationOK) {
        SensorManager.getOrientation(rotationMatrix, orientationValues);
    };
    float azimuth = orientationValues[0];
    float pitch = orientationValues[1];
    float roll = orientationValues[2];
    mTextSensorAzimuth.setText(getResources().getString(R.string.value_format, azimuth));
    mTextSensorPitch.setText(getResources().getString(R.string.value_format, pitch));
    mTextSensorRoll.setText(getResources().getString(R.string.value_format, roll));
}

Run the app! The values should now update

Direction Visualization using Pitch and Roll

Create a circle shape

<shape
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:shape="oval">
   <solid android:color="@android:color/black"/>
</shape>

Let's add four circles with 50% Opacity to show

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.tiltorientation.MainActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/label_group"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <TextView
            android:id="@+id/label_azimuth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/label_azimuth_string"
            style="@style/Label"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="@+id/label_group"/>

        <TextView
            android:id="@+id/label_pitch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/label_pitch_string"
            style="@style/Label"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/label_azimuth"/>

        <TextView
            android:id="@+id/label_roll"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/label_roll_string"
            style="@style/Label"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/label_pitch"/>

        <!-- Values -->
        <TextView
            android:id="@+id/value_azimuth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:layout_marginStart="8dp"
            android:text="@string/value_format"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            app:layout_constraintLeft_toRightOf="@id/label_azimuth"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/label_group"/>

        <TextView
            android:id="@+id/value_pitch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/value_format"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/value_azimuth"/>

        <TextView
            android:id="@+id/value_roll"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/value_format"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/value_pitch"/>
    </androidx.constraintlayout.widget.ConstraintLayout>

    <ImageView
        android:id="@+id/spot_top"
        android:layout_width="84dp"
        android:layout_height="84dp"
        android:layout_margin="8dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:srcCompat="@drawable/spot"
        android:alpha="0.05"/>

    <ImageView
        android:id="@+id/spot_bottom"
        android:layout_width="84dp"
        android:layout_height="84dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:srcCompat="@drawable/spot"
        android:alpha="0.05" />

    <ImageView
        android:id="@+id/spot_right"
        android:layout_width="84dp"
        android:layout_height="84dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/spot"
        android:alpha="0.05"/>

    <ImageView
        android:id="@+id/spot_left"
        android:layout_width="84dp"
        android:layout_height="84dp"
        android:layout_marginLeft="8dp"
        android:layout_marginStart="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/spot"
        android:alpha="0.05" />

</androidx.constraintlayout.widget.ConstraintLayout>

In MainActivity

private ImageView mSpotTop;
private ImageView mSpotBottom;
private ImageView mSpotLeft;
private ImageView mSpotRight;
mSpotTop = (ImageView) findViewById(R.id.spot_top);
mSpotBottom = (ImageView) findViewById(R.id.spot_bottom);
mSpotLeft = (ImageView) findViewById(R.id.spot_left);
mSpotRight = (ImageView) findViewById(R.id.spot_right);

onSensorChange() : if the change is not enough, set it to 0.

Set up the alpha to 0 to prevent retaining old values.

if (Math.abs(pitch) < 0.5f) {
    pitch = 0;
}
if (Math.abs(roll) < 0.5f) {
    roll = 0;
}
mSpotTop.setAlpha(0f);
mSpotBottom.setAlpha(0f);
mSpotLeft.setAlpha(0f);
mSpotRight.setAlpha(0f);
if (pitch > 0) {
   mSpotBottom.setAlpha(pitch);
} else {
   mSpotTop.setAlpha(Math.abs(pitch));
}
if (roll > 0) {
   mSpotLeft.setAlpha(roll);
} else {
   mSpotRight.setAlpha(Math.abs(roll));
}

For the TiltSpot app you're only interested in displaying dots in response to some device tilt, not the full range. This means that you can conveniently use the radian units directly as input to the alpha.

Last updated