现代手机上,不论是苹果 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 开发中却极难使用。不知大家是什么感想?