在 Flutter App 中使用 GPS 定位

现代手机上,不论是苹果 iPhone 还是安卓 Android,都配备了强大的定位能力。

定位主要通过卫星和地面基站提供的信号,获得不同精度的定位信息。

通过手机的操作系统,可以获取这些定位信息。这是手机操作系统给应用层提供的能力。

在 Flutter App 中,我们可以调用手机的定位信息。

手机定位

隐私权限

我对安卓不太熟悉,但是在苹果手机上,定位是一项重要的隐私权限,如果你要通过 App 获得用户的定位,必须在 App 运行时取得用户的显式授权。否则,通过接口将无法获得定位的信息。

此外,用户仍然需要在使用 App 的时候,打开系统的定位服务功能开关,否则仍然无法获得用户当前的定位信息。

如果要在苹果 App 中使用定位权限,在开发的时候,需要在 info.plist 中配置很多的权限项,才能在不同的版本的操作系统中,正确获取定位信息。太过冗长,请参考《高德SDK:权限配置》。

安卓手机的权限配置,比苹果还要复杂,主要是因为安卓手机的操作系统和硬件的分化更加离开,给开发者带来了很大的困扰。《权限配置

大厂的定位 SDK

我们在手机 App 的开发过程中,都会使用大厂的 SDK,比如百度地图,或者高德地图的 SDK,这些厂商都会提供 Flutter 的 SDK。

上次我撰文骂过这件事情,这些大厂都商量好了,开始对 SDK 进行收费,价格不菲。那么手机本来就带有 GPS 定位功能,为什么我们不直接使用操作系统的 API 获取定位信息,而是去使用大厂的 SDK 呢?

我想,可能有这么几方面的原因:

第一,操作系统差异。苹果手机的不同版本操作系统,获取定位的权限和方式略有不同。如果自己开发的话,需要考虑这些差异,逐一处理。而安卓不同厂商的差距之大,有时候可以认为是天壤之别。对付硬件和软件差异,操作系统差异的麻烦更大。如果使用了大厂的 SDK,等于 SDK 已经封装了这些差异。

第二,坐标标准。我后来才知道,你获得定位信息,经纬度坐标,竟然也是存在不同标准的。至少就有三种标准,国际标准 WGS84,从苹果手机默认获取到的坐标系统,但是在中国,为了安全等因素考量,我们不使用此标准,如果你使用中国的地图信息,则该坐标不能正确定位位置。火星坐标 GCJ-02,这是中国使用的经过混淆的坐标系统。还有百度坐标 BD-09 在 GCJ-02 的基础上,进行二次加密后,得到的坐标系统。如果没有大厂的 SDK,你需要自己去转换这些坐标系统,才能在地图上得到比较准确的定位位置。

我想,这些额外的开发成本,繁琐而且,非常的难以获得对应的更新信息,是每个开发者和小公司无法承担的。因此才会去依赖大厂的 SDK。

我以前就在自己的 App 中使用了高德的 SDK,不过后来被销售威胁付费,不胜其烦。

Flutter 三方包

我最近找到了一个 Flutter 热门的定位包,https://pub.dev/packages/location,看到很多人点 like,还以为会蛮好用,实际上发现太难用了。勉勉强强能用的水平。

Location location = new Location();

bool _serviceEnabled;
PermissionStatus _permissionGranted;
LocationData _locationData;

_serviceEnabled = await location.serviceEnabled();
if (!_serviceEnabled) {
  _serviceEnabled = await location.requestService();
  if (!_serviceEnabled) {
    return;
  }
}

_permissionGranted = await location.hasPermission();
if (_permissionGranted == PermissionStatus.denied) {
  _permissionGranted = await location.requestPermission();
  if (_permissionGranted != PermissionStatus.granted) {
    return;
  }
}

_locationData = await location.getLocation();

这个就是其调用定位信息的代码,看起来还挺简洁的。不过我实际使用的过程中发现,在 iOS 16 上,这个包会导致界面假死。远远不如高德的 SDK 好用。

我运行了这个包官方提供的 excample,发现没有假死现象,这让我百思不得其解。Log 里提示是在主线程运行了一个什么操作,导致假死,但是维护者又说,那个没关系的,我就完全不知所措了。

总之,我的结论是,这个包远没有看起来那么好。

这个包,对中国的 App 不友好的地方在于,其提供的坐标是 WGS84 标准的,如果在中国做举例估算,地区定位,或者地理围栏等,需要将坐标转换成 GCJ-02 标准,这个转换,我找到了一个库:https://github.com/JackZhouCn/JZLocationConverter

不过这个库里只提供了 Obj-C 的版本,我自己翻译了一个 Dart 版本,分享给大家:

class LocationUtils {
  ///用haversine公式计算经纬度两点间的距离,
  ///注意:这里将地球当做了一个正球体来计算距离,当经纬度跨度较大时,有轻微的距离误差
  static double distanceBetween(LatLng latLng1, LatLng latLng2) {
    //经纬度转换成弧度
    double lat1 = _convertDegreesToRadians(latLng1.latitude);
    double lon1 = _convertDegreesToRadians(latLng1.longitude);
    double lat2 = _convertDegreesToRadians(latLng2.latitude);
    double lon2 = _convertDegreesToRadians(latLng2.longitude);
    //差值
    double deltaLat = (lat1 - lat2).abs();
    double deltaLon = (lon1 - lon2).abs();
    //h is the great circle distance in radians, great circle
    //就是一个球体上的切面,它的圆心即是球心的一个周长最大的圆。
    double h = _haverSin(deltaLat) + cos(lat1) * cos(lat2) * _haverSin(deltaLon);
    return (2 * earthRadius * asin(sqrt(h)));
  }

  /// 将角度换算为弧度。
  static double _convertDegreesToRadians(double degrees) {
    return degrees * pi / 180;
  }

  static double _haverSin(double theta) {
    var v = sin(theta / 2);
    return v * v;
  }

  // 假设的中国大陆经纬度范围常量
  static const double chinaLongitudeMin = 72.004; // 示例值,需要根据实际情况调整
  static const double chinaLongitudeMax = 137.8347; // 示例值,需要根据实际情况调整
  static const double chinaLatitudeMin = 0.8293; // 示例值,需要根据实际情况调整
  static const double chinaLatitudeMax = 55.8271; // 示例值,需要根据实际情况调整

  static const double jzA = 6378245.0;
  static const double jzEE = 0.00669342162296594323;

  static bool _outOfChina(double lat, double lon) {
    if (lon < chinaLongitudeMin || lon > chinaLongitudeMax) return true;
    if (lat < chinaLatitudeMin || lat > chinaLatitudeMax) return true;
    return false;
  }

  static double latOffset0(double x, double y) {
    return -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(x.abs());
  }

  static double latOffset1(double x) {
    return (20.0 * sin(6.0 * x * pi) + 20.0 * sin(2.0 * x * pi)) * 2.0 / 3.0;
  }

  static double latOffset2(double y) {
    return (20.0 * sin(y * pi) + 40.0 * sin(y / 3.0 * pi)) * 2.0 / 3.0;
  }

  static double latOffset3(double y) {
    return (160.0 * sin(y / 12.0 * pi) + 320 * sin(y * pi / 30.0)) * 2.0 / 3.0;
  }

  static double lonOffset0(double x, double y) {
    return 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(x.abs());
  }

  static double lonOffset1(double x) {
    return (20.0 * sin(6.0 * x * pi) + 20.0 * sin(2.0 * x * pi)) * 2.0 / 3.0;
  }

  static double lonOffset2(double x) {
    return (20.0 * sin(x * pi) + 40.0 * sin(x / 3.0 * pi)) * 2.0 / 3.0;
  }

  static double lonOffset3(double x) {
    return (150.0 * sin(x / 12.0 * pi) + 300.0 * sin(x / 30.0 * pi)) * 2.0 / 3.0;
  }

  static double transformLat(double x, double y) {
    double ret = latOffset0(x, y);
    ret += latOffset1(x); // 假设应该传递x
    ret += latOffset2(y); // 假设应该传递y
    ret += latOffset3(y); // 假设应该传递y
    return ret;
  }

  static double transformLon(double x, double y) {
    double ret = lonOffset0(x, y);
    ret += lonOffset1(x); // 假设应该传递x
    ret += lonOffset2(x); // 假设应该传递x
    ret += lonOffset3(x); // 假设应该传递x
    return ret;
  }

  /// 将WGS84坐标转换为GCJ02坐标
  /// 实现来自 https://github.com/JackZhouCn/JZLocationConverter
  /// 介绍文章:https://blog.csdn.net/ZhengYanFeng1989/article/details/83787998
  static LatLng gcj02Encrypt(LatLng origin) {
    double mgLat;
    double mgLon;
    if (_outOfChina(origin.latitude, origin.longitude)) {
      return LatLng(origin.latitude, origin.longitude);
    }
    double dLat = transformLat(origin.longitude - 105.0, origin.latitude - 35.0);
    double dLon = transformLon(origin.longitude - 105.0, origin.latitude - 35.0);
    double radLat = origin.latitude / 180.0 * pi;
    double magic = sin(radLat);
    magic = 1 - jzEE * magic * magic;
    double sqrtMagic = sqrt(magic);
    dLat = (dLat * 180.0) / ((jzA * (1 - jzEE)) / (magic * sqrtMagic) * pi);
    dLon = (dLon * 180.0) / (jzA / sqrtMagic * cos(radLat) * pi);
    mgLat = origin.latitude + dLat;
    mgLon = origin.longitude + dLon;

    return LatLng(mgLat, mgLon);
  }
}

总结

作为个人开发者,或者小企业的开发者,在手机中使用定位信息,殊为不易。要学习的东西还有很多。虽然看起来是每个手机都有的一个服务,但是在 App 开发中却极难使用。不知大家是什么感想?

相关推荐

  1. Go定义方法

    2024-04-13 09:16:06       67 阅读
  2. Go定义结构体

    2024-04-13 09:16:06       61 阅读
  3. 如何Go使用泛型

    2024-04-13 09:16:06       62 阅读
  4. 如何Go使用模板

    2024-04-13 09:16:06       50 阅读
  5. Go语言如何使用变量

    2024-04-13 09:16:06       27 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-13 09:16:06       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-13 09:16:06       100 阅读
  3. 在Django里面运行非项目文件

    2024-04-13 09:16:06       82 阅读
  4. Python语言-面向对象

    2024-04-13 09:16:06       91 阅读

热门阅读

  1. unicloud中文字段排序bug

    2024-04-13 09:16:06       136 阅读
  2. test4132

    test4132

    2024-04-13 09:16:06      36 阅读
  3. 二维数组得学习

    2024-04-13 09:16:06       133 阅读
  4. Scala详解(2)

    2024-04-13 09:16:06       75 阅读
  5. 数据库DMP格式备份文件

    2024-04-13 09:16:06       121 阅读
  6. 3d max快捷键命令大全

    2024-04-13 09:16:06       41 阅读
  7. 用自动LOD简化3D网格【Babylon.js】

    2024-04-13 09:16:06       42 阅读
  8. Python如何使用不同的库来导出PDF和XLSX

    2024-04-13 09:16:06       42 阅读
  9. C语言--第二章之位运算符

    2024-04-13 09:16:06       34 阅读