[英]Implementing current cardinal direction method with TYPE_ROTATION_VECTOR while standing still?
There appear to be many old examples of getting the current cardinal direction on Android devices, but an official solution provided by Google does not appear to be in their documentation. 似乎有许多旧的例子表明在Android设备上获得当前的主要方向,但Google提供的官方解决方案似乎并未出现在他们的文档中。
The oldest reference Sensor.TYPE_ORIENTATION
which is deprecated, more recent ones mention Sensor.TYPE_ACCELEROMETER
and Sensor.TYPE_MAGNETIC_FIELD
(which I have tried with little success - accuracy shifts rapidly depending on device orientation). 不推荐使用的最旧参考
Sensor.TYPE_ORIENTATION
,最新的参考文献提到了Sensor.TYPE_ACCELEROMETER
和Sensor.TYPE_MAGNETIC_FIELD
(我试过但收效甚微 - 准确性根据设备方向快速变化)。 I've been experimenting with implementations using those two like this. 我一直在尝试使用这两个这样的实现。 I've even seen some with
TYPE.GRAVITY
. 我甚至见过一些
TYPE.GRAVITY
。
The most recent seem to suggest TYPE_ROTATION_VECTOR which apparently is a fused sensor( reference ), but example implementations do not seem to be readily available. 最近似乎建议 TYPE_ROTATION_VECTOR显然是一个融合传感器( 参考 ),但示例实现似乎并不容易获得。
I need to use these position/motion sensors, and not GPS, because the user will not be moving during the time when this measurement is needed. 我需要使用这些位置/运动传感器,而不是GPS,因为在需要进行此测量时,用户不会移动。 Also need the measurement to be stable regardless of whether phone is flat, or vertical(as if you are taking a photo)
无论手机是平的还是垂直的(如果你正在拍照),也需要测量稳定
After we pull the degree measurement somehow, converting to cardinal direction seems to be the easy part.( https://stackoverflow.com/a/25349774/1238737 ) 在我们以某种方式拉出度数测量之后,转换到基数方向似乎很容易。( https://stackoverflow.com/a/25349774/1238737 )
Previous solutions 以前的方案
I was working on open source map projects such as OsmAnd, MapsWithMe and MapBox before. 我之前正在研究开源地图项目,如OsmAnd,MapsWithMe和MapBox。 I think these projects are the best available android open sources in the field of map and navigation.
我认为这些项目是地图和导航领域中最好的Android开源。 I've checked their codes and found that MapBox approach to show compass is stable when the phone is vertical then rotating around vertical axis (y).
我检查了他们的代码,发现当手机垂直然后围绕垂直轴(y)旋转时,显示罗盘的MapBox方法是稳定的。 It uses
TYPE_ROTATION_VECTOR
if the rotation vector sensor is available. 如果旋转矢量传感器可用,它使用
TYPE_ROTATION_VECTOR
。 Otherwise it uses TYPE_ORIENTATION
sensor or combination of TYPE_ACCELEROMETER
and TYPE_MAGNETIC_FIELD
. 否则,它使用
TYPE_ORIENTATION
传感器或TYPE_ACCELEROMETER
和TYPE_MAGNETIC_FIELD
组合。 In case of using TYPE_ACCELEROMETER
and TYPE_MAGNETIC_FIELD
, it is possible to reduce the oscillation in result by a low-pass filter to achieve smoother values. 在使用
TYPE_ACCELEROMETER
和TYPE_MAGNETIC_FIELD
情况下,可以通过低通滤波器减少结果的振荡,以实现更平滑的值。
Here is the compass engine of MapBox and its usage. 这是MapBox的指南针引擎及其用法。
. 。
LocationComponentCompassEngine.java: LocationComponentCompassEngine.java:
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Surface;
import android.view.WindowManager;
import timber.log.Timber;
import java.util.ArrayList;
import java.util.List;
/**
* This manager class handles compass events such as starting the tracking of device bearing, or
* when a new compass update occurs.
*/
public class LocationComponentCompassEngine implements SensorEventListener {
// The rate sensor events will be delivered at. As the Android documentation states, this is only
// a hint to the system and the events might actually be received faster or slower then this
// specified rate. Since the minimum Android API levels about 9, we are able to set this value
// ourselves rather than using one of the provided constants which deliver updates too quickly for
// our use case. The default is set to 100ms
private static final int SENSOR_DELAY_MICROS = 100 * 1000;
// Filtering coefficient 0 < ALPHA < 1
private static final float ALPHA = 0.45f;
// Controls the compass update rate in milliseconds
private static final int COMPASS_UPDATE_RATE_MS = 500;
private final WindowManager windowManager;
private final SensorManager sensorManager;
private final List<CompassListener> compassListeners = new ArrayList<>();
// Not all devices have a compassSensor
@Nullable
private Sensor compassSensor;
@Nullable
private Sensor gravitySensor;
@Nullable
private Sensor magneticFieldSensor;
private float[] truncatedRotationVectorValue = new float[4];
private float[] rotationMatrix = new float[9];
private float[] rotationVectorValue;
private float lastHeading;
private int lastAccuracySensorStatus;
private long compassUpdateNextTimestamp;
private float[] gravityValues = new float[3];
private float[] magneticValues = new float[3];
/**
* Construct a new instance of the this class. A internal compass listeners needed to separate it
* from the cleared list of public listeners.
*/
LocationComponentCompassEngine(WindowManager windowManager, SensorManager sensorManager) {
this.windowManager = windowManager;
this.sensorManager = sensorManager;
compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
if (compassSensor == null) {
if (isGyroscopeAvailable()) {
Timber.d("Rotation vector sensor not supported on device, falling back to orientation.");
compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
} else {
Timber.d("Rotation vector sensor not supported on device, falling back to accelerometer and magnetic field.");
gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
magneticFieldSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
}
}
}
public void addCompassListener(@NonNull CompassListener compassListener) {
if (compassListeners.isEmpty()) {
onStart();
}
compassListeners.add(compassListener);
}
public void removeCompassListener(@NonNull CompassListener compassListener) {
compassListeners.remove(compassListener);
if (compassListeners.isEmpty()) {
onStop();
}
}
public int getLastAccuracySensorStatus() {
return lastAccuracySensorStatus;
}
public float getLastHeading() {
return lastHeading;
}
public void onStart() {
registerSensorListeners();
}
public void onStop() {
unregisterSensorListeners();
}
@Override
public void onSensorChanged(SensorEvent event) {
// check when the last time the compass was updated, return if too soon.
long currentTime = SystemClock.elapsedRealtime();
if (currentTime < compassUpdateNextTimestamp) {
return;
}
if (lastAccuracySensorStatus == SensorManager.SENSOR_STATUS_UNRELIABLE) {
Timber.d("Compass sensor is unreliable, device calibration is needed.");
return;
}
if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
rotationVectorValue = getRotationVectorFromSensorEvent(event);
updateOrientation();
// Update the compassUpdateNextTimestamp
compassUpdateNextTimestamp = currentTime + COMPASS_UPDATE_RATE_MS;
} else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
notifyCompassChangeListeners((event.values[0] + 360) % 360);
} else if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
gravityValues = lowPassFilter(getRotationVectorFromSensorEvent(event), gravityValues);
updateOrientation();
} else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
magneticValues = lowPassFilter(getRotationVectorFromSensorEvent(event), magneticValues);
updateOrientation();
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
if (lastAccuracySensorStatus != accuracy) {
for (CompassListener compassListener : compassListeners) {
compassListener.onCompassAccuracyChange(accuracy);
}
lastAccuracySensorStatus = accuracy;
}
}
private boolean isGyroscopeAvailable() {
return sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null;
}
@SuppressWarnings("SuspiciousNameCombination")
private void updateOrientation() {
if (rotationVectorValue != null) {
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVectorValue);
} else {
// Get rotation matrix given the gravity and geomagnetic matrices
SensorManager.getRotationMatrix(rotationMatrix, null, gravityValues, magneticValues);
}
final int worldAxisForDeviceAxisX;
final int worldAxisForDeviceAxisY;
// Remap the axes as if the device screen was the instrument panel,
// and adjust the rotation matrix for the device orientation.
switch (windowManager.getDefaultDisplay().getRotation()) {
case Surface.ROTATION_90:
worldAxisForDeviceAxisX = SensorManager.AXIS_Z;
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X;
break;
case Surface.ROTATION_180:
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X;
worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Z;
break;
case Surface.ROTATION_270:
worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Z;
worldAxisForDeviceAxisY = SensorManager.AXIS_X;
break;
case Surface.ROTATION_0:
default:
worldAxisForDeviceAxisX = SensorManager.AXIS_X;
worldAxisForDeviceAxisY = SensorManager.AXIS_Z;
break;
}
float[] adjustedRotationMatrix = new float[9];
SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisX,
worldAxisForDeviceAxisY, adjustedRotationMatrix);
// Transform rotation matrix into azimuth/pitch/roll
float[] orientation = new float[3];
SensorManager.getOrientation(adjustedRotationMatrix, orientation);
// The x-axis is all we care about here.
notifyCompassChangeListeners((float) Math.toDegrees(orientation[0]));
}
private void notifyCompassChangeListeners(float heading) {
for (CompassListener compassListener : compassListeners) {
compassListener.onCompassChanged(heading);
}
lastHeading = heading;
}
private void registerSensorListeners() {
if (isCompassSensorAvailable()) {
// Does nothing if the sensors already registered.
sensorManager.registerListener(this, compassSensor, SENSOR_DELAY_MICROS);
} else {
sensorManager.registerListener(this, gravitySensor, SENSOR_DELAY_MICROS);
sensorManager.registerListener(this, magneticFieldSensor, SENSOR_DELAY_MICROS);
}
}
private void unregisterSensorListeners() {
if (isCompassSensorAvailable()) {
sensorManager.unregisterListener(this, compassSensor);
} else {
sensorManager.unregisterListener(this, gravitySensor);
sensorManager.unregisterListener(this, magneticFieldSensor);
}
}
private boolean isCompassSensorAvailable() {
return compassSensor != null;
}
/**
* Helper function, that filters newValues, considering previous values
*
* @param newValues array of float, that contains new data
* @param smoothedValues array of float, that contains previous state
* @return float filtered array of float
*/
private float[] lowPassFilter(float[] newValues, float[] smoothedValues) {
if (smoothedValues == null) {
return newValues;
}
for (int i = 0; i < newValues.length; i++) {
smoothedValues[i] = smoothedValues[i] + ALPHA * (newValues[i] - smoothedValues[i]);
}
return smoothedValues;
}
/**
* Pulls out the rotation vector from a SensorEvent, with a maximum length
* vector of four elements to avoid potential compatibility issues.
*
* @param event the sensor event
* @return the events rotation vector, potentially truncated
*/
@NonNull
private float[] getRotationVectorFromSensorEvent(@NonNull SensorEvent event) {
if (event.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).
// Only affects Android 4.3
System.arraycopy(event.values, 0, truncatedRotationVectorValue, 0, 4);
return truncatedRotationVectorValue;
} else {
return event.values;
}
}
public static float shortestRotation(float heading, float previousHeading) {
double diff = previousHeading - heading;
if (diff > 180.0f) {
heading += 360.0f;
} else if (diff < -180.0f) {
heading -= 360.f;
}
return heading;
}
}
CompassListener.java: CompassListener.java:
/**
* Callbacks related to the compass
*/
public interface CompassListener {
/**
* Callback's invoked when a new compass update occurs. You can listen into the compass updates
* using {@link LocationComponent#addCompassListener(CompassListener)} and implementing these
* callbacks. Note that this interface is also used internally to to update the UI chevron/arrow.
*
* @param userHeading the new compass heading
*/
void onCompassChanged(float userHeading);
/**
* This gets invoked when the compass accuracy status changes from one value to another. It
* provides an integer value which is identical to the {@code SensorManager} class constants:
* <ul>
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_NO_CONTACT}</li>
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_UNRELIABLE}</li>
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_LOW}</li>
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_MEDIUM}</li>
* <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_HIGH}</li>
* </ul>
*
* @param compassStatus the new accuracy of this sensor, one of
* {@code SensorManager.SENSOR_STATUS_*}
*/
void onCompassAccuracyChange(int compassStatus);
}
MainActivity.java: MainActivity.java:
import android.content.Context;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.WindowManager;
import android.widget.TextView;
import java.util.Locale;
public class MainActivity extends AppCompatActivity {
private LocationComponentCompassEngine compassEngine;
private float previousCompassBearing = -1f;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView textView = findViewById(R.id.textView);
CompassListener compassListener = new CompassListener() {
@Override
public void onCompassChanged(float targetCompassBearing) {
if (previousCompassBearing < 0) {
previousCompassBearing = targetCompassBearing;
}
float normalizedBearing =
LocationComponentCompassEngine.shortestRotation(targetCompassBearing, previousCompassBearing);
previousCompassBearing = targetCompassBearing;
String status = "NO_CONTACT";
switch (compassEngine.getLastAccuracySensorStatus()) {
case SensorManager.SENSOR_STATUS_NO_CONTACT:
status = "NO_CONTACT";
break;
case SensorManager.SENSOR_STATUS_UNRELIABLE:
status = "UNRELIABLE";
break;
case SensorManager.SENSOR_STATUS_ACCURACY_LOW:
status = "ACCURACY_LOW";
break;
case SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM:
status = "ACCURACY_MEDIUM";
break;
case SensorManager.SENSOR_STATUS_ACCURACY_HIGH:
status = "ACCURACY_HIGH";
break;
}
textView.setText(String.format(Locale.getDefault(),
"CompassBearing: %f\nAccuracySensorStatus: %s", normalizedBearing, status));
}
@Override
public void onCompassAccuracyChange(int compassStatus) {
}
};
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
compassEngine = new LocationComponentCompassEngine(windowManager, sensorManager);
compassEngine.addCompassListener(compassListener);
compassEngine.onStart();
}
@Override
protected void onDestroy() {
super.onDestroy();
compassEngine.onStop();
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.