Android 传感器(三)— 使用传感器实现获取屏幕方向

在一些使用地图功能的App中,通常地图上会有一个圆点或是箭头代表用户,这个圆点或者箭头在用户拿着手机转动时,朝向也会跟着改变,如下图:

本文介绍如何使用传感器获取屏幕方向实现类似的效果。

谷歌地图

我的测试设备Pixel上默认是谷歌地图,所以示例中直接使用谷歌地图。

添加依赖

在项目下的build.gradle中添加代码,如下:

scss 复制代码
buildscript {

    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        ...
        classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
    }
}

在app module下的build.gradle中添加代码,如下:

bash 复制代码
plugins {
    ...
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}

dependencies {
    implementation 'com.android.volley:volley:1.2.1'
    implementation 'com.google.android.libraries.maps:maps:3.1.0-beta'
}

配置API_KEY

  • 在Google Cloud Console中的Google Maps Platform中获取API_KEY,如图:
  • 在项目下的local.properties中配置API_KEY,如下:
  • 在Manifest中配置API_KEY,如下:
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        ...
        >

        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="${MAPS_API_KEY}" />
            
    </application>
</manifest>

实现获取屏幕方向

可以通过Android提供的屏幕方向传感器或SensorManagergetOrientation方法来获取设备屏幕方向。

方向角

屏幕方向传感器或SensorManagergetOrientation方法都会提供三个方向角数据,如下:

名称 备注
方位角(绕z轴旋转的角度) 此为设备当前指南针方向与磁北向之间的角度。如果设备的上边缘面朝磁北向,则方位角为 0 度;如果上边缘朝南,则方位角为 180 度。与之类似,如果上边缘朝东,则方位角为 90 度;如果上边缘朝西,则方位角为 270 度。
俯仰角(绕 x 轴旋转的角度) 此为平行于设备屏幕的平面与平行于地面的平面之间的角度。如果将设备与地面平行放置,且其下边缘最靠近您,同时将设备上边缘向地面倾斜,则俯仰角将变为正值。沿相反方向倾斜(将设备上边缘向远离地面的方向移动)将使俯仰角变为负值。值的范围为 -180 度到 180 度。
倾侧角(绕 y 轴旋转的角度) 此为垂直于设备屏幕的平面与垂直于地面的平面之间的角度。如果将设备与地面平行放置,且其下边缘最靠近您,同时将设备左边缘向地面倾斜,则侧倾角将变为正值。沿相反方向倾斜(将设备右边缘移向地面)将使侧倾角变为负值。值的范围为 -90 度到 90 度

屏幕方向传感器

通过屏幕方向传感器获取设备屏幕方向并在地图上绘制用户朝向代码如下:

kotlin 复制代码
class SensorExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutSensorExampleActivityBinding

    private lateinit var sensorManager: SensorManager
    private var orientationSensor: Sensor? = null

    private var isSensorListenerRegister = false

    private val mapViewBundleKey = "MapViewBundleKey"
    private var googleMap: GoogleMap? = null
    private var locationManager: LocationManager? = null
    private var currentLatLong: LatLng? = null

    private val locationListener = LocationListener { location ->
        if (currentLatLong?.latitude != location.latitude && currentLatLong?.longitude != location.longitude) {
            googleMap?.run {
                currentLatLong = LatLng(location.latitude, location.longitude)
                // 移动地图到当前位置
                animateCamera(CameraUpdateFactory.newLatLng(currentLatLong))
            }
        }
    }

    private val requestMultiplePermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions: Map<String, Boolean> ->
        val noGrantedPermissions = ArrayList<String>()
        permissions.entries.forEach {
            if (!it.value) {
                noGrantedPermissions.add(it.key)
            }
        }
        if (noGrantedPermissions.isEmpty()) {
            // 申请权限通过,可以使用地图
            initMapView()
        } else {
            //未同意授权
            noGrantedPermissions.forEach {
                if (!shouldShowRequestPermissionRationale(it)) {
                    //用户拒绝权限并且系统不再弹出请求权限的弹窗
                    //这时需要我们自己处理,比如自定义弹窗告知用户为何必须要申请这个权限
                }
            }
        }
    }

    private val sensorEventListener = object : SensorEventListener {
        override fun onSensorChanged(event: SensorEvent?) {
            // 传感器数据变化时回调此方法
            when (event?.sensor?.type) {
                Sensor.TYPE_ORIENTATION -> {
                    // 设置方位角旋转度数
                    addMarkToMap(event.values[0])
                }
            }
        }

        override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
            // 传感器的精度发生变化时回调此方法,通常无需做处理
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutSensorExampleActivityBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.includeTitle.tvTitle.text = "Sensor Example"
        binding.mapView.onCreate(savedInstanceState?.getBundle(mapViewBundleKey))

        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        orientationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION)

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
            ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
        ) {
            initMapView()
        } else {
            requestMultiplePermissionLauncher.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION))
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        binding.mapView.onSaveInstanceState(outState.getBundle(mapViewBundleKey) ?: Bundle().apply {
            putBundle(mapViewBundleKey, this)
        })
    }

    override fun onStart() {
        super.onStart()
        binding.mapView.onStart()
    }

    override fun onResume() {
        super.onResume()
        binding.mapView.onResume()
        registerSensorListener()
    }

    override fun onPause() {
        binding.mapView.onPause()
        super.onPause()
        // 移除传感器监听
        sensorManager.unregisterListener(sensorEventListener)
        isSensorListenerRegister = false
    }

    override fun onStop() {
        binding.mapView.onStop()
        super.onStop()
    }

    override fun onDestroy() {
        binding.mapView.onDestroy()
        locationManager?.removeUpdates(locationListener)
        super.onDestroy()
    }

    override fun onLowMemory() {
        super.onLowMemory()
        binding.mapView.onLowMemory()
    }

    @SuppressLint("MissingPermission")
    private fun initMapView() {
        locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
        locationManager?.run {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                // 结合多种数据源(传感器、定位)提供定位信息
                requestLocationUpdates(LocationManager.FUSED_PROVIDER, 2000, 0f, locationListener)
            } else {
                if (isProviderEnabled(LocationManager.GPS_PROVIDER)) {
                    // 使用GPS提供定位信息
                    requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 0f, locationListener)
                } else {
                    // 使用网络提供定位信息
                    requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 2000, 0f, locationListener)
                }
            }
        }
        binding.mapView.getMapAsync { googleMap ->
            this.googleMap = googleMap.apply {
                // 关闭谷歌地图自带的小圆点
                isMyLocationEnabled = false
                // 设置地图缩放等级
                moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(0.0, 0.0), maxZoomLevel - 5))
            }
        }
        binding.mapView.visibility = View.VISIBLE
    }

    private fun addMarkToMap(rotationDegree: Float) {
        googleMap?.run {
            // 清除已有的Mark
            clear()
            currentLatLong?.let {
                addMarker(MarkerOptions()
                    // 设置图标的位置
                    .position(it)
                    // 设置图标的旋转角度
                    .rotation(rotationDegree)
                    .icon(BitmapDescriptorFactory.fromResource(R.drawable.icon_device_orientation)))
            }
        }
    }

    private fun registerSensorListener() {
        orientationSensor?.let {
            if (!isSensorListenerRegister) {
                isSensorListenerRegister = true
                // 注册传感器监听并且设置数据采样延迟
                // SensorManager.SENSOR_DELAY_FASTEST 延迟0微妙
                // SensorManager.SENSOR_DELAY_GAME 演示20000微妙
                // SensorManager.SENSOR_DELAY_UI 延迟60000微妙
                // SensorManager.SENSOR_DELAY_NORMAL 延迟200000微秒
                sensorManager.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_NORMAL, SensorManager.SENSOR_DELAY_GAME)
            }
        }
    }
}

效果如图:

SensorManagergetOrientation方法

通过SensorManagergetOrientation方法获取设备屏幕方向并在地图上绘制用户朝向代码如下:

kotlin 复制代码
class SensorExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutSensorExampleActivityBinding

    private lateinit var sensorManager: SensorManager
    private var sensor = ArrayList<Sensor?>()

    private var isSensorListenerRegister = false

    private val orientationAccelerometerData = FloatArray(3)
    private val orientationMagnetometerData = FloatArray(3)
    private val rotationMatrix = FloatArray(9)
    private val orientationAngles = FloatArray(3)

    private val mapViewBundleKey = "MapViewBundleKey"
    private var googleMap: GoogleMap? = null
    private var locationManager: LocationManager? = null
    private var currentLatLong: LatLng? = null

    private val locationListener = LocationListener { location ->
        if (currentLatLong?.latitude != location.latitude && currentLatLong?.longitude != location.longitude) {
            googleMap?.run {
                currentLatLong = LatLng(location.latitude, location.longitude)
                // 移动地图,显示当前位置
                animateCamera(CameraUpdateFactory.newLatLng(currentLatLong))
            }
        }
    }

    private val requestMultiplePermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions: Map<String, Boolean> ->
        val noGrantedPermissions = ArrayList<String>()
        permissions.entries.forEach {
            if (!it.value) {
                noGrantedPermissions.add(it.key)
            }
        }
        if (noGrantedPermissions.isEmpty()) {
            // 申请权限通过,可以使用地图
            initMapView()
        } else {
            //未同意授权
            noGrantedPermissions.forEach {
                if (!shouldShowRequestPermissionRationale(it)) {
                    //用户拒绝权限并且系统不再弹出请求权限的弹窗
                    //这时需要我们自己处理,比如自定义弹窗告知用户为何必须要申请这个权限
                }
            }
        }
    }

    private val sensorEventListener = object : SensorEventListener {
        override fun onSensorChanged(event: SensorEvent?) {
            // 传感器数据变化时回调此方法
            when (event?.sensor?.type) {
                Sensor.TYPE_ACCELEROMETER -> {
                    System.arraycopy(event.values, 0, orientationAccelerometerData, 0, orientationAccelerometerData.size)
                    updateOrientationAngles()
                }

                Sensor.TYPE_MAGNETIC_FIELD -> {
                    System.arraycopy(event.values, 0, orientationMagnetometerData, 0, orientationMagnetometerData.size)
                    updateOrientationAngles()
                }
            }
        }

        override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
            // 传感器的精度发生变化时回调此方法,通常无需做处理
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutSensorExampleActivityBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.includeTitle.tvTitle.text = "Sensor Example"
        binding.mapView.onCreate(savedInstanceState?.getBundle(mapViewBundleKey))

        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        sensor.add(sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER))
        sensor.add(sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD))

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
            ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
        ) {
            initMapView()
        } else {
            requestMultiplePermissionLauncher.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION))
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        binding.mapView.onSaveInstanceState(outState.getBundle(mapViewBundleKey) ?: Bundle().apply {
            putBundle(mapViewBundleKey, this)
        })
    }

    override fun onStart() {
        super.onStart()
        binding.mapView.onStart()
    }

    override fun onResume() {
        super.onResume()
        binding.mapView.onResume()
        registerSensorListener()
    }

    override fun onPause() {
        binding.mapView.onPause()
        super.onPause()
        // 移除传感器监听
        sensorManager.unregisterListener(sensorEventListener)
        isSensorListenerRegister = false
    }

    override fun onStop() {
        binding.mapView.onStop()
        super.onStop()
    }

    override fun onDestroy() {
        binding.mapView.onDestroy()
        locationManager?.removeUpdates(locationListener)
        super.onDestroy()
    }

    override fun onLowMemory() {
        super.onLowMemory()
        binding.mapView.onLowMemory()
    }

    private fun updateOrientationAngles() {
        // 根据加速度计传感器和磁力计传感器的读数更新旋转矩阵
        SensorManager.getRotationMatrix(rotationMatrix, null, orientationAccelerometerData, orientationMagnetometerData)
        // 根据旋转矩阵重新计算三个方向角
        SensorManager.getOrientation(rotationMatrix, orientationAngles)
        val degree = Math.toDegrees(orientationAngles[0].toDouble()).toFloat()
        // 设置方位角旋转度数
        addMarkToMap(degree)
    }

    @SuppressLint("MissingPermission")
    private fun initMapView() {
        locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
        locationManager?.run {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                // 结合多种数据源(传感器、定位)提供定位信息
                requestLocationUpdates(LocationManager.FUSED_PROVIDER, 2000, 0f, locationListener)
            } else {
                if (isProviderEnabled(LocationManager.GPS_PROVIDER)) {
                    // 使用GPS提供定位信息
                    requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 0f, locationListener)
                } else {
                    // 使用网络提供定位信息
                    requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 2000, 0f, locationListener)
                }
            }
        }
        binding.mapView.getMapAsync { googleMap ->
            this.googleMap = googleMap.apply {
                // 关闭谷歌地图自带的小圆点
                isMyLocationEnabled = false
                // 设置地图缩放等级
                moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(0.0, 0.0), maxZoomLevel - 5))
            }
        }
        binding.mapView.visibility = View.VISIBLE
    }

    private fun addMarkToMap(rotationDegree: Float) {
        googleMap?.run {
            // 清除已有的Mark
            clear()
            currentLatLong?.let {
                addMarker(MarkerOptions()
                    // 设置图标的位置
                    .position(it)
                    // 设置图标的旋转角度
                    .rotation(rotationDegree)
                    .icon(BitmapDescriptorFactory.fromResource(R.drawable.icon_device_orientation)))
            }
        }
    }

    private fun registerSensorListener() {
        if (sensor.isNotEmpty() && !isSensorListenerRegister) {
            isSensorListenerRegister = true
            sensor.forEach { item ->
                item?.let {
                    // 注册传感器监听并且设置数据采样延迟
                    // SensorManager.SENSOR_DELAY_FASTEST 延迟0微妙
                    // SensorManager.SENSOR_DELAY_GAME 演示20000微妙
                    // SensorManager.SENSOR_DELAY_UI 延迟60000微妙
                    // SensorManager.SENSOR_DELAY_NORMAL 延迟200000微秒
                    sensorManager.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_NORMAL, SensorManager.SENSOR_DELAY_GAME)
                }
            }
        }
    }
}

效果如图:

示例

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee

相关推荐
梁同学与Android19 分钟前
Android --- 在AIDL进程间通信中,为什么使用RemoteCallbackList 代替 ArrayList?
android
Frank_HarmonyOS3 小时前
【无标题】Android消息机制
android
凯文的内存5 小时前
Android14 OTA升级速度过慢问题解决方案
android·ota·update engine·系统升级·virtual ab
VinRichard5 小时前
Android 常用三方库
android
Aileen_0v06 小时前
【玩转OCR | 腾讯云智能结构化OCR在图像增强与发票识别中的应用实践】
android·java·人工智能·云计算·ocr·腾讯云·玩转腾讯云ocr
江上清风山间明月9 小时前
Flutter DragTarget拖拽控件详解
android·flutter·ios·拖拽·dragtarget
debug_cat12 小时前
AndroidStudio Ladybug中编译完成apk之后定制名字kts复制到指定目录
android·android studio
编程洪同学16 小时前
Spring Boot 中实现自定义注解记录接口日志功能
android·java·spring boot·后端
氤氲息18 小时前
Android 底部tab,使用recycleview实现
android
Clockwiseee18 小时前
PHP之伪协议
android·开发语言·php