I have an Android app that's using the Rotation Vector sensor. Having read the comment here, I see that with newer API levels, the SensorEvent contains 4-5 values, instead of 3. My code uses an arraycopy for the data.
lastRotVal is a class member and is initialized as an array of size [3]. Here's the relevant parts of the code responding to the sensor events.
public void onSensorChanged(SensorEvent event) {
System.arraycopy(event.values, 0, lastRotVal, 0, 3); //Hardcoded size
lastRotValSet = true;
updateDisplay(event);
}
private void updateDisplay(SensorEvent event){
if (lastRotValSet){
float[] rotation = new float[9];
float[] orientation = new float[3];
SensorManager.getRotationMatrixFromVector(rotation, lastRotVal);
SensorManager.getOrientation(rotation, orientation);
double pitch = orientation[1];
double roll = orientation[2];
//Do stuff with roll and pitch
}
}
I've hardcoded in only using 3 values in the arraycopy. It seems to be working on both older and the new API levels. Is this the best way to maintain that compatibility between versions, or could I be doing something better?
Edit: As stated by the accepted answer, the IllegalArgumentException that prompted this question apparently arises as a consequence of a bug in the API on Samsung devices, not the general API version. So I'll add the fact that my initial error was encountered on a Samsung Galaxy SIII.
The post that you reference actually refers to a bug in a few Samsung devices (Galaxy S4, Galaxy Note 3) - see this Android Developer list post. You actually shouldn't have to do any special handling between SDK levels for this code to work on normal devices. But, alas, fragmentation...
Chromium handles this issue by truncating the array if the size is greater than 4:
if (values.length > 4) {
// On some Samsung devices SensorManager.getRotationMatrixFromVector
// appears to throw an exception if rotation vector has length > 4.
// For the purposes of this class the first 4 values of the
// rotation vector are sufficient (see crbug.com/335298 for details).
if (mTruncatedRotationVector == null) {
mTruncatedRotationVector = new float[4];
}
System.arraycopy(values, 0, mTruncatedRotationVector, 0, 4);
getOrientationFromRotationVector(mTruncatedRotationVector);
} else {
getOrientationFromRotationVector(values);
}
However, I found in my app GPSTest that this solution didn't seem to work on the Galaxy S3 (see Github issue here).
So, I ended up only truncating the array on devices that throw the IllegalArgumentException
. This also avoids the extra System.arraycopy() unless its absolutely necessary.
Here's the code snippet (that also supports orientation sensors on devices with API levels less than Gingerbread (i.e., prior to when the ROTATION_VECTOR sensor was introduced), and handles remapping the coordinate system for orientation changes), which uses a class member mTruncateVector
that is initialized to false
:
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
@Override
public void onSensorChanged(SensorEvent event) {
double orientation = Double.NaN;
double tilt = Double.NaN;
switch (event.sensor.getType()) {
case Sensor.TYPE_ROTATION_VECTOR:
// Modern rotation vector sensors
if (!mTruncateVector) {
try {
SensorManager.getRotationMatrixFromVector(mRotationMatrix, event.values);
} catch (IllegalArgumentException e) {
// On some Samsung devices, an exception is thrown if this vector > 4 (see #39)
// Truncate the array, since we can deal with only the first four values
Log.e(TAG, "Samsung device error? Will truncate vectors - " + e);
mTruncateVector = true;
// Do the truncation here the first time the exception occurs
getRotationMatrixFromTruncatedVector(event.values);
}
} else {
// Truncate the array to avoid the exception on some devices (see #39)
getRotationMatrixFromTruncatedVector(event.values);
}
int rot = getWindowManager().getDefaultDisplay().getRotation();
switch (rot) {
case Surface.ROTATION_0:
// No orientation change, use default coordinate system
SensorManager.getOrientation(mRotationMatrix, mValues);
// Log.d(TAG, "Rotation-0");
break;
case Surface.ROTATION_90:
// Log.d(TAG, "Rotation-90");
SensorManager.remapCoordinateSystem(mRotationMatrix, SensorManager.AXIS_Y,
SensorManager.AXIS_MINUS_X, mRemappedMatrix);
SensorManager.getOrientation(mRemappedMatrix, mValues);
break;
case Surface.ROTATION_180:
// Log.d(TAG, "Rotation-180");
SensorManager
.remapCoordinateSystem(mRotationMatrix, SensorManager.AXIS_MINUS_X,
SensorManager.AXIS_MINUS_Y, mRemappedMatrix);
SensorManager.getOrientation(mRemappedMatrix, mValues);
break;
case Surface.ROTATION_270:
// Log.d(TAG, "Rotation-270");
SensorManager
.remapCoordinateSystem(mRotationMatrix, SensorManager.AXIS_MINUS_Y,
SensorManager.AXIS_X, mRemappedMatrix);
SensorManager.getOrientation(mRemappedMatrix, mValues);
break;
default:
// This shouldn't happen - assume default orientation
SensorManager.getOrientation(mRotationMatrix, mValues);
// Log.d(TAG, "Rotation-Unknown");
break;
}
orientation = Math.toDegrees(mValues[0]); // azimuth
tilt = Math.toDegrees(mValues[1]);
break;
case Sensor.TYPE_ORIENTATION:
// Legacy orientation sensors
orientation = event.values[0];
break;
default:
// A sensor we're not using, so return
return;
}
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private void getRotationMatrixFromTruncatedVector(float[] vector) {
System.arraycopy(vector, 0, mTruncatedRotationVector, 0, 4);
SensorManager.getRotationMatrixFromVector(mRotationMatrix, mTruncatedRotationVector);
}
and to register the sensors in onResume()
:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
// Use the modern rotation vector sensors
Sensor vectorSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
mSensorManager.registerListener(this, vectorSensor, 16000); // ~60hz
} else {
// Use the legacy orientation sensors
Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
if (sensor != null) {
mSensorManager.registerListener(this, sensor,
SensorManager.SENSOR_DELAY_GAME);
}
}
Full implementation is here on Github.
Short summary: Presuming you don't want to make use of values[3] and values[4], your code is fine as is.
From the docs:
values[0]: x*sin(θ/2)
values[1]: y*sin(θ/2)
values[2]: z*sin(θ/2)
values[3]: cos(θ/2)
values[4]: estimated heading Accuracy (in radians) (-1 if unavailable)
values[3], originally optional, will always be present from SDK Level 18 onwards. values[4] is a new value that has been added in SDK Level 18.
If I read that right, event.values.length will only be greater 3 if you compiled with SDK 18 or older.
SensorManager.getRotationMatrixFromVector seems to assume a rotation vector of length==3. I'm not certain what that function does if the rotation vector passed in is larger than 3 elements anyway.
If you ever needed to make use of the event.values[3] and event.values[4], you could detect if the device supports these extended values simply by checking the event.values.length. You could also check to see if Build.VERSION.SDK_INT is >= 18 at runtime as well. But if you don't need it, stick with your hardcoded assumption of 3.
来源:https://stackoverflow.com/questions/22029542/how-can-i-ensure-compatibility-between-android-api-levels-for-rotationvector