Merge "Slight improvement (hopefully) to orientation sensing." into gingerbread
This commit is contained in:
@@ -68,7 +68,7 @@ public abstract class WindowOrientationListener {
|
||||
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
|
||||
if (mSensor != null) {
|
||||
// Create listener only if sensors do exist
|
||||
mSensorEventListener = new SensorEventListenerImpl();
|
||||
mSensorEventListener = new SensorEventListenerImpl(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,8 +109,35 @@ public abstract class WindowOrientationListener {
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
class SensorEventListenerImpl implements SensorEventListener {
|
||||
|
||||
/**
|
||||
* This class filters the raw accelerometer data and tries to detect actual changes in
|
||||
* orientation. This is a very ill-defined problem so there are a lot of tweakable parameters,
|
||||
* but here's the outline:
|
||||
*
|
||||
* - Convert the acceleromter vector from cartesian to spherical coordinates. Since we're
|
||||
* dealing with rotation of the device, this is the sensible coordinate system to work in. The
|
||||
* zenith direction is the Z-axis, i.e. the direction the screen is facing. The radial distance
|
||||
* is referred to as the magnitude below. The elevation angle is referred to as the "tilt"
|
||||
* below. The azimuth angle is referred to as the "orientation" below (and the azimuth axis is
|
||||
* the Y-axis). See http://en.wikipedia.org/wiki/Spherical_coordinate_system for reference.
|
||||
*
|
||||
* - Low-pass filter the tilt and orientation angles to avoid "twitchy" behavior.
|
||||
*
|
||||
* - When the orientation angle reaches a certain threshold, transition to the corresponding
|
||||
* orientation. These thresholds have some hysteresis built-in to avoid oscillation.
|
||||
*
|
||||
* - Use the magnitude to judge the accuracy of the data. Under ideal conditions, the magnitude
|
||||
* should equal to that of gravity. When it differs significantly, we know the device is under
|
||||
* external acceleration and we can't trust the data.
|
||||
*
|
||||
* - Use the tilt angle to judge the accuracy of orientation data. When the tilt angle is high
|
||||
* in magnitude, we distrust the orientation data, because when the device is nearly flat, small
|
||||
* physical movements produce large changes in orientation angle.
|
||||
*
|
||||
* Details are explained below.
|
||||
*/
|
||||
static class SensorEventListenerImpl implements SensorEventListener {
|
||||
// We work with all angles in degrees in this class.
|
||||
private static final float RADIANS_TO_DEGREES = (float) (180 / Math.PI);
|
||||
|
||||
@@ -125,54 +152,50 @@ public abstract class WindowOrientationListener {
|
||||
private static final int ROTATION_90 = 1;
|
||||
private static final int ROTATION_270 = 2;
|
||||
|
||||
// Current orientation state
|
||||
private int mRotation = ROTATION_0;
|
||||
|
||||
// Mapping our internal aliases into actual Surface rotation values
|
||||
private final int[] SURFACE_ROTATIONS = new int[] {Surface.ROTATION_0, Surface.ROTATION_90,
|
||||
Surface.ROTATION_270};
|
||||
private static final int[] SURFACE_ROTATIONS = new int[] {
|
||||
Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_270};
|
||||
|
||||
// Threshold ranges of orientation angle to transition into other orientation states.
|
||||
// The first list is for transitions from ROTATION_0, the next for ROTATION_90, etc.
|
||||
// ROTATE_TO defines the orientation each threshold range transitions to, and must be kept
|
||||
// in sync with this.
|
||||
// The thresholds are nearly regular -- we generally transition about the halfway point
|
||||
// between two states with a swing of 30 degrees for hysteresis. For ROTATION_180,
|
||||
// however, we enforce stricter thresholds, pushing the thresholds 15 degrees closer to 180.
|
||||
private final int[][][] THRESHOLDS = new int[][][] {
|
||||
// We generally transition about the halfway point between two states with a swing of 30
|
||||
// degrees for hysteresis.
|
||||
private static final int[][][] THRESHOLDS = new int[][][] {
|
||||
{{60, 180}, {180, 300}},
|
||||
{{0, 30}, {195, 315}, {315, 360}},
|
||||
{{0, 45}, {45, 165}, {330, 360}},
|
||||
{{0, 30}, {195, 315}, {315, 360}}
|
||||
};
|
||||
|
||||
// See THRESHOLDS
|
||||
private final int[][] ROTATE_TO = new int[][] {
|
||||
{ROTATION_270, ROTATION_90},
|
||||
private static final int[][] ROTATE_TO = new int[][] {
|
||||
{ROTATION_90, ROTATION_270},
|
||||
{ROTATION_0, ROTATION_270, ROTATION_0},
|
||||
{ROTATION_0, ROTATION_90, ROTATION_0}
|
||||
{ROTATION_0, ROTATION_90, ROTATION_0},
|
||||
};
|
||||
|
||||
// Maximum absolute tilt angle at which to consider orientation changes. Beyond this (i.e.
|
||||
// when screen is facing the sky or ground), we refuse to make any orientation changes.
|
||||
private static final int MAX_TILT = 65;
|
||||
// Maximum absolute tilt angle at which to consider orientation data. Beyond this (i.e.
|
||||
// when screen is facing the sky or ground), we completely ignore orientation data.
|
||||
private static final int MAX_TILT = 75;
|
||||
|
||||
// Additional limits on tilt angle to transition to each new orientation. We ignore all
|
||||
// vectors with tilt beyond MAX_TILT, but we can set stricter limits on transition to a
|
||||
// data with tilt beyond MAX_TILT, but we can set stricter limits on transitions to a
|
||||
// particular orientation here.
|
||||
private final int[] MAX_TRANSITION_TILT = new int[] {MAX_TILT, MAX_TILT, MAX_TILT};
|
||||
private static final int[] MAX_TRANSITION_TILT = new int[] {MAX_TILT, 65, 65};
|
||||
|
||||
// Between this tilt angle and MAX_TILT, we'll allow orientation changes, but we'll filter
|
||||
// with a higher time constant, making us less sensitive to change. This primarily helps
|
||||
// prevent momentary orientation changes when placing a device on a table from the side (or
|
||||
// picking one up).
|
||||
private static final int PARTIAL_TILT = 45;
|
||||
private static final int PARTIAL_TILT = 50;
|
||||
|
||||
// Maximum allowable deviation of the magnitude of the sensor vector from that of gravity,
|
||||
// in m/s^2. Beyond this, we assume the phone is under external forces and we can't trust
|
||||
// the sensor data. However, under constantly vibrating conditions (think car mount), we
|
||||
// still want to pick up changes, so rather than ignore the data, we filter it with a very
|
||||
// high time constant.
|
||||
private static final int MAX_DEVIATION_FROM_GRAVITY = 1;
|
||||
private static final float MAX_DEVIATION_FROM_GRAVITY = 1.5f;
|
||||
|
||||
// Actual sampling period corresponding to SensorManager.SENSOR_DELAY_NORMAL. There's no
|
||||
// way to get this information from SensorManager.
|
||||
@@ -185,28 +208,46 @@ public abstract class WindowOrientationListener {
|
||||
// background.
|
||||
|
||||
// When device is near-vertical (screen approximately facing the horizon)
|
||||
private static final int DEFAULT_TIME_CONSTANT_MS = 200;
|
||||
private static final int DEFAULT_TIME_CONSTANT_MS = 50;
|
||||
// When device is partially tilted towards the sky or ground
|
||||
private static final int TILTED_TIME_CONSTANT_MS = 600;
|
||||
private static final int TILTED_TIME_CONSTANT_MS = 300;
|
||||
// When device is under external acceleration, i.e. not just gravity. We heavily distrust
|
||||
// such readings.
|
||||
private static final int ACCELERATING_TIME_CONSTANT_MS = 5000;
|
||||
private static final int ACCELERATING_TIME_CONSTANT_MS = 2000;
|
||||
|
||||
private static final float DEFAULT_LOWPASS_ALPHA =
|
||||
(float) SAMPLING_PERIOD_MS / (DEFAULT_TIME_CONSTANT_MS + SAMPLING_PERIOD_MS);
|
||||
computeLowpassAlpha(DEFAULT_TIME_CONSTANT_MS);
|
||||
private static final float TILTED_LOWPASS_ALPHA =
|
||||
(float) SAMPLING_PERIOD_MS / (TILTED_TIME_CONSTANT_MS + SAMPLING_PERIOD_MS);
|
||||
computeLowpassAlpha(TILTED_TIME_CONSTANT_MS);
|
||||
private static final float ACCELERATING_LOWPASS_ALPHA =
|
||||
(float) SAMPLING_PERIOD_MS / (ACCELERATING_TIME_CONSTANT_MS + SAMPLING_PERIOD_MS);
|
||||
computeLowpassAlpha(ACCELERATING_TIME_CONSTANT_MS);
|
||||
|
||||
// The low-pass filtered accelerometer data
|
||||
private float[] mFilteredVector = new float[] {0, 0, 0};
|
||||
private WindowOrientationListener mOrientationListener;
|
||||
private int mRotation = ROTATION_0; // Current orientation state
|
||||
private float mTiltAngle = 0; // low-pass filtered
|
||||
private float mOrientationAngle = 0; // low-pass filtered
|
||||
|
||||
/*
|
||||
* Each "distrust" counter represents our current level of distrust in the data based on
|
||||
* a certain signal. For each data point that is deemed unreliable based on that signal,
|
||||
* the counter increases; otherwise, the counter decreases. Exact rules vary.
|
||||
*/
|
||||
private int mAccelerationDistrust = 0; // based on magnitude != gravity
|
||||
private int mTiltDistrust = 0; // based on tilt close to +/- 90 degrees
|
||||
|
||||
public SensorEventListenerImpl(WindowOrientationListener orientationListener) {
|
||||
mOrientationListener = orientationListener;
|
||||
}
|
||||
|
||||
private static float computeLowpassAlpha(int timeConstantMs) {
|
||||
return (float) SAMPLING_PERIOD_MS / (timeConstantMs + SAMPLING_PERIOD_MS);
|
||||
}
|
||||
|
||||
int getCurrentRotation() {
|
||||
return SURFACE_ROTATIONS[mRotation];
|
||||
}
|
||||
|
||||
private void calculateNewRotation(int orientation, int tiltAngle) {
|
||||
private void calculateNewRotation(float orientation, float tiltAngle) {
|
||||
if (localLOGV) Log.i(TAG, orientation + ", " + tiltAngle + ", " + mRotation);
|
||||
int thresholdRanges[][] = THRESHOLDS[mRotation];
|
||||
int row = -1;
|
||||
@@ -226,7 +267,7 @@ public abstract class WindowOrientationListener {
|
||||
|
||||
if (localLOGV) Log.i(TAG, " new rotation = " + rotation);
|
||||
mRotation = rotation;
|
||||
onOrientationChanged(SURFACE_ROTATIONS[rotation]);
|
||||
mOrientationListener.onOrientationChanged(getCurrentRotation());
|
||||
}
|
||||
|
||||
private float lowpassFilter(float newValue, float oldValue, float alpha) {
|
||||
@@ -238,11 +279,11 @@ public abstract class WindowOrientationListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute angle between upVector and the x-y plane (the plane of the screen), in [0, 90].
|
||||
* 90 degrees = screen facing the sky or ground.
|
||||
* Angle between upVector and the x-y plane (the plane of the screen), in [-90, 90].
|
||||
* +/- 90 degrees = screen facing the sky or ground.
|
||||
*/
|
||||
private float tiltAngle(float z, float magnitude) {
|
||||
return Math.abs((float) Math.asin(z / magnitude) * RADIANS_TO_DEGREES);
|
||||
return (float) Math.asin(z / magnitude) * RADIANS_TO_DEGREES;
|
||||
}
|
||||
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
@@ -253,34 +294,124 @@ public abstract class WindowOrientationListener {
|
||||
float z = event.values[_DATA_Z];
|
||||
float magnitude = vectorMagnitude(x, y, z);
|
||||
float deviation = Math.abs(magnitude - SensorManager.STANDARD_GRAVITY);
|
||||
float tiltAngle = tiltAngle(z, magnitude);
|
||||
|
||||
float alpha = DEFAULT_LOWPASS_ALPHA;
|
||||
if (tiltAngle > MAX_TILT) {
|
||||
return;
|
||||
} else if (deviation > MAX_DEVIATION_FROM_GRAVITY) {
|
||||
handleAccelerationDistrust(deviation);
|
||||
|
||||
// only filter tilt when we're accelerating
|
||||
float alpha = 1;
|
||||
if (mAccelerationDistrust > 0) {
|
||||
alpha = ACCELERATING_LOWPASS_ALPHA;
|
||||
} else if (tiltAngle > PARTIAL_TILT) {
|
||||
}
|
||||
float newTiltAngle = tiltAngle(z, magnitude);
|
||||
mTiltAngle = lowpassFilter(newTiltAngle, mTiltAngle, alpha);
|
||||
|
||||
float absoluteTilt = Math.abs(mTiltAngle);
|
||||
if (checkFullyTilted(absoluteTilt)) {
|
||||
return; // when fully tilted, ignore orientation entirely
|
||||
}
|
||||
|
||||
float newOrientationAngle = computeNewOrientation(x, y);
|
||||
filterOrientation(absoluteTilt, newOrientationAngle);
|
||||
calculateNewRotation(mOrientationAngle, absoluteTilt);
|
||||
}
|
||||
|
||||
/**
|
||||
* When accelerating, increment distrust; otherwise, decrement distrust. The idea is that
|
||||
* if a single jolt happens among otherwise good data, we should keep trusting the good
|
||||
* data. On the other hand, if a series of many bad readings comes in (as if the phone is
|
||||
* being rapidly shaken), we should wait until things "settle down", i.e. we get a string
|
||||
* of good readings.
|
||||
*
|
||||
* @param deviation absolute difference between the current magnitude and gravity
|
||||
*/
|
||||
private void handleAccelerationDistrust(float deviation) {
|
||||
if (deviation > MAX_DEVIATION_FROM_GRAVITY) {
|
||||
if (mAccelerationDistrust < 5) {
|
||||
mAccelerationDistrust++;
|
||||
}
|
||||
} else if (mAccelerationDistrust > 0) {
|
||||
mAccelerationDistrust--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the phone is tilted towards the sky or ground and handle that appropriately.
|
||||
* When fully tilted, we automatically push the tilt up to a fixed value; otherwise we
|
||||
* decrement it. The idea is to distrust the first few readings after the phone gets
|
||||
* un-tilted, no matter what, i.e. preventing an accidental transition when the phone is
|
||||
* picked up from a table.
|
||||
*
|
||||
* We also reset the orientation angle to the center of the current screen orientation.
|
||||
* Since there is no real orientation of the phone, we want to ignore the most recent sensor
|
||||
* data and reset it to this value to avoid a premature transition when the phone starts to
|
||||
* get un-tilted.
|
||||
*
|
||||
* @param absoluteTilt the absolute value of the current tilt angle
|
||||
* @return true if the phone is fully tilted
|
||||
*/
|
||||
private boolean checkFullyTilted(float absoluteTilt) {
|
||||
boolean fullyTilted = absoluteTilt > MAX_TILT;
|
||||
if (fullyTilted) {
|
||||
if (mRotation == ROTATION_0) {
|
||||
mOrientationAngle = 0;
|
||||
} else if (mRotation == ROTATION_90) {
|
||||
mOrientationAngle = 90;
|
||||
} else { // ROTATION_270
|
||||
mOrientationAngle = 270;
|
||||
}
|
||||
|
||||
if (mTiltDistrust < 3) {
|
||||
mTiltDistrust = 3;
|
||||
}
|
||||
} else if (mTiltDistrust > 0) {
|
||||
mTiltDistrust--;
|
||||
}
|
||||
return fullyTilted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Angle between the x-y projection of upVector and the +y-axis, increasing
|
||||
* clockwise.
|
||||
* 0 degrees = speaker end towards the sky
|
||||
* 90 degrees = right edge of device towards the sky
|
||||
*/
|
||||
private float computeNewOrientation(float x, float y) {
|
||||
float orientationAngle = (float) -Math.atan2(-x, y) * RADIANS_TO_DEGREES;
|
||||
// atan2 returns [-180, 180]; normalize to [0, 360]
|
||||
if (orientationAngle < 0) {
|
||||
orientationAngle += 360;
|
||||
}
|
||||
return orientationAngle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a new filtered orientation angle.
|
||||
*/
|
||||
private void filterOrientation(float absoluteTilt, float orientationAngle) {
|
||||
float alpha = DEFAULT_LOWPASS_ALPHA;
|
||||
if (mTiltDistrust > 0 || mAccelerationDistrust > 1) {
|
||||
// when fully tilted, or under more than a transient acceleration, distrust heavily
|
||||
alpha = ACCELERATING_LOWPASS_ALPHA;
|
||||
} else if (absoluteTilt > PARTIAL_TILT || mAccelerationDistrust == 1) {
|
||||
// when tilted partway, or under transient acceleration, distrust lightly
|
||||
alpha = TILTED_LOWPASS_ALPHA;
|
||||
}
|
||||
|
||||
x = mFilteredVector[0] = lowpassFilter(x, mFilteredVector[0], alpha);
|
||||
y = mFilteredVector[1] = lowpassFilter(y, mFilteredVector[1], alpha);
|
||||
z = mFilteredVector[2] = lowpassFilter(z, mFilteredVector[2], alpha);
|
||||
magnitude = vectorMagnitude(x, y, z);
|
||||
tiltAngle = tiltAngle(z, magnitude);
|
||||
|
||||
// Angle between the x-y projection of upVector and the +y-axis, increasing
|
||||
// counter-clockwise.
|
||||
// 0 degrees = speaker end towards the sky
|
||||
// 90 degrees = left edge of device towards the sky
|
||||
float orientationAngle = (float) Math.atan2(-x, y) * RADIANS_TO_DEGREES;
|
||||
int orientation = Math.round(orientationAngle);
|
||||
// atan2 returns (-180, 180]; normalize to [0, 360)
|
||||
if (orientation < 0) {
|
||||
orientation += 360;
|
||||
// since we're lowpass filtering a value with periodic boundary conditions, we need to
|
||||
// adjust the new value to filter in the right direction...
|
||||
float deltaOrientation = orientationAngle - mOrientationAngle;
|
||||
if (deltaOrientation > 180) {
|
||||
orientationAngle -= 360;
|
||||
} else if (deltaOrientation < -180) {
|
||||
orientationAngle += 360;
|
||||
}
|
||||
mOrientationAngle = lowpassFilter(orientationAngle, mOrientationAngle, alpha);
|
||||
// ...and then adjust back to ensure we're in the range [0, 360]
|
||||
if (mOrientationAngle > 360) {
|
||||
mOrientationAngle -= 360;
|
||||
} else if (mOrientationAngle < 0) {
|
||||
mOrientationAngle += 360;
|
||||
}
|
||||
calculateNewRotation(orientation, Math.round(tiltAngle));
|
||||
}
|
||||
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
|
||||
Reference in New Issue
Block a user