关于Android定位功能如何实现的文章实在太多,有些文章着重于Android API的用法,有些则没有整个定位实现的完整流程,有些虽然有流程,但当你按照文章中的步骤实现好之后,很可能会发现各种问题,最常见的问题就是拿不到位置信息。本文会整理Android定位实现的各个步骤,解释其中可能存在的问题,并提供一些最佳实践作为参考。
获取定位权限
相信大家都知道Android系统从6.0开始要求对dangerous permission在运行时动态申请权限,而定位权限也是dangerous permission,所以要获取位置信息,必须先获取定位权限。关于权限获取,有些文章建议将targetSdkVersion设置为23之前,也就是Android6.0之前。这种方式的确可以让应用自动获取权限,不需要在运行时申请,但如今Android版本都已经进化到Android 11了,很多APP最低版本都已经是6.0起跳,再把target设置成23之前,已经不大合适了,所以还是需要在运行时主动调用API来申请权限。
用原生API方式来做权限申请流程比较复杂,如果不想自己实现,也可以用一些第三方的sdk来简化申请流程,在github上随便搜一下就能搜到很多这样的repo,不过个人还是推荐用原生API来实现,一方面自由度高,另一方面github上很多实现是有bug的。运行时权限申请的一般流程可以参考我之前写过的一篇文章,https://blog.csdn.net/ccpat/article/details/51151863。这里再将定位权限申请的主要流程列举一下。
在AndroidManifest.xml中增加权限配置
在主工程或需要使用定位权限的module的AndroidManifest.xml中增加如下两条权限配置。由于manifest文件在编译时会自动合并,有多条重复的也没有关系,所以不用担心是不是工程的其他AndroidManifest.xml文件已经添加过这两条权限了。
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
ACCESS_COARSE_LOCATION权限允许在APP中通过NETWORK_PROVIDER来获取粗略位置信息,而ACCESS_FINE_LOCATION则允许APP通过GPS_PROVIDER获取精确位置信息。关于NETWORK_PROVIDER和GPS_PROVIDER的概念会在后面介绍。
理论上来说如果一个应用只需要粗略位置,则只需要在AndroidManifest.xml中声明ACCESS_COARSE_LOCATION即可,不需要声明ACCESS_FINE_LOCATION。但事实是绝大多数APP都希望拿到的位置信息越精确越好,谁都不希望被用户吐槽说APP里定位不准,至于GPS定位需要消耗更多电量,忽略就好🙄️
检查是否有定位权限
在申请权限之前一定要先判断是否已经有权限,如果已经有权限就不用再走后面的申请权限流程了。
前面在AndroidManifest.xml中声明了ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION两个权限,但这里并不需要检查ACCESS_COARSE_LOCATION权限,因为如果用户授予了ACCESS_FINE_LOCATION权限,系统会自动为APP同时赋予ACCESS_COARSE_LOCATION权限 ,所以这里只需要检查ACCESS_FINE_LOCATION就可以了。
val granted = ActivityCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
if (granted) {
} else {
}
申请权限
代码如下。如前所说,授予了ACCESS_FINE_LOCATION权限后系统会自动授予ACCESS_COARSE_LOCATION权限,所以这里也只需要请求ACCESS_FINE_LOCATION权限就可以了。
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), requestCode)
另外这里的requestCode一定要大于等于0,否则没有任何反应。
判断请求权限结果
重写Activity或Fragment的onRequestPermissionsResult()方法,检查用户是否被授予了权限。同样的这里检查的是ACCESS_FINE_LOCATION权限。
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode == 之前请求时用的requestCode) {
if (permissions.size == 1 && grantResults.size == 1) {
if (permissions[0] == Manifest.permission.ACCESS_FINE_LOCATION) {
val granted = grantResults[0] == PackageManager.PERMISSION_GRANTED
if (granted) {
getLocation()
} else {
handleNoPermission()
}
}
}
}
}
弹框提醒用户去设置打开(可选)
在申请权限出现的系统弹框中是允许用户勾选不再提醒的,一旦用户勾选了不再提醒,那么调用requestPermissions申请权限是不会再出现权限弹框的,onRequestPermissionsResult中也会立刻收到PERMISSION_DENIED的结果(国内有些机型会模仿iOS把定位权限的不再提醒做成一个独立的选项放到权限弹框中,效果是一样的)。有些APP希望在这种情况下给用户一些反馈,引导用户去系统设置中打开应用权限。为了实现这个功能需要在onRequestPermissionsResult中收到PERMISSION_DENIED的时候打开系统设置。
为了判断用户是否勾选了不再提醒需要使用shouldShowRequestPermissionRationale这个API,shouldShowRequestPermissionRationale在第一次请求权限和用户勾选了不再提醒后都会返回false,结合SharedPreferences去掉第一次请求权限的情况,就可以判断用户是否勾选了不再提醒。示例如下,这里的SharedPreferencesUtils是用来辅助读取和保存SharedPreferences数据的,由于SharedPreferences直接用起来比较麻烦,一般每个应用都会有一个类似的辅助类。
```kotlin
private fun goPermissionSetting() {
if (!shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
val shouldGoSetting = SharedPreferencesUtils.get<Boolean>("should_go_location_setting")
if (shouldGoSetting == true) {
// 显示一个对话框,点去设置就跳转到系统权限设置页面
} else {
SharedPreferencesUtils.cache("should_go_location_setting", true)
}
}
}
```
这里再补充一点,除非是像地图这样一打开APP就必须要使用定位权限的应用,否则请务必在用户打开需要使用定位权限的功能的时候再检查和申请定位权限 。
打开权限设置页面
打开权限设置页面如果要做的简单一点就直接跳转到对应应用的设置界面即可。
private fun goAppDetailSetting(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", context.packageName, null)
context.startActivity(intent)
}
由于国内很多手机厂商在原生的权限管理机制上又自行实现了一套自己的权限管理系统,一般在设置中会有一个独立的权限管理页面,不同机型的权限管理页面是不一样的。如果想要跳转到各个机型对应的权限管理页面就需要做一些额外的跳转。示例如下。具体每个机型的判断和跳转设置的方法由于很难有一个完整的实现,可参考其他文章。
fun gotoPermissionSetting(context: Context) {
var isUnknownDevice = false
var hasException = false
try {
if (isXiaomi(context)) {
gotoMiuiPermission(context) // 小米
} else if (isHuawei(context)) {
gotoHuaweiPermission(context) // 华为
} else if (...)