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

相关推荐
2501_916008891 小时前
Web 前端开发常用工具推荐与团队实践分享
android·前端·ios·小程序·uni-app·iphone·webview
我科绝伦(Huanhuan Zhou)2 小时前
MySQL一键升级脚本(5.7-8.0)
android·mysql·adb
怪兽20143 小时前
Android View, SurfaceView, GLSurfaceView 的区别
android·面试
龚礼鹏4 小时前
android 图像显示框架二——流程分析
android
消失的旧时光-19434 小时前
kmp需要技能
android·设计模式·kotlin
帅得不敢出门5 小时前
Linux服务器编译android报no space left on device导致失败的定位解决
android·linux·服务器
雨白5 小时前
协程间的通信管道 —— Kotlin Channel 详解
android·kotlin
TimeFine7 小时前
kotlin协程 容易被忽视的CompletableDeferred
android
czhc11400756638 小时前
Linux1023 mysql 修改密码等
android·mysql·adb
GOATLong9 小时前
MySQL内置函数
android·数据库·c++·vscode·mysql