Android之仿ele地图定位效果
PS:最近项目要求,希望在选择地址的时候能够仿ele来实现定位效果.因此就去做了一下.不过ele使用高德地图实现的,我是用百度地图实现的.没办法,公司说用百度那就用百度的吧.个人觉得高德应该更加的精准.但也无所谓反正都是地图定位+Poi搜索.都差不多.
1.使用LocationClient核心类实现定位
2.使用GeoCoder实现地理编码和反地理编码
3.使用PoiSearch实现相关的Poi搜索
4.使用SuggestionSearch实现在线建议查询
5.ele定位效果的实现
百度地图定位的相关流程我就不进行介绍了.以前也写过流程,至于如何创建应用,如何申请什么的,我就不进行介绍了,官方上有关于如何创建应用申请AK的流程,还是非常的详细的.还是说一下如何实现ele的定位效果吧,可能最后的效果稍微有些偏差,不过大体还是差不多的,主要还是提供一个思路.
1.LocationClient定位核心类
LocationClient是实现地图定位的核心类,LBS基站定位就是通过使用LocationClient来实现的,具体的使用方式如下:
/** * 设置定位的option * */ mLocClient = new LocationClient(this); //实例化LocationClient mLocClient.registerLocationListener(new MyLocationListener()); //注册定位监听 LocationClientOption option = new LocationClientOption(); option.setOpenGps(true); // 打开gps option.setCoorType("bd09ll"); // 设置坐标类型 option.setScanSpan(1000); // 设置查询范围,默认500 mLocClient.setLocOption(option); // 设置option mLocClient.start();
LocationClient实例化之后,需要设置相应的LocationOption,也就是定位的一些选项,通过指定的设置,决定以怎样的形式实现定位,比如说定位的时候需要打开gps,设置坐标的类型,以及搜索的范围等等.同时需要设置相关的监听.
/** * LBS定位监听 * */ public class MyLocationListener implements BDLocationListener { @Override public void onReceiveLocation(BDLocation location) { // map view 销毁后不在处理新接收的位置 if (IsFirstLoc) { IsFirstLoc = false; point = new LatLng(location.getLatitude(), location.getLongitude()); geoCoder.reverseGeoCode(new ReverseGeoCodeOption().location(point)); /** * 设置当前位置为定位到的地理位置 * */ MapStatus.Builder builder = new MapStatus.Builder(); builder.target(point).zoom(20.0f); baiduMap.animateMapStatus(MapStatusUpdateFactory.newMapStatus(builder.build())); } } }
设置完相关的监听之后就可以在LocationClient完成定位后去做一些其他的事情,比如说在地图上显示定位到的当前位置,获取到定位的坐标,坐标的形式是以经度和纬度的组合形式,获取到定位到的坐标后,我们就可以根据坐标实现地理编码和反地理编码.
2.使用GeoCoder实现地理编码与反编码
GeoCoder的使用方式还是非常简单的,只需要实例化对象,然后设置监听回调就可以了..
地理编码:将当前的位置信息转化成坐标的形式(经度+纬度)
geoCoder = GeoCoder.newInstance(); //GeoCoder对象的实例化 geoCoder.setOnGetGeoCodeResultListener(this); //设置监听 //需要实现的方法: @Override public void onGetGeoCodeResult(GeoCodeResult geoCodeResult) { //这里一般不做其他处理 }
反地理编码:将我们当前的坐标信息转化成物理位置.需要额外注意:反地理编码需要在网络状态连接良好的情况下才能够实现
geoCoder = GeoCoder.newInstance(); //GeoCoder对象的实例化 geoCoder.setOnGetGeoCodeResultListener(this); //设置监听 //需要实现的方法: @Override public void onGetReverseGeoCodeResult(ReverseGeoCodeResult reverseGeoCodeResult) { /** * 获取反地理编码后的城市信息,街道信息,Poi信息等 * */ currentCity = reverseGeoCodeResult.getAddress(); }
ReverseGeoCodeResult类的具体形式
package com.baidu.mapapi.search.geocode; import android.os.Parcel; import android.os.Parcelable; import android.os.Parcelable.Creator; import com.baidu.mapapi.model.LatLng; import com.baidu.mapapi.search.core.PoiInfo; import com.baidu.mapapi.search.core.SearchResult; import com.baidu.mapapi.search.core.SearchResult.ERRORNO; import com.baidu.mapapi.search.geocode.c; import com.baidu.mapapi.search.geocode.d; import java.util.List; public class ReverseGeoCodeResult extends SearchResult { private String a; private String b; private ReverseGeoCodeResult.AddressComponent c; private LatLng d; private List<PoiInfo> e; public static final Creator<ReverseGeoCodeResult> CREATOR = new c(); ReverseGeoCodeResult() { } ReverseGeoCodeResult(ERRORNO var1) { super(var1); } protected ReverseGeoCodeResult(Parcel var1) { super(var1); this.a = var1.readString(); this.b = var1.readString(); this.c = (ReverseGeoCodeResult.AddressComponent)var1.readParcelable(ReverseGeoCodeResult.AddressComponent.class.getClassLoader()); this.d = (LatLng)var1.readValue(LatLng.class.getClassLoader()); this.e = var1.createTypedArrayList(PoiInfo.CREATOR); } public String getBusinessCircle() { return this.a; } void a(String var1) { this.a = var1; } public String getAddress() { return this.b; } void b(String var1) { this.b = var1; } public ReverseGeoCodeResult.AddressComponent getAddressDetail() { return this.c; } void a(ReverseGeoCodeResult.AddressComponent var1) { this.c = var1; } public LatLng getLocation() { return this.d; } void a(LatLng var1) { this.d = var1; } public List<PoiInfo> getPoiList() { return this.e; } void a(List<PoiInfo> var1) { this.e = var1; } public int describeContents() { return 0; } public void writeToParcel(Parcel var1, int var2) { super.writeToParcel(var1, var2); var1.writeString(this.a); var1.writeString(this.b); var1.writeParcelable(this.c, 0); var1.writeValue(this.d); var1.writeTypedList(this.e); } public static class AddressComponent implements Parcelable { public String streetNumber; public String street; public String district; public String city; public String province; public static final Creator<ReverseGeoCodeResult.AddressComponent> CREATOR = new d(); public int describeContents() { return 0; } public void writeToParcel(Parcel var1, int var2) { var1.writeString(this.streetNumber); var1.writeString(this.street); var1.writeString(this.district); var1.writeString(this.city); var1.writeString(this.province); } public AddressComponent() { } protected AddressComponent(Parcel var1) { this.streetNumber = var1.readString(); this.street = var1.readString(); this.district = var1.readString(); this.city = var1.readString(); this.province = var1.readString(); } } }
反地理编码结束之后,我们就可以拿到一些相关信息,比如说我们当前位置所在的省市,城市,区域,街道,以及街道号,以及附近的一些Poi等等.这些都可以通过反地理编码的结束后的回调拿到相关的信息.这里我们的反地理编码只是获取了相关的城市信息.为了后续的在线建议查询做准备.
3.使用PoiSearch实现相关的Poi搜索
Poi:poi中文翻译为兴趣点.其实就是周边的一些ktv,酒店,餐馆,理发店等等都是一个poi.在实现了基础定位的前提后,去搜索附近的poi.这样就可以完成一些其他事情.比如说订一份外卖,预定一个房间等等.这些都是基于poi搜索才能够实现的.
Poi搜索有三种不同的方式,周边搜索,区域搜索,城市内搜索.这里我只说周边搜索,因为ele应该也是使用的周边搜索.
三种搜索方式代码上其实大体相同,只是搜索的方式不大一样而已.大体分为三个步骤,首先是对象的实例化,然后设置PoiNearBySearchOption,最后设置监听回调即可.
/** * 房子Poi数据搜索 * @注意: 所有的Poi搜索都是异步完成的 * */ private void nearByAllPoiSearch() { allpoiSearch = PoiSearch.newInstance(); allPoiData.clear(); allpoiSearch.setOnGetPoiSearchResultListener(new OnGetPoiSearchResultListener() { @Override public void onGetPoiResult(PoiResult poiResult) { if (poiResult.getAllPoi() == null) { Toast.makeText(getApplicationContext(),"定位失败,暂无数据信息",Toast.LENGTH_LONG).show(); } else { allPoiData.addAll(poiResult.getAllPoi()); } } @Override public void onGetPoiDetailResult(PoiDetailResult poiDetailResult) { //Poi详情数据 } @Override public void onGetPoiIndoorResult(PoiIndoorResult poiIndoorResult) { //室内Poi数据 } }); /** * 设置Poi Option * 当前搜索为附近搜索:以圆形的状态进行搜索 * 还有两种其他的搜素方式:范围搜素和城市内搜索 * */ PoiNearbySearchOption nearbySearchOption = new PoiNearbySearchOption(); nearbySearchOption.location(new LatLng(point.latitude, point.longitude)); //设置坐标 nearbySearchOption.keyword("房子"); //设置关键字 nearbySearchOption.radius(2000); //搜索范围的半径 nearbySearchOption.pageCapacity(15); //设置最多允许加载的poi数量,默认10 allpoiSearch.searchNearby(nearbySearchOption); }
这里我设置了Poi允许加载的数量,默认是10条数据,也就是说,如果我们不设置分页,百度只会给我们返回10条Poi数据信息,这是默认的情况,这里我修改了允许加载的Poi数量,也就是15个Poi数据信息,如果大家不想写分页,那么可以设置这个属性,按照自己的方式去指定加载多少条数据.
4.使用SuggestionSearch实现在线建议查询
在线建议查询:根据城市和关键字搜索出相应的位置信息(模糊查询)使用起来还是非常的简单的.只需要实例化对象,设置结果回调就可以根据我们输入的关键字搜索相关的地理位置信息.
/** * 在线建议查询对象实例化+设置监听 * @在线建议查询: 根据城市和关键字搜索出相应的位置信息(模糊查询) * */ keyWordsPoiSearch = SuggestionSearch.newInstance(); keyWordsPoiSearch.setOnGetSuggestionResultListener(new OnGetSuggestionResultListener() { @Override public void onGetSuggestionResult(SuggestionResult suggestionResult) { keyWordPoiData.clear(); if (suggestionResult.getAllSuggestions() == null) { Toast.makeText(getApplicationContext(),"暂无数据信息",Toast.LENGTH_LONG).show(); } else { keyWordPoiData = suggestionResult.getAllSuggestions(); //设置Adapter结束 suggestAdapter = new SuggestAddressAdapter(getApplicationContext(), keyWordPoiData); inputPoiListView.setAdapter(suggestAdapter); } } }); keyWordsPoiSearch.requestSuggestion((new SuggestionSearchOption()).keyword(location_name.getText().toString()).city(currentCity));
大体的东西基本就介绍完了.还是来分析一下ele是如何实现的定位效果吧.我是按照我的思路去实现的,可能会有一些与其并不是特别的一样.总之还是提供一个大体的思路才是关键.我们先看一下效果图
我们看着这个效果图来说,上层是一个搜索框和一个MapView.并且中间位置有一个ImageView.下层是一个ViewIndicator,我这个ViewIndicator并没有自定义View,只是一个布局,因此实现起来可能没有那么的优雅,大家可以选择去优化这里,然后下面是一个ViewPager来实现4个Fragment的切换.
需要说明的就是中间这个ImageView,我在这里标识了它并不是地图上的Marker.而是直接定义了一个FrameLayout,让这个ImageView粘在了这个中间位置,我们在移动地图的时候,我们看着好像是这个ImageView也在移动,其实只是这个MapView在移动而已,这个ImageView实际是一直保持不动的.那么移动的时候我们明显看到数据发生了变化,这里只是每次在移动的时候都获取MapView的中心点坐标,因为ImageView是始终在中心显示的,因此每次取得就是中心坐标,然后再进行Poi搜素就可以了.这样就可以发现Fragment里的数据会发生明显的变化.
这里我第一次定位和Poi搜索都是在MainActivity里面完成的,然后将数据通过setArguments()传递过去.Fragment在首次加载的时候只需要getArguments()来获取相应的数据,然后在自己的ListView当中设置adapter就可以第一次直接显示数据了,同时Fragment在onAttach到Activity的时候需要注册一个广播,这个广播的用来接收,当地图状态发生改变的时候,也就是我们平移了地图,MainActivity需要告知Fragment地图状态已经发生了变化,需要更新Poi数据了.那么Fragment在接收到这条广播的时候,就知道地图状态已经改变,需要根据当前的中心点坐标搜索出现在的Poi数据,然后通过adapter.notifyDataSetChanged来更新ListView里面的数据即可.
/** * 第一次加载的时候由Activity搜索,将数据传递给Fragment,后续由Fragment来完成搜索功能 * */ Bundle allPoiBundle = new Bundle(); allPoiBundle.putParcelableArrayList("allPoiData", (ArrayList<? extends Parcelable>) allPoiData); allPoiFragment.setArguments(allPoiBundle); //获取数据,只需要在OnCreateView获取 @Nullable @Override public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_all, null); allPoiSearch = PoiSearch.newInstance(); allData.clear(); allData = getArguments().getParcelableArrayList("allPoiData"); initView(view); return view; }
这里我们可以看到我只在MainActivity搜索了一次Poi,如果我们每次都在MainActivity中搜索完Poi然后再传递Fragment的话,这样其实会耗费大量的时间,数据更新的速度也比较的慢,大家可以去试试每次在MainActivity中定位完数据之后再发送个Fragment.看看这样实现是否优雅.其实我们也可以完全不在MainActivity中搜索Poi,完全交给Fragment其实也是可以的.
这里还需要说一下Fragment的一个数据预加载问题,我们都知道Fragment是有预加载机制的,默认情况下Fragment只会预加载一页数据,因此这里我改变了它的预加载数量.
/** * 这里修改了Fragment的预加载数量,一次加载三页数据,默认是一页 * */ viewPager.setOffscreenPageLimit(3);
改变这个也是有原因的,因为我们需要在OnAttach时为Fragment注册一个广播,监听地图状态是否发生了变化,如果使用默认的加载机制,比如说我们现在就在全部这个Fragment页面,那么只有全部和写字楼注册了这个广播,其他两个页面还没有OnAttach到Activity上,这时我们改变地图状态,前面这两页数据会发生明显变化,而后面的两页是根本不知道数据已经变化了.同理一样.如果我们在最后一页,前两页已经被销毁,已经onDetach()Activity了,那么这两页也是拿不到数据的.因此我这里改变了它的预加载数量.无论如何滑动,都能够接收到数据.使用默认的预加载机制,出现问题的主要原因其实和Fragment的生命周期有紧密关联的.有空我会去写一篇关于Fragment的生命周期的博客.现在我们只需要知道就可以了.
最后就是这个蛋疼的搜索框,浪费了我相当长的时间.按照ele的效果来看,当点击搜索框的时候,需要弹出一个页面覆盖掉这个页面,然后根据关键字搜索出数据,以列表项的形式展现出来,其实这里就使用到了在下建议查询,根据我们输入的关键字搜素出相应的数据信息.但是这里蛋疼的问题在于当我们点击返回的键的时候不是结束这个Activity,而是返回到地图这个页面.因此这里我这里只能重写onKeyDown事件,然后拦截Back事件.如果搜索框中的EditText在获取焦点状态的情况下,点击返回键的话,那么返回地图页面,一直不结束这个Activity,但是蛋疼的事就来了,当返回到这个MapView页面的时候,搜索框中的EditText仍然优先获取到了焦点,无论我们怎么点击返回键,这个Activity都不会被销毁.这样就有很大的问题.最后我找到了一种方法,只能让整个搜索框这个布局去抢占EditText的焦点.那么在返回地图的时候,EditText就永远不会优先获取到焦点事件了.而且还能够正常销毁Activity.
/** * 监听onKeyDown事件 * 目的是判断当前页面是地图显示页面还是在线建议查询页面 * */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { if (isFocus) { inputPoiSearchLayout.setVisibility(View.GONE); location_name.setText(""); location_name.clearFocus(); keyWordPoiData.clear(); layout.setFocusable(true); layout.setFocusableInTouchMode(true); layout.requestFocus(); isFocus = false; return true; } } return super.onKeyDown(keyCode, event); }
该说的也就这么多了,用上的知识点,以及如何具体实现.最后放上一个源代码.还有一些需要注意的就是如果使用Android Studio开发.并且jar包都保存在libs文件夹中的话,在build.gradle中别忘了配置.
sourceSets { main { jniLibs.srcDir 'libs' //这里必须要有,否则会报.so文件的异常 } instrumentTest.setRoot('tests') debug.setRoot('build-types/debug') release.setRoot('build-types/release') }
源代码(链接) http://pan.baidu.com/s/1mhMQumc