好久以前写的博客了,发现好多人都在搜,因为写的确实没啥技术含量,阅读量也名不副实,真的惭愧。趁着这两天有时间,整理了一下博客,希望对大家有所帮助。
本篇主要介绍谷歌自带的LocationManager 获取手机定位的方法,以及通过谷歌服务Geocoder 来进行反地理编码。但是由于Geocoder 进行反地理编码需要一个未包含在android核心框架之中的后端服务,如果平台中没有这个后端服务,Geocoder查询方法将返回空列表。不幸的是,国内很多手机厂商都没有内置这种服务(例如百度、高德服务),所以Geocoder也就不能使用了。所以为了稳定,消除不同手机系统所带来的不稳定性,开发者只能选择国内的反地理编码web服务了。
其实LocationManager 的api使用起来很简单,难点在于不同android机型,不同系统可能存在着很多未知的坑。最大的坑就是有的手机系统底层的定位服务是直接连接的google服务器,手机厂商没有将定位的服务器重定向为国内的定位服务商,致使我们通过LocationManager根本拿不到定位信息。针对这种情况,我在gitHub里,提供了基站定位的方式来获取当前的经纬度信息。所以如果项目比较大,对定位要求比较高,那就需要稳定优先了,还是建议选择市面上比较成熟的定位sdk。如果对于定位精度、稳定性要求不高,例如只是辅助用户选择所在城市的场景等,那么LocationManager 还是值得尝试一下的。
LocationManager 提供的位置提供器,默认为PASSIVE_PROVIDER,这是一个特殊的位置提供器,用来被动的接收位置信息,这个位置信息是由其他的服务提供的位置信息,而不是自己主动请求的,相当于共享了一个位置服务信息,不常用。使用比较多的还是GPS和网络定位,这两种定位方式各有特点,GPS定位精度高,但是比较耗电,而且室内很难获取到gps经纬度。而网络定位虽然精度稍低,但耗电量比较小,而且在室内室外效果都很不错。
lm = (LocationManager) mContext.getApplicationContext()
.getSystemService(Context.LOCATION_SERVICE);
// 检查是否有相关定位权限
if(!Helper.checkPermission(mContext.getApplicationContext())){
mListener.onFail("location no permission");
return;
}
// 如果不传配置类,则按默认配置
if(mSiLoOption == null)
mSiLoOption = new SiLoOption();
mProvider = mSiLoOption.isGpsFirst ? Helper.getGPSProvider(lm) : Helper.getNetWorkProvider(lm);
if(mProvider == null)
mListener.onFail("location provider no exist");
else
getLocation(mProvider);
在使用之前,记得检查定位所需的权限,如果在业务端app里,推荐使用easypermissions ,使用起来很简单。
在这里提供一下权限列表:
/** 所要申请的权限*/
String[] perms = {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.INTERNET
};
在这里提供一个获取gps位置提供器的代码片段:
/**
* gps 提供器
* @param locationManager
* @return
*/
public static String getGPSProvider(LocationManager locationManager) {
List<String> prodiverlist = locationManager.getProviders(true);
// gps定位
if(prodiverlist.contains(LocationManager.GPS_PROVIDER))
return LocationManager.GPS_PROVIDER;
return null;
}
获取位置提供器成功后,就可以通过requestLocationUpdates 方法来请求经纬度了,在这里不推荐使用getLastKnownLocation 获取,getLastKnownLocation 在第一次获取位置的时候,由于定位信号的原因可能会导致无法立刻定位到地址,这时就会获取上一次暂存的地理位置信息,很多手机都是null,即使循环获取也没法彻底解决这个问题。需要注意的是,requestLocationUpdates 的参数最短时间间隔,最短更新距离,注意是最短,系统只能保证最短,并不能保证每个time就给你回调一次。
在这里贴出getLastKnownLocation 的代码片段:
@SuppressWarnings("all")
private String getLocation(String provider) {
if(provider == null)
return null;
lm.requestLocationUpdates(provider, mSiLoOption.time, mSiLoOption.distance, mChangeListener);
return provider;
}
在这里我的逻辑是,如果当前优先使用gps,当time*3,如果此时还未获取到gps信息,那么可能当前是处于室内、桥下、隧道等场所,可以自动切换到network。
在这里贴下逻辑代码片段:
@Override
public void onLocationChanged(Location location) {
if(mProvider == null)
return;
latlng = location != null ? ConverHelper.loConverToLatlng(location) : latlng;
mListener.onSuccess(latlng);
if(mSiLoOption.isGpsFirst){
if(location != null){
if(mProvider == LocationManager.GPS_PROVIDER)
mCounter = 0;
else{
mCounter += mSiLoOption.time/1000;
if(mCounter > NET_TIME_SPACE){
mProvider = getLocation(Helper.getGPSProvider(lm));
mCounter = 0;
}
}
}else{
// gps无信号时,尝试network获取
mCounter += mSiLoOption.time/1000;
if(mCounter >= GPS_TIME_SPACE && mCounter < NET_TIME_SPACE){
mProvider = getLocation(Helper.getNetWorkProvider(lm));
}else if(mCounter > NET_TIME_SPACE){
// 只要最后一个net点失败,就抛出回调;前两个点,由于开始,可能为空,不做强制判断处理
mListener.onFail("location latlng = null , provider = "+ mProvider);
}
}
}else{
// 当network获取不到定位
if(location == null)
mListener.onFail("location latlng = null , provider = "+ mProvider);
}
}
需要注意的是,在api 29以后,LocationListener的监听就无法获取到provider状态了。所以逻辑还是和原来有些区别的。
@Override
public void onStatusChanged(String s, int state, Bundle bundle) {
if(mListener == null)
return;
if(Build.VERSION.SDK_INT > 28){
LogUtil.e("after api 29 always AVAILABLE");
}else{
switch (state){
case LocationProvider.OUT_OF_SERVICE:
case LocationProvider.TEMPORARILY_UNAVAILABLE:
LogUtil.e("current provider out of condition");
break;
}
}
}
针对LocationManager获取到的定位location==null,或者直接就是onLocationChanged无法回调的这种悲惨情形,我在gitHub中提供了基站定位的方法。即通过TelephonyManager 来获取mmc、mnc、lac、cid,随后通过基站服务公开的api接口,来查询当前的经纬度。
我在这里贴下代码片段:
String operator = telephonyManager.getNetworkOperator();
if(operator == null || operator.isEmpty())
return;
int mcc = Integer.parseInt(operator.substring(0, 3));
int mnc = Integer.parseInt(operator.substring(3));
CellLocation celo = null;
if(Helper.checkPermission(mContext.getApplicationContext())
&& Helper.checkPhonePermission(mContext.getApplicationContext())){
celo = telephonyManager.getCellLocation();
}else{
mListener.onFail("phone manager no permission");
return;
}
if(celo == null){
mListener.onFail("phone manager getCellLocation null");
return;
}
int cid = 0;
int lac = 0;
if(celo instanceof GsmCellLocation){
cid = ((GsmCellLocation) celo).getCid();
lac = ((GsmCellLocation) celo).getLac();
} else if(celo instanceof CdmaCellLocation) {//03 05 11 为电信CDMA
cid = ((CdmaCellLocation) celo).getBaseStationId();
lac = ((CdmaCellLocation) celo).getNetworkId();
mnc = ((CdmaCellLocation) celo).getSystemId();
}
基站服务的api,我使用的是cellocation.com api免费接口,但是由于近期19/09/05无法进行访问了,所以我又提供了调用openCellid 服务器的api接口来进行基站定位查询。这是我目前发现的最好用的免费api了,当然大家也可以自行尝试聚合数据、图吧、驴博士等。
Geocoder 的api很简单,如果国内手机在自定义系统时能够使用geooder的后端服务或已内置百度高德服务,那么使用Geocoder 反地理编码功能会很方便。所以在使用前,我们可以先检查服务是否可用,再决定使用哪家的web 反地理编码服务。
简单贴下代码片段:
if(!Geocoder.isPresent()){
if(mListener != null)
mListener.onFail(ResponseConstant.GOOGLE_API_OUT_OF_CONDITON, "geocoder is out of condition");
return;
}
简单贴下Geocoder 的使用方法代码片段:
geocoder = new Geocoder(mContext.getApplicationContext());
try {
mAddresses = geocoder.getFromLocation(latlng.getLatitude(), latlng.getLongitude(), MAX_RESULTS);
}catch (IOException e){
if(mListener != null)
mListener.onFail(ResponseConstant.GOOGLE_API_ON_FALI, "geocoder get from location error:"+e.getMessage());
else
LogUtil.e("geocoder get from location error:"+e.getMessage());
}
根据Criteria 指定的条件,设备可以自动选择那种provider、是否要求海波、是否要求方向;设置电池消耗要求、设置方向的精准度等一系列的筛选条件。但这里需要注意的是,network一般根据wifi节点mac地址和基站地理位置来进行定位的,在精度上来说是比较差的,虽然有的手机返回的经纬度的精度级别非常高,但是其实际偏差却是非常大的,所以我们不能完全信赖net定位点。
Address 是反编码成功后的实体bean,并没有什么特殊的地方,只是存储了谷歌服务返回来的字段值罢了。例如我们平时常用的国家、城市、城市编码、区、街道、POi名称等信息。
代码片段介绍的很简单,如果大家想了解更多,可以移步gitHub clone一下代码,代码写的比较详细。除了谷歌反地理编码之外,又额外添加了高德反地理编码api,腾讯反地理编码api,百度反地理编码api,白名单方式和sn签名校验的使用方式都有。
这里贴下使用获取经纬度方法的代码片段:
GeocodingManager.GeoOption option = new GeocodingManager.GeoOption()
.setGeoType(Constant.BS_OPENCELLID_API) // 使用openCellid服务器的基站地位
.setOption(new GoogleGeocoding.SiLoOption()
.setGpsFirst(true));// locationManager定位方式时,gps优先
siLoManager = new GeocodingManager(this, option);
siLoManager.start(new IGeocoding.ISiLoResponseListener() {
@Override
public void onSuccess(Latlng latlng) {
Log.e(LOG_TAG,"siLoManager onSuccess:" + latlng);
tvSimpleLo.setText("latlng:" + latlng.getLatitude()
+ "\n,long:" + latlng.getLongitude()
+ "\n,provider:" + latlng.getProvider());
reGeManager.reGeToAddress(latlng);
}
@Override
public void onFail(String msg) {
Log.e(LOG_TAG,"error:" + msg);
tvSimpleAd.setText("error:" + msg);
}
});
贴一下使用反地理编码的代码片段:
ReverseGeocodingManager.ReGeOption reGeOption = new ReverseGeocodingManager.ReGeOption()
.setReGeType(Constant.BAIDU_API)// 百度api返地理编码
.setSn(true)// sn 签名校验方式
.setIslog(true);// 打印log
reGeManager = new ReverseGeocodingManager(this, reGeOption);
reGeManager.addReGeListener(new IReGe.IReGeListener() {
@Override
public void onSuccess(int state, Latlng latlng) {
Log.e(LOG_TAG,"reGeManager onSuccess:" + latlng);
}
@Override
public void onFail(int errorCode, String error) {
Log.e(LOG_TAG,"error:" + error);
}
});
因为LocationManager 使用起来确实不怎么稳定,我测试了手头的三星S8 android9,华为 JSN-AL00a android9,小米M8 android9,魅族M6 Note andoird7.1.2,都是没有问题的。但是在低端机酷派4.4.4就遇到了获取不到location的问题。