「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战」。
今天介绍一个 Flutter 的地理位置插件 geolocator。
下面内容肉翻自 geolocator 的 pub,水平有限,不吝赐教。
geolocator 简介
geolocator是一个 Flutter 的地理位置插件,提供了对各平台特定的地理位置服务的简易访问途径。
- Android:默认使用 FusedLocationProviderClient,如果不可用,则使用 LocationManager。
- iOS:CLLocationManager。
特性
- 获取上一次的已知位置
- 获取设备的当前位置
- 获取持续的位置更新
- 检查设备的位置服务是否可用
- 计算两个地理坐标之间的距离(单位:米)
- 计算两个地理坐标之间的方位
重要: geolocator插件的7.0.0版本包含了一些不兼容的改动,完整的概览可以看一下 Breaking changes in 7.0.0 的 wiki。
从6.0.0版本开始的 geocoding 特性(placemarkFromAddress 和 placemarkFromCoordinates)不再是 geolocator 的一部分。这些特性已经转移到单独的 geocoding 插件中。这个新插件是旧方法的升级版。
使用
参考 安装说明 把 geolocator 插件添加到 Flutter 应用中。 为了使 geolocator 插件在各种设备上能正常工作,需要做如下设置:
Android
升级1.12之前的 Android 工程
从5.0.0版本开始,插件的实现使用了 Flutter 1.12 的 Android 插件的 API。 不幸的是,这意味着 APP 开发者也需要迁移他们的 APP 以支持新的 Android 架构。可以按照 Upgrading pre 1.12 Android projects 的迁移向导来迁移。迁移失败的话,使用新的插件有可能导致无法预期的行为。
AndroidX
geolocator 插件需要 Android Support Libraries 的 AndroidX 版本。 这意味着需要确认你的 Android 工程支持 AndroidX。 详细说明可以参考这里。
内容摘要:
- 向
gradle.properties文件里添加下面的内容。
android.useAndroidX=true
android.enableJetifier=true
- 确保
android/app/build.gradle中的compileSdkVersion设定为 31。
android {
compileSdkVersion 31
...
}
- 确保所有
android.的依赖已替换为相应的 AndroidX 版本。
完整的列表在此:Migrating to AndroidX。
权限
在 Android 中需要添加 ACCESS_COARSE_LOCATION 或者 ACCESS_FINE_LOCATION 权限到 Android Manifest 里。
打开 AndroidManifest.xml 文件(位置:android/app/src/main),添加下面两行中的一行作为 <manifest> 标签的直接子项。
如果两个权限都被配置的话,geolocator 插件会使用 ACCESS_FINE_LOCATION 权限。
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
从 Android10 开始,如果 APP 在后台运行时想要持续获取(地理位置信息的)更新(注意 geolocator 插件在后台运行时不支持接收一个处理中的位置更新),需要添加 ACCESS_BACKGROUND_LOCATION 权限(
在 ACCESS_COARSE_LOCATION 或 ACCESS_FINE_LOCATION 权限下面)。
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
注: 指定
ACCESS_COARSE_LOCATION权限会得到和一个城市街区大致相等精度的位置更新。 在获取第一次的位置修正之前可能会花很长时间(几分钟), 因为ACCESS_COARSE_LOCATION只使用网络服务来计算设备的位置。更多信息参考这里。
iOS
在 iOS 中需要在 Info.plist 文件(位置:ios/Runner)中添加以下内容以访问设备的位置。
简单来说,就是打开 Info.plist 文件,添加下面的内容
(确保已更新描述内容使其符合 APP 的语境):
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location when open.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to location when in the background.</string>
如果 APP 在后台运行时想要持续获取(地理位置的)更新,需要向 XCode 工程中添加 Backgroud Modes capability(Project > Signing and Capabilities > "+ Capability" 按钮)和选中位置更新。 如此设置需要当心的是,当提交 App 到 AppStore 时,需要向 Apple 解释为什么你的 APP 需要如此设置的细节。如果 Apple 不满意你的解释,你的 APP 会被拒。
使用 requestTemporaryFullAccuracy({purposeKey: "YourPurposeKey"}) 方法时,需要向 Info.plist 文件中添加下面的 dictionary。
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
<key>YourPurposeKey</key>
<string>The example App requires temporary access to the device's precise location.</string>
</dict>
第二个 Key (此例中是 YourPurposeKey )需要和传递给 requestTemporaryFullAccuracy() 方法的参数的 purposeKey 匹配。
也可以定义针对 APP 的不同特性定义多个 Key 。
更多信息可在这里找到。
注: 第一次临时请求完整精度访问会花很长时间来显示确认弹框。这是因为 iOS 确认精确的用户位置需要花很长时间。不幸的是,这个是插件无法控制的。
macOS
在 macOS 中需要在 Info.plist 文件(位置:macOS/Runner)中添加以下内容以访问设备的位置。
简单来说,就是打开 Info.plist 文件,添加下面的内容
(确保已更新描述内容使其符合 APP 的语境):
<key>NSLocationUsageDescription</key>
<string>This app needs access to location.</string>
同时需要在 DebugProfile.entitlements 和 Release.entitlements 中添加以下内容。 这用来声明 APP 想要使用设备的位置服务。 APP 会被添加到 系统偏好设置 -> 安全性与隐私 -> 隐私 设定的列表中。
<key>com.apple.security.personal-information.location</key>
<true/>
使用 requestTemporaryFullAccuracy({purposeKey: "YourPurposeKey"}) 方法时,需要向 Info.plist 文件中添加下面的 dictionary。
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
<key>YourPurposeKey</key>
<string>The example App requires temporary access to the device's precise location.</string>
</dict>
第二个 Key (此例中是 YourPurposeKey )需要和传递给 requestTemporaryFullAccuracy() 方法的参数的 purposeKey 匹配。
也可以定义针对 APP 的不同特性定义多个 Key 。
更多信息可在这里找到。
注: 第一次临时请求完整精度访问会花很长时间来显示确认弹框。这是因为 macOS 确认精确的用户位置需要花很长时间。不幸的是,这个是插件无法控制的。
Web
要在 Web 中使用 geolocator 插件,需要 Flutter 1.20或更高版本。
当向 pubspec.yaml 里添加geolocator: ^6.2.0 的依赖时, Flutter 会自动添加 geolocator_web 包到应用中。
以下的 geolocator API 的方法没有在 Web 中支持。
如果使用的话,会引发带着 UNSUPPORTED_OPERATION 错误码的 PlatformException 异常。
-
getLastKnownPosition({ bool forceAndroidLocationManager = true }) -
openAppSettings() -
openLocationSettings()注: 由于 dart:html 库里的一个 bug ,Web 版的 geolocator 插件在空安全模式下,无法正常使用,也无法在 release 模式下编译。 在 release 模式下运行 APP 会引发
Uncaught TypeError(issue #693)。当前的变通方法是在 release 模式下禁用空安全来编译。flutter build web --no-sound-null-safety --releaseDart 团队已经进行了修正(当前在 Dart 2.15.0-63.0.dev 版本中已可用),当然也已经发布并集成到了 Flutter 中。
示例
以下代码展示了如何请求设备当前位置,包括检查位置服务是否可用 和请求访问设备的位置权限。
import 'package:geolocator/geolocator.dart';
/// Determine the current position of the device.
///
/// When the location services are not enabled or permissions
/// are denied the `Future` will return an error.
///
/// 确定设备的当前位置
/// 当位置服务不可用或权限被拒绝时,会返回 error。
Future<Position> _determinePosition() async {
bool serviceEnabled;
LocationPermission permission;
// Test if location services are enabled.
// 检查位置服务是否可用。
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
// Location services are not enabled don't continue
// accessing the position and request users of the
// App to enable the location services.
// 位置服务不可用,不再继续访问位置,也不会向 APP 的用户请求允许使用位置服务。
return Future.error('Location services are disabled.');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
// Permissions are denied, next time you could try
// requesting permissions again (this is also where
// Android's shouldShowRequestPermissionRationale
// returned true. According to Android guidelines
// your App should show an explanatory UI now.
// 权限被拒绝,下次会继续尝试请求权限(安卓的shouldShowRequestPermissionRationale也会返回 true 。
// 按照安卓的指南,这时APP 会显示一个有说明信息的界面。)
return Future.error('Location permissions are denied');
}
}
if (permission == LocationPermission.deniedForever) {
// Permissions are denied forever, handle appropriately.
// 权限被永久拒绝,进行适当处理。
return Future.error(
'Location permissions are permanently denied, we cannot request permissions.');
}
// When we reach here, permissions are granted and we can
// continue accessing the position of the device.
// 至此说明已被授权,可以继续访问设备的位置了。
return await Geolocator.getCurrentPosition();
}
API
地理位置
查询设备的当前位置,只需调用 getCurrentPosition 方法。 可通过以下参数来调整结果:
desiredAccuracy: APP 想要接收到的位置数据的精度。timeLimit: 请求当前位置允许的最大时间。超过这个时间时,会抛出TimeOutException异常,调用会被取消。默认无超时时间。
import 'package:geolocator/geolocator.dart';
Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
查询在设备中存储的上一次已知的位置,可以使用 getLastKnownPosition 方法(如果没有位置详细可用,该方法会返回 null ):
import 'package:geolocator/geolocator.dart';
Position position = await Geolocator.getLastKnownPosition();
监听位置变化,可以调用 getPositionStream 来接收用来监听和接收位置更新的流。可通过以下参数来调整结果:
desiredAccuracy: APP 想要接收到的位置数据的精度。distanceFilter: 触发位置更新事件所需的设备水平移动的最小距离(单位:米)timeInterval: (仅 Android ),下次触发位置更新事件所需的最小时间间隔。timeLimit: 两次位置更新之间允许的最大时间。超过这个时间时,会抛出TimeOutException异常,流会被取消。默认无超时时间。
import 'package:geolocator/geolocator.dart';
StreamSubscription<Position> positionStream = Geolocator.getPositionStream(locationOptions).listen(
(Position position) {
print(position == null ? 'Unknown' : position.latitude.toString() + ', ' + position.longitude.toString());
});
监听服务状态,可以调用 getServiceStatusStream 方法。这个方法会返回一个 Stream<ServiceStatus> ,可被监听,然后接收位置服务状态的更新。
import 'package:geolocator/geolocator.dart';
StreamSubscription<ServiceStatus> serviceStatusStream = Geolocator.getServiceStatusStream().listen(
(ServiceStatus status) {
print(status);
});
获取位置精度 (仅 Android 和 iOS 14+)
如果想要知道用户能获取大体的位置还是能获取精确的位置,可以调用 Geolocator().getLocationAccuracy() 方法。这个方法会返回 Future<LocationAccuracyStatus> 。当用户能获取大体的位置时,返回值包含 LocationAccuracyStatus.reduced ;当用户能获取精确的位置时,返回值包含 LocationAccuracyStatus.precise 。如果在赋予用户权限之前调用 getLocationAccuracy ,会默认返回 LocationAccuracyStatus.reduced 。
iOS 13 或更低版本, getLocationAccuracy 总是会返回 LocationAccuracyStatus.precise ,因为这是 iOS 13 或更低版本的默认值。
import 'package:geolocator/geolocator.dart';
var accuracy = await Geolocator.getLocationAccuracy();
检查服务位置是否可用,可以调用 isLocationServiceEnabled 方法:
import 'package:geolocator/geolocator.dart';
bool isLocationServiceEnabled = await Geolocator.isLocationServiceEnabled();
权限
在你尝试通过 getCurrentPosition 或者 getPositionStream 方法获得位置时,geolocator 会自动尝试请求权限。
即使这样,我们也提供了允许手动请求权限的方法。
如果想要检查用户是否已被授权获得位置,可以调用 checkPermission 方法。
import 'package:geolocator/geolocator.dart';
LocationPermission permission = await Geolocator.checkPermission();
如果想要请求访问设备位置的权限,可以调用 requestPermission 方法。
import 'package:geolocator/geolocator.dart';
LocationPermission permission = await Geolocator.requestPermission();
checkPermission 和 requestPermission 方法可能的结果如下:
| 权限 | 说明 |
|---|---|
| denied | 访问设备位置的权限已被用户拒绝。可以再次请求权限(这也是初始的权限状态) |
| deniedForever | 访问设备位置的权限已被永久拒绝。直到用户在 APP 设定里更新权限之前,请求权限的许可对话框不会再表示。 |
| whileInUse | 仅在使用 APP 时允许访问设备位置。 |
| always | 即使 APP 在后台运行也允许访问设备位置。 |
注:在 Android 上检查权限时只会返回
whileInUser、always或者denied。这是由于受限于在 Android 操作系统上,检查权限时,无法确认权限是否是被永久拒绝。可用的变通方法只能是让requestPermission方法返回这样的结果。 更多信息可以在我们的 wiki 上找到。
设定
在一些情况下,有必要向用户请求权限和更新设备的设定。例如用户一开始永久拒绝了访问设备位置的权限,或者位置服务不可用(还有,在 Android 上,自动解析不工作)。这些情况下,你可以使用 openAppSettings 或 openLocationSettings 方法来让用户立即跳转到设备的设定界面。
在 Android 上,openAppSettings 方法会跳转到 APP 专有的设定,在这里用户可以更新必要的权限。openLocationSettings 方法会跳转到位置设定,在这里用户可以允许使用/禁用位置服务。
在 iOS 上,不允许打开特定的设定界面,所以两个方法都会跳转到设定APP( iOS系统的设定),在这里,用户可以浏览和修正设定种类来更新权限或者允许使用/禁用位置服务。
import 'package:geolocator/geolocator.dart';
await Geolocator.openAppSettings();
await Geolocator.openLocationSettings();
实用方法
要计算两个地理坐标之间的距离(单位:米),可以使用 distanceBetween 方法。该方法有四个参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| startLatitude | double | 开始位置的纬度 |
| startLongitude | double | 开始位置的经度 |
| endLatitude | double | 开始位置的纬度 |
| endLongitude | double | 开始位置的经度 |
import 'package:geolocator/geolocator.dart';
double distanceInMeters = Geolocator.distanceBetween(52.2165157, 6.9437819, 52.3546274, 4.8285838);
要计算两个地理坐标之间的方位,可以使用 bearingBetween方法。该方法也有四个参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| startLatitude | double | 开始位置的纬度 |
| startLongitude | double | 开始位置的经度 |
| endLatitude | double | 开始位置的纬度 |
| endLongitude | double | 开始位置的经度 |
import 'package:geolocator/geolocator.dart';
double bearing = Geolocator.bearingBetween(52.2165157, 6.9437819, 52.3546274, 4.8285838);
位置精度
下表列出了各平台的精度选项
| Android | iOS | |
|---|---|---|
| lowest | 500m | 3000m |
| low | 500m | 1000m |
| medium | 100m - 500m | 100m |
| high | 0 - 100m | 10m |
| best | 0 - 100m | ~0m |
| bestForNavigation | 0 - 100m | Optimized for navigation |
问题联系
有任何要提交的问题、bug 或者特性,可以在我们的 GitHub 页面上提交 issue 。需要商业支持,可以邮件联系我们 hello@baseflow.com。
有贡献意向
如果想对该插件有所贡献(例如:改进文档、解决 bug 、或者添加酷的新特性),请仔细检阅我们的 贡献指南,然后发送你的 pull request 给我们。
作者
适用于 Flutter 的 geolocator 插件是由 Baseflow 开发 。