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

相关推荐
追光天使40 分钟前
【Mac】和【安卓手机】 通过有线方式实现投屏
android·macos·智能手机·投屏·有线
小雨cc5566ru1 小时前
uniapp+Android智慧居家养老服务平台 0fjae微信小程序
android·微信小程序·uni-app
一切皆是定数2 小时前
Android车载——VehicleHal初始化(Android 11)
android·gitee
一切皆是定数2 小时前
Android车载——VehicleHal运行流程(Android 11)
android
problc2 小时前
Android 组件化利器:WMRouter 与 DRouter 的选择与实践
android·java
图王大胜3 小时前
Android SystemUI组件(11)SystemUIVisibility解读
android·framework·systemui·visibility
服装学院的IT男7 小时前
【Android 13源码分析】Activity生命周期之onCreate,onStart,onResume-2
android
Arms2067 小时前
android 全面屏最底部栏沉浸式
android
服装学院的IT男7 小时前
【Android 源码分析】Activity生命周期之onStop-1
android
ChinaDragonDreamer10 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin