传感器专题(2)——方向传感器
传感器专题(2)——方向传感器
方向传感器(Orientation Sensor)在 Android 中用于检测设备的方向,广泛应用于指南针、地图导航、增强现实(AR)以及交互式动画等场景。结合 Android 动画合集的背景,方向传感器可以驱动动态动画效果,例如根据设备朝向调整视图的旋转角度或触发平移动画。本文将详细介绍 Android 中方向传感器的概念、工作原理、实现方法及与动画结合的示例,重点讲解如何使用 SensorManager
获取方向数据并应用于交互场景。
方向传感器的作用与原理
- 作用:方向传感器用于确定设备相对于地球坐标系的朝向,通常提供设备的方位角(Azimuth)、俯仰角(Pitch)和翻滚角(Roll)。它在指南针、导航、游戏和 AR 应用中有广泛用途。
- 原理:Android 的方向数据主要通过以下两种方式获取:
- 硬件传感器组合:结合加速度计(
Sensor.TYPE_ACCELEROMETER
)和磁力计(Sensor.TYPE_MAGNETIC_FIELD
)计算设备方向。 - 虚拟传感器:使用
Sensor.TYPE_ROTATION_VECTOR
(旋转向量传感器)或已弃用的Sensor.TYPE_ORIENTATION
(API 8 引入,API 20 弃用),通过传感器融合算法提供更精确的方向数据。
- 数据输出:
- 方位角(Azimuth):设备顶部指向相对于地磁北极的角度(0° 到 360°,0° 表示正北)。
- 俯仰角(Pitch):设备绕 X 轴的倾斜角度(-90° 到 90°,正值表示顶部向上)。
- 翻滚角(Roll):设备绕 Y 轴的倾斜角度(-90° 到 90°,正值表示右侧向上)。
- 应用场景:
- 指南针应用:显示设备朝向。
- 动画驱动:根据设备方向调整视图旋转或平移(如指南针指针动画)。
- AR/VR:实时调整虚拟对象的方向。
- 游戏控制:通过设备倾斜控制角色移动。
权限要求
方向传感器通常无需特殊权限,但如果涉及位置相关功能(如磁力计用于导航),可能需要:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
- 动态权限(API 23+):
ACCESS_FINE_LOCATION
用于高精度定位或磁力计增强。 - 注意:大多数方向传感器操作无需权限,但建议检查设备是否支持相关传感器。
获取方向传感器数据
Android 中获取方向数据主要通过 SensorManager
,结合加速度计和磁力计,或直接使用旋转向量传感器。以下是两种主要方法:
- 使用加速度计和磁力计计算方向:
- 通过
SensorManager.getRotationMatrix()
和SensorManager.getOrientation()
计算方位角、俯仰角和翻滚角。 - 需要同时监听
TYPE_ACCELEROMETER
和TYPE_MAGNETIC_FIELD
。
- 使用旋转向量传感器(推荐,API 9+):
- 使用
Sensor.TYPE_ROTATION_VECTOR
获取设备方向数据(四元数或旋转矩阵)。 - 通过
SensorManager.getRotationMatrixFromVector()
和SensorManager.getOrientation()
转换为角度。
常用 API
以下是与方向传感器相关的核心类和方法:
类/方法 | 描述 | 用法示例 |
---|---|---|
SensorManager.getDefaultSensor(int type) | 获取指定类型传感器。 | Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); |
SensorManager.registerListener(SensorEventListener, Sensor, int) | 注册传感器监听器。 | sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI); |
SensorManager.getRotationMatrix(float[] R, float[] I, float[] gravity, float[] geomagnetic) | 计算旋转矩阵。 | SensorManager.getRotationMatrix(R, null, accelData, magneticData); |
SensorManager.getOrientation(float[] R, float[] values) | 从旋转矩阵获取方向角度(方位角、俯仰角、翻滚角)。 | SensorManager.getOrientation(R, values); |
SensorEvent.values | 传感器数据数组(旋转向量或加速度/磁场数据)。 | float azimuth = values[0]; |
- 采样频率(
registerListener
的第三个参数): SENSOR_DELAY_NORMAL
:低频率,适合简单应用。SENSOR_DELAY_UI
:适中,适合 UI 更新。SENSOR_DELAY_GAME
:高频率,适合游戏。SENSOR_DELAY_FASTEST
:最高频率,耗电较高。
完整示例(方向传感器 + 动画)
以下是一个使用 SensorManager
获取方向数据(通过旋转向量传感器)并驱动 ImageView 旋转动画的示例,模拟指南针效果。
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.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.animation.ObjectAnimator;
import androidx.appcompat.app.AppCompatActivity;
public class OrientationSensorActivity extends AppCompatActivity {
private SensorManager sensorManager;
private Sensor rotationVectorSensor;
private ImageView compassImage;
private TextView azimuthText;
private ObjectAnimator rotationAnimator;
private float lastAzimuth = 0f;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_orientation_sensor);
compassImage = findViewById(R.id.compass_image);
azimuthText = findViewById(R.id.azimuth_text);
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
rotationVectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
// 检查传感器可用性
if (rotationVectorSensor == null) {
Toast.makeText(this, "Rotation vector sensor not available", Toast.LENGTH_LONG).show();
finish();
return;
}
// 初始化动画
rotationAnimator = ObjectAnimator.ofFloat(compassImage, "rotation", 0f, 0f);
rotationAnimator.setDuration(200);
}
@Override
protected void onResume() {
super.onResume();
sensorManager.registerListener(sensorListener, rotationVectorSensor, SensorManager.SENSOR_DELAY_UI);
}
@Override
protected void onPause() {
super.onPause();
sensorManager.unregisterListener(sensorListener);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (rotationAnimator != null) {
rotationAnimator.cancel();
}
}
private final SensorEventListener sensorListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
// 获取旋转矩阵
float[] rotationMatrix = new float[9];
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values);
// 获取方向角度
float[] orientation = new float[3];
SensorManager.getOrientation(rotationMatrix, orientation);
// 转换为度数
float azimuth = (float) Math.toDegrees(orientation[0]); // 方位角
float pitch = (float) Math.toDegrees(orientation[1]); // 俯仰角
float roll = (float) Math.toDegrees(orientation[2]); // 翻滚角
// 更新 UI
azimuthText.setText(String.format("Azimuth: %.1f°", azimuth));
// 触发动画(平滑旋转)
rotationAnimator.setFloatValues(lastAzimuth, -azimuth); // 负号调整指南针方向
rotationAnimator.start();
lastAzimuth = -azimuth;
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
Toast.makeText(OrientationSensorActivity.this, "Sensor data unreliable", Toast.LENGTH_SHORT).show();
}
}
};
}
- 布局 XML(res/layout/activity_orientation_sensor.xml):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<ImageView
android:id="@+id/compass_image"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/compass_icon" />
<TextView
android:id="@+id/azimuth_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Azimuth: 0°"
android:textSize="18sp" />
</LinearLayout>
- 说明:
- 传感器:使用
TYPE_ROTATION_VECTOR
获取设备方向,转换为旋转矩阵和角度。 - 动画:根据方位角(Azimuth)动态更新
ImageView
的旋转角度,模拟指南针。 - 生命周期:在
onResume
注册传感器监听,在onPause
注销,onDestroy
取消动画。 - UI 更新:实时显示方位角,动画平滑过渡。
- 资源:需要一个指南针图标(
res/drawable/compass_icon.png
),表示指针。
使用加速度计和磁力计
如果设备不支持旋转向量传感器,可结合加速度计和磁力计:
private float[] accelData = new float[3];
private float[] magneticData = new float[3];
private final SensorEventListener sensorListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
accelData = event.values.clone();
} else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
magneticData = event.values.clone();
}
// 计算方向
float[] R = new float[9];
float[] orientation = new float[3];
if (SensorManager.getRotationMatrix(R, null, accelData, magneticData)) {
SensorManager.getOrientation(R, orientation);
float azimuth = (float) Math.toDegrees(orientation[0]);
azimuthText.setText(String.format("Azimuth: %.1f°", azimuth));
rotationAnimator.setFloatValues(lastAzimuth, -azimuth);
rotationAnimator.start();
lastAzimuth = -azimuth;
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};
// 注册两个传感器
Sensor accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Sensor magneticField = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
sensorManager.registerListener(sensorListener, accelerometer, SensorManager.SENSOR_DELAY_UI);
sensorManager.registerListener(sensorListener, magneticField, SensorManager.SENSOR_DELAY_UI);
优缺点
- 优点:
- 提供精确的方向数据,适合指南针、AR 等场景。
- 旋转向量传感器融合多传感器数据,精度较高。
- 与动画结合可创造沉浸式交互体验。
- 缺点:
- 磁力计易受电磁干扰,需校准。
- 高频率采样耗电量较大。
- 老旧设备可能不支持
TYPE_ROTATION_VECTOR
。 - 替代方案:
- Fused Orientation Provider(Google Play Services):更精确的方向数据。
- Quaternion-based Libraries:如 Sensor Fusion 库。
- Jetpack Compose:声明式 UI 响应方向变化。
注意事项
- 传感器可用性:检查
getDefaultSensor()
是否返回null
。 - 功耗:选择合适的采样频率(如
SENSOR_DELAY_UI
),避免SENSOR_DELAY_FASTEST
。 - 校准:磁力计需校准(如引导用户画“8”字形)。
- 生命周期:在
onPause
注销传感器监听,避免资源泄漏。 - 数据平滑:方向数据可能有噪声,可使用低通滤波:
float smoothedAzimuth = lastAzimuth * 0.9f + azimuth * 0.1f;
- 调试:通过 Log 检查传感器数据和动画效果。
扩展:Jetpack Compose 中的实现
在 Jetpack Compose 中,方向传感器可驱动 Composable 动画:
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@Composable
fun OrientationSensorScreen() {
val context = LocalContext.current
val sensorManager = remember { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager }
val rotationVectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
var azimuth by remember { mutableStateOf(0f) }
val rotation by animateFloatAsState(
targetValue = -azimuth, // 负号调整指南针方向
animationSpec = tween(200)
)
LaunchedEffect(Unit) {
val listener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent?) {
if (event?.sensor?.type == Sensor.TYPE_ROTATION_VECTOR) {
val rotationMatrix = FloatArray(9)
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
val orientation = FloatArray(3)
SensorManager.getOrientation(rotationMatrix, orientation)
azimuth = Math.toDegrees(orientation[0].toDouble()).toFloat()
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
sensorManager.registerListener(listener, rotationVectorSensor, SensorManager.SENSOR_DELAY_UI)
onDispose {
sensorManager.unregisterListener(listener)
}
}
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.compass_icon),
contentDescription = "Compass",
modifier = Modifier
.size(100.dp)
.rotate(rotation)
)
Text(
text = String.format("Azimuth: %.1f°", azimuth),
fontSize = 18.sp
)
}
}
- 说明:
- 使用
LaunchedEffect
注册传感器监听,onDispose
注销。 animateFloatAsState
实现平滑的指南针旋转动画。Image
显示指南针图标,根据方位角动态旋转。- 显示实时方位角数据。
示例:结合振动
结合 Vibrator
(前文介绍),当设备朝向正北(方位角接近 0°)时触发振动:
if (Math.abs(azimuth) < 5) {
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
vibrator.vibrate(100);
}
}
如果需要更复杂的功能(如结合 WindowManager
创建悬浮指南针、Canvas 绘制自定义指南针动画、或处理传感器噪声),或想继续探讨其他传感器(如加速度计、光线传感器)或动画效果,请告诉我!