传感器专题(2)——方向传感器

传感器专题(2)——方向传感器

方向传感器(Orientation Sensor)在 Android 中用于检测设备的方向,广泛应用于指南针、地图导航、增强现实(AR)以及交互式动画等场景。结合 Android 动画合集的背景,方向传感器可以驱动动态动画效果,例如根据设备朝向调整视图的旋转角度或触发平移动画。本文将详细介绍 Android 中方向传感器的概念、工作原理、实现方法及与动画结合的示例,重点讲解如何使用 SensorManager 获取方向数据并应用于交互场景。

方向传感器的作用与原理

  • 作用:方向传感器用于确定设备相对于地球坐标系的朝向,通常提供设备的方位角(Azimuth)、俯仰角(Pitch)和翻滚角(Roll)。它在指南针、导航、游戏和 AR 应用中有广泛用途。
  • 原理:Android 的方向数据主要通过以下两种方式获取:
  1. 硬件传感器组合:结合加速度计(Sensor.TYPE_ACCELEROMETER)和磁力计(Sensor.TYPE_MAGNETIC_FIELD)计算设备方向。
  2. 虚拟传感器:使用 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,结合加速度计和磁力计,或直接使用旋转向量传感器。以下是两种主要方法:

  1. 使用加速度计和磁力计计算方向
  • 通过 SensorManager.getRotationMatrix()SensorManager.getOrientation() 计算方位角、俯仰角和翻滚角。
  • 需要同时监听 TYPE_ACCELEROMETERTYPE_MAGNETIC_FIELD
  1. 使用旋转向量传感器(推荐,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 绘制自定义指南针动画、或处理传感器噪声),或想继续探讨其他传感器(如加速度计、光线传感器)或动画效果,请告诉我!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注