Android 快速获得当前所在地理位置(简单定位)

好久以前写的博客了,发现好多人都在搜,因为写的确实没啥技术含量,阅读量也名不副实,真的惭愧。趁着这两天有时间,整理了一下博客,希望对大家有所帮助。

本篇主要介绍谷歌自带的LocationManager 获取手机定位的方法,以及通过谷歌服务Geocoder 来进行反地理编码。但是由于Geocoder 进行反地理编码需要一个未包含在android核心框架之中的后端服务,如果平台中没有这个后端服务,Geocoder查询方法将返回空列表。不幸的是,国内很多手机厂商都没有内置这种服务(例如百度、高德服务),所以Geocoder也就不能使用了。所以为了稳定,消除不同手机系统所带来的不稳定性,开发者只能选择国内的反地理编码web服务了。

LocationManager

其实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

获取位置提供器成功后,就可以通过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

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 和 Address

根据Criteria 指定的条件,设备可以自动选择那种provider、是否要求海波、是否要求方向;设置电池消耗要求、设置方向的精准度等一系列的筛选条件。但这里需要注意的是,network一般根据wifi节点mac地址和基站地理位置来进行定位的,在精度上来说是比较差的,虽然有的手机返回的经纬度的精度级别非常高,但是其实际偏差却是非常大的,所以我们不能完全信赖net定位点。

Address 是反编码成功后的实体bean,并没有什么特殊的地方,只是存储了谷歌服务返回来的字段值罢了。例如我们平时常用的国家、城市、城市编码、区、街道、POi名称等信息。

运行结果

Android 快速获得当前所在地理位置(简单定位)_第1张图片

github

代码片段介绍的很简单,如果大家想了解更多,可以移步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的问题。

你可能感兴趣的:(android随笔)