Android定位功能实现

关于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 (...) {
	        ...
	    } else {
	        isUnknownDevice = true
	    }
    } catch (t: Throwable) {
        hasException = true
    }
    if (isUnknownDevice || hasException) {
        goAppDetailSetting(context)
    }
}

检查定位服务开关

Android系统除了对每个应用有各自的权限管理,还有一个总的定位服务开关,当定位服务被关闭后,是没有办法使用系统的定位服务的。所以在获得定位权限后,还需要检查是否已打开定位服务开关。在Android系统上定位权限和定位服务开关之间是独立的,所以获取定位权限和检查定位开关是否开启这两个步骤谁先谁后都是可以的,但是iOS上定位服务开关优先级更高,在定位服务被关闭后是没法在设置中修改定位权限的,所以iOS上需要先检查定位服务开关,再申请定位权限,两端如果想要保持一致的话也可以将Android检查定位服务开关放到申请定位权限之前。

检查是否开启定位服务

检查定位服务是否开启的方法在不同的系统版本上不一样。具体实现如下。

private fun isLocationServiceEnabled(locationManager: LocationManager, context: Context): Boolean {
    return when {
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> {
            try {
                Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE) != Settings.Secure.LOCATION_MODE_OFF
            } catch (e: Settings.SettingNotFoundException) {
                false
            }
        }
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
            locationManager.isLocationEnabled
        }
        else -> {
            Settings.Secure.getString(context.contentResolver, Settings.Secure.LOCATION_PROVIDERS_ALLOWED).isNotNullOrEmpty()
        }
    }
}

跳转定位服务设置(可选)

如果判断定位服务被关闭了,需要提醒用户去设置打开定位服务。实现如下。

fun goLocationServiceSetting(context: Context) {
   // 显示一个对话框,点去开启就尝试跳转到权限开关页面
   showDialog(
        title = "开启位置服务",
        description = "需要位置服务来。。。",
        negativeButtonText = "取消",
        negativeAction = {},
        positiveButtonText = "去开启",
        positiveAction = {
            try {
		       val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
		       context.startActivity(intent)
		   } catch (e: Exception) {
		       val intent = Intent(Settings.ACTION_SETTINGS)
		       context.startActivity(intent)
		   }
        }
    )
}

Android系统允许在APP中通过代码直接打开定位服务开关,有些APP在用户点击对话框确定后就自动打开了定位服务,而不是跳转到设置页,有些甚至在后台就静悄悄的打开了定位服务,一般来说不推荐这种方式实现。

获取位置信息的几种方式

Android官方提供了两种方式来请求位置信息,一种是通过原生提供的LocationManager服务,另一种是通过 Google Play services提供的Google Location Services API,官方强烈建议用Google Location Services API,它有更容易使用的API,更高的精确度和更低的电量消耗。不过由于众所周知的原因,国内APP只能用LocationManager服务来获取定位,所以本文还是讲述如何通过LocationManager来获取位置信息。如果你是海外APP的开发者,建议直接用Google Location Services API,就不用看本文后面部分了(Google Location Services API并没有封装权限请求的过程,所以前面的请求权限和检查服务开关的功能还是需要的)。

除了官方提供的这两种方式外,还可以通过接入一些第三方SDK来实现,尤其是一些地图SDK,例如百度或高德都有相应的SDK来获取位置信息。通过第三方SDK来获取位置信息相对来说比自己实现要简单很多,但需要额外接入一个SDK,同时需要注册对应平台的开发者账号。本质上来说,第三方的SDK获取位置信息也是通过原生的LocationManger来获取,只是做了一层封装,并不能提供额外的定位能力,所以一般没有必要去接。

当上述定位方法都不能用时,例如用户没有授予定位权限时,还可以通过IP定位来获取一个大致的位置信息。IP定位必须通过网络请求来实现,百度和高德都有相应的IP定位服务,有些IP定位服务都只能返回具体的城市,有些则可以有经纬度信息。IP定位可以作为本机定位的一个补充,当没有权限获取位置信息时,可以通过IP定位来获取用户所在的城市信息。

本文还是讲述如何通过LocationManager来实现定位,在这之前先阐述一下两个定位相关的概念,一个是Provider,一个是经纬度。

位置信息提供者Provider

Provider字面意义是提供者,它表示的是我们通过系统API获取到的地理位置是由从哪里获取到的。LocationManger中定义了三种类型的Provider,分别是LocationManager.NETWORK_PROVIDER,LocationManager.GPS_PROVIDER和LocationManager.PASSIVE_PROVIDER。

网络定位Provider

LocationManager.NETWORK_PROVIDER表示网络定位,当设备通过基站或WiFi连入网络后,设备可以获取附近基站/AP的位置,再通过一些计算得到设备当前的位置信息。

GPS定位Provider

LocationManager.GPS_PROVIDER也就是我们通常所说的GPS定位,Android设备上通常会内置一个GPS接收机(GPS Receiver),GPS接收机可以和天上的卫星定位系统(Global Navigation Satellite Systems: GNSS) 通信以获取设备所在的地理位置。

这里所说的GPS接收机并不是一个很准确的概念,现有的卫星定位系统除了最常见的GPS系统,还有北斗/伽利略等系统,国内手机中携带的GPS接收机大都既能接收GPS系统的定位,也能通过北斗系统来定位。

被动定位Provider

LocationManager.PASSIVE_PROVIDER表示被动定位,被动定位是相对于主动定位来说的。网络定位和GPS定位都属于主动定位,当我们通过网络定位或GPS定位来获取位置信息时,系统会主动和附近的基站/AP/卫星通信,以获取当前的位置信息。而被动定位并不会做任何获取位置信息的尝试,它只是被动的接收位置信息的更新,只有其他应用使用了网络定位或GPS定位获取到了新的位置信息后,被动定位的监听者才能获取当前位置。

判断Provider是否可用

在使用某个Provider之前必须先判断这个Provider是否可用。LocationManager中提供了一个isProviderEnabled方法可以用来判断Provider是否可用。

val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
var networkEnable = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
var gpsEnable = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
var passiveEnable = locationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER)

调用isProviderEnabled并不需要定位权限,它的结果也不受是否有定位权限影响。isProviderEnabled(LocationManager.NETWORK_PROVIDER)结果不受是否网络服务相关开关的影响,关闭移动数据和WiFi,打开飞行模式,isProviderEnabled(LocationManager.NETWORK_PROVIDER)的返回值并不会因此而变化。

某个Provider不可用,可能是这个设备上没有携带对应功能的芯片,也可能是用户在系统设置中关闭了定位服务的开关。如果用户关闭了定位服务,则所有的Provider都不可用。由于现在的手机基本上都支持定位功能,所以要判断是否关闭了定位服务时,除了可以用前面介绍的方法外,还可以用下面这种方法来间接的判断是否开启了定位服务。

private fun isLocationServiceEnabled(context: Context): Boolean {
    val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
    val gpsEnable = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
    val networkEnable = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
    return gpsEnable || networkEnable
}

三种Provider所需的权限

判断Provider是否可用不需要权限,但如果要使用某个Provider获取位置信息,必须先拿到位置信息权限。

使用NETWORK_PROVIDER需要ACCESS_COARSE_LOCATION权限,使用GPS_PROVIDER需要ACCESS_FINE_LOCATION权限。由于被动定位可能会获取到GPS定位信息,所以如果要使用被动定位同样需要ACCESS_FINE_LOCATION权限。

反过来说,如果只有ACCESS_COARSE_LOCATION权限,则只能使用NETWORK_PROVIDER。由于有ACCESS_FINE_LOCATION权限就一定有ACCESS_COARSE_LOCATION权限,所以如果有ACCESS_FINE_LOCATION权限,则可以使用所有的Provider。

三种Provider对比和选取

一般来说GPS_PROVIDER能提供比NETWORK_PROVIDER更精确的位置信息,大部分文章在讲述获取定位时都会使用GPS_PROVIDER来获取位置信息,然而GPS_PROVIDER有一个很致命的缺陷,就是GPS接收机需要在室外环境才能正常工作,在室内环境下几乎无法获取到位置信息。所以只用GPS_PROVIDER是不可行的,必须要结合NETWORK_PROVIDER一起使用。NETWORK_PROVIDER在室内和室外都能很好工作。

Google在设计定位服务时,把Provider选择的权力留给APP,希望APP能够按需使用,能用被动定位就只用被动定位,粗略位置就够用的就只用NETWORK_PROVIDER,只有在确实需要精确位置时才用GPS_PROVIDER。但实际上所有的APP都希望能获取到精确位置,能够拿到精确位置就绝不用粗略位置🐶,所以GPS_PROVIDER是一定要使用的,而NETWORK_PROVIDER由于GPS接收在室内的工作缺陷所以也是一定要使用的,至于PASSIVE_PROVIDER,则几乎没有使用场景。

经纬度

经纬度本身的含义相信大家都很清楚,这里就不介绍了。这里主要讲下目前国内存在的不同的经纬度坐标系。

目前最常见也是最通用的坐标系是WGS84,这个坐标系也是GPS系统使用的坐标系。通过原生API获取到的经纬度使用的就是WGS84坐标系。除了WGS84坐标系外,国家测绘局做了一套经纬度坐标系统称为GCJ02,这套坐标系统是通过对WGS84坐标通过一定的算法加密后得到,如果将GCJ02坐标当作WGS84坐标来使用会有几百米左右的一个偏移。除了这两套常见的坐标系外,百度自己又做了两套坐标系,分别为BD09ll和BD09mc。BD09ll表示百度经纬度坐标,BD09mc表示百度墨卡托坐标。这两个坐标系是在GCJ02坐标系基础上再次加密后得到的。

通过LocationManager获取位置信息

LocationManager的用法

LocationManager中提供了三组API来获取位置信息。每组API都可以传入一个Provider参数,获取对应Provider的位置信息。

  1. getLastKnownLocation
    获取上次定位的位置信息,上次位置信息指的是上次成功通过指定的Provider获取到的位置信息。这个方法是一个同步方法,可以直接将位置信息返回回来。如果没有上次的位置信息,也可能会返回null。

    上次的位置信息可能和当前实际的位置信息差别很大,取决于上次获取位置信息的时间和设备是否移动。举例来说,如果一个人带着手机坐着火车从北京到了上海,这期间又没有打开任何需要获取定位的APP,那么到上海后getLastKnownLocation得到的结果就还是在北京的位置。所以一般只有在实在拿不到当前位置信息的时候才会使用上次的位置信息。

  2. requestSingleUpdate
    请求单次位置信息,单次的意思是只会获取一次位置信息,当位置信息成功更新后,就不会再继续尝试获取。这是一个异步方法,需要在参数中传入一个Listener或PendingIntent参数以接收定位结果,由于它是一个单次更新,所以Listener或PendingIntent只会被回调一次。此外它还有一个Looper参数,表示执行回调时的线程,如果传入null则表示在调用这个方法的线程中收到回调。有些文章说requestSingleUpdate只能在主线程中调用,这是不对的,requestSingleUpdate对调用线程并无要求。

  3. requestLocationUpdates
    请求连续的位置信息,这里连续的意思是会连续不断的请求位置信息,直到用户手动调用removeUpdates取消定位。相比requestSingleUpdate,requestLocationUpdates还多了两个参数minTime和minDistance用来控制更新频率,minTime表示最小的更新时间间隔,minDistance表示最短的更新距离。

至于使用requestSingleUpdate还是requestLocationUpdates取决余实际需要,如果不是需要实时更新的地图类应用,一般来说使用requestSingleUpdate就可以了,即使用requestLocationUpdates也可以设置一个相对较长的最小时间和最短距离,以减少资源消耗。

获取位置信息的流程

有了上述API,我们就能通过这些API来获取设备的位置信息。

在大部分讲述定位实现的文章中我们都能看到这样的一段代码。

val criteria = Criteria()
criteria.accuracy = Criteria.ACCURACY_FINE
val bestProvider = locationManager.getBestProvider(criteria, true)
locationManager.requestSingleUpdate(bestProvider, object: LocationListener {
    override fun onLocationChanged(location: Location) {
        locationManager.removeUpdates(this)
        mLocation = location
        updateUI()
    }

    override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
    override fun onProviderEnabled(provider: String?) {}
    override fun onProviderDisabled(provider: String?) {}
}, null)

这段代码中先是获取了当前可用的精度最高的Provider,然后调用requestSingleUpdate/requestLocationUpdates来获取位置信息。

的确这段代码在很多时候能够获取到位置信息,但它有两个问题。

首先,它使用的是bestProvider,这在手机上基本上就是GPS_PROVIDER,而GPS_PROVIDER在室内是基本拿不到位置信息的。
第二,如果拿不到位置信息,onLocationChanged就不会被调用,UI就会一直处在同一个状态,即使有loading页,用户也会看到一直在loading,完全不知道这时在干什么。

对第一个问题,如前所诉,需要结合NETWORK_PROVIDER来获取位置信息,这就有一个新的问题:如何将这两个位置信息结合起来使用。

由于请求网络位置和请求GPS位置都是异步回调,它们之间没有先后顺序,可能先收到网络位置,也可能先收到GPS位置,如果先收到网络位置后立刻更新界面,则收到GPS位置后又需要更新界面。如果先收到GPS位置,后收到网络位置,由于网络位置通常比如GPS精确,所以这时一般又不需要更新界面,但有一种例外情况是如果两个位置信息获取的时间间隔较长,可能之前的GPS位置已经过时,反而不如后来的网络位置精确,这时就需要更新界面。这里又有一个新的问题,如何判断两个位置信息哪个更好,仅仅单纯的认为GPS比网络位置好是不对的。

对第二个问题,可以设置一定的超时时间,当超时时间达到后还没有收到位置信息就更新界面,给出定位失败,或使用getLastKnownLocation获取上次的位置信息来作为一个补充。

这里给出一个完整的实现策略。

首先这里没有使用getBestProvider,因为这里既要使用GPS_PROVIDER和又要使用NETWORK_PROVIDER,而总共可以用来主动定位也就这两个PROVIDER,所以getBestProvider就没有使用的必要了。

这里的主要逻辑如下。

  1. 获取上次的GPS位置作为当前的最佳位置
  2. 获取上次的网络位置,并和当前最佳位置比较,如果比当前最佳位置好,则用它替换当前最佳位置。
  3. 更新当前GPS位置,如果获取到并且比当前最佳位置好,则替换当前最佳位置。由于GPS位置获取到了一般都是比其他位置信息要精确的,所以这里就不等超时时间到,就直接发送这个位置信息。
  4. 更新当前的网络位置,如果获取到并且比当前最佳位置好,则替换当前最佳位置。由于网络位置精度不如GPS,所以这里不会立刻发送位置,而是继续等待,直到超时时间到,或拿到GPS位置。
  5. 等待超时,时间到了后判断是否已经发送了位置信息(超时前拿到GPS位置就会立刻发送),如果没有,则发送当前最好的位置。这个最好的位置这时仍然可能是空值。

比较两个位置哪个更好的方法似乎是以前官方文档里推荐的写法,后来Google推荐用Google Location Services API来获取位置信息后,文档里就找不到这段了。

这里获取位置信息的流程或者说策略并不一定是最佳的,读者可以根据实际情况做调整。

private fun getLocation(activity: Activity, timeout: Long) {
    GlobalScope.launch {
        val locationManager = activity.getSystemService(Context.LOCATION_SERVICE) as LocationManager
        try {
            var bestLocation: Location? = null
            var hasSendResult = false
            if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
                val location =  locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
                if (isBetterLocation(location, bestLocation)) {
                    bestLocation = location
                }
            }
            if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
                val location =  locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
                if (isBetterLocation(location, bestLocation)) {
                    bestLocation = location
                }
            }
            var gpsListener: LocationListener?
            var networkListener : LocationListener? = null
            gpsListener = object: LocationListener {
                override fun onLocationChanged(location: Location) {
                    if (isBetterLocation(location, bestLocation)) {
                        bestLocation = location
                    }
                    locationManager.removeUpdates(this)
                    gpsListener = null
                    if (bestLocation != null) {
                        sendLocation(bestLocation)
                        hasSendResult = true
                        if (networkListener != null) {
                            locationManager.removeUpdates(networkListener)
                        }
                    }
                }

                override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
                override fun onProviderEnabled(provider: String?) {}
                override fun onProviderDisabled(provider: String?) {}
            }
            networkListener = object: LocationListener {
                override fun onLocationChanged(location: Location) {
                    if (isBetterLocation(location, bestLocation)) {
                        bestLocation = location
                    }
                    locationManager.removeUpdates(this)
                    networkListener = null
                }

                override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
                override fun onProviderEnabled(provider: String?) {}
                override fun onProviderDisabled(provider: String?) {}
            }
            if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
                locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER, gpsListener, null)
            }
            if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
                locationManager.requestSingleUpdate(LocationManager.NETWORK_PROVIDER, networkListener, null)
            }
            delay(timeout)
            if (!hasSendResult) {
                sendLocation(bestLocation)
            }
            if (gpsListener != null) {
                locationManager.removeUpdates(gpsListener)
            }
            if (networkListener != null) {
                locationManager.removeUpdates(networkListener)
            }
        } catch (t: SecurityException) {
            sendLocation(null)
        }
    }
}

private fun isBetterLocation(location: Location?, currentBestLocation: Location?): Boolean {
    if (location == null) {
        return false
    }
    if (currentBestLocation == null) {
        // A new location is always better than no location
        return true
    }

    val TWO_MINUTES = 1000 * 60 * 2

    // Check whether the new location fix is newer or older
    val timeDelta = location.time - currentBestLocation.time
    val isSignificantlyNewer: Boolean = timeDelta > TWO_MINUTES
    val isSignificantlyOlder: Boolean = timeDelta < -TWO_MINUTES
    val isNewer = timeDelta > 0

    // If it's been more than two minutes since the current location, use
    // the new location
    // because the user has likely moved
    if (isSignificantlyNewer) {
        return true
        // If the new location is more than two minutes older, it must be
        // worse
    } else if (isSignificantlyOlder) {
        return false
    }

    // Check whether the new location fix is more or less accurate
    val accuracyDelta = (location.accuracy - currentBestLocation
        .accuracy).toInt()
    val isLessAccurate = accuracyDelta > 0
    val isMoreAccurate = accuracyDelta < 0
    val isSignificantlyLessAccurate = accuracyDelta > 200

    // Check if the old and new location are from the same provider
    val isFromSameProvider = location.provider == currentBestLocation.provider

    // Not significantly newer or older, so check for Accuracy
    if (isMoreAccurate) {
        // If more accurate return true
        return true
    } else if (isNewer && !isLessAccurate) {
        // Same accuracy but newer, return true
        return true
    } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
        // Accuracy is less (not much though) but is new, so if from same
        // provider return true
        return true
    }
    return false
}

private fun sendLocation(location: Location?) {
    // 自行实现
}

至此通过原生方式获取位置信息的整个流程已经结束了。下面再来介绍下获取到位置信息之后的一些使用场景。

反向地址编码

什么是反向地址编码

将经纬度转化为地址是最常用的一个位置信息使用场景,将经纬度转化为地址的过程一般成为反向地址解析或反向地址编码(Reverse Geocoding),和这个过程相反的将地址转化为经纬度的过程就称为正向地址编码(Geocoding)。

通过Geocoder实现反向地址编码

由于这个功能很常用,所以Google也把这个功能直接加到Android系统里了。可以直接通过Geocoder类来实现反向地址编码。Geocoder的用法很简单。

if (Geocoder.isPresent()) {
    val geocoder = Geocoder(this, Locale.getDefault())
    try {
        val addresses = geocoder.getFromLocation(latitude, longitude, 1)
    } catch (t: Throwable) {
        
    }
}

getFromLocation不需要定位权限,也不需要打开定位开关,它会发起一个网络请求去获取地址,它是一个同步调用,建议在非主线程调用,但在主线程调用也不会报错。它可能会抛一些异常,所以需要try catch一下。如果后端服务不可用,或者没有匹配的地址,会返回null或者空的数组,所以在使用前需要判空一下。

虽然Google的服务在国内不能用,但是Geocoder在国内大部分手机上还是可以用的,猜测是各个厂商自行修改了Geocoder服务,通过抓包可以看到小米的Geocoder调用的是腾讯地图的一个服务。

Geocoder的一些问题

使用Geocoder做反向地址编码存在一些问题。首先可能存在一些机型在国内无法通过Geocoder解析出地址。其次,由于iOS使用不同的服务来获取地址信息,使用Geocoder还会导致相同的经纬度地址在Android和iOS两端的结果不一致。最最重要的是,由于无法控制Geocoder使用哪家的服务,可能存在一些不靠谱的服务商,尤其是海外的服务商在做反向地址编码的时候将tw/xz等一些敏感的地址划归到不合适的地方,如果把这样的信息显示给用户,对国内的应用来说很可能是致命的。

所以不推荐用Geocoder来实现反向地址解析。

使用靠谱的第三方服务来实现反向地址编码

一般建议用百度或高德的反向地址编码服务来实现经纬度获取地址的功能。这里又有两种方式,一种是客户端直接集成百度/高德的SDK,或直接调用它们的Web API,另一种是客户端不直接使用百度/高德的服务,而是在server增加一个接口,客户端调用server的接口来获取地址,server再去调用百度/高德的API。使用server去调用的好处是自由度更高,更可控。可以随时更换服务商,也可以随时掐断服务。一般来说建议用这种方式来实现。具体百度/高德的API使用方法,请参考对应的官方开发文档。

计算距离

如果知道当前位置和目的地位置的经纬度就可以直接在客户端计算它们之间的距离。可以参考 https://www.geodatasource.com/developers/java 来实现。这个距离是它们在大地球面上的距离,并非实际路线距离。如果想要获取到目的地的路线距离,则需要使用地图服务的路线规划功能。如果有获取路线距离的需求,也建议通过server来请求地图服务,客户端调用server接口来获取距离信息。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页