轉載請標明出處:https://www.jianshu.com/p/29b60ca83be2
吐槽自己:好長的標題啊
這個功能想必大家都很熟悉,但是網上搜索到的幾篇文章要麼是大段的代碼看的頭暈,要麼是不求甚解的複製粘貼,今天我們從佈局到實現原理一步步分析,讓你也能完成一個仿美團外賣的地址選擇頁面。
本文項目 GitHub 地址:https://github.com/junerver/BaiduMapDemo
注意:示例項目使用 Kotlin 編寫,不瞭解 Kotlin 的小夥伴可以參考博文中的 Java 代碼;
頁面佈局
首先我們從美團外賣的頁面佈局開始分析,如下圖所示:
可以看出該頁面由4個部分組成:1、城市選擇;2、地點搜索;3、可拖拽選點的地圖空間(我們用百度地圖來實現);4、地圖選點附近的建築(POI信息);
城市選擇部分我們不做詳細分析,因爲該處網上有很多示例,注意要使用百度地圖的定位sdk獲取當前位置城市一次(用於POI搜索)。
地點搜索:如下圖所示,點擊搜索輸入框後並不是打開一個新頁面,而是遮擋了地圖選點與附近POI信息。
所以我們的頁面佈局可以是這樣的:
UI邏輯:使用一個boolean變量作爲標誌位,默認爲true顯示選點佈局,false顯示搜索佈局。
當用戶點擊搜索框時,隱藏選點佈局、顯示搜索佈局,並將標誌位置爲false;點擊左上角的返回按鈕時判斷爲選點佈局直接finish頁面,爲搜索佈局則隱藏搜索佈局、顯示選點佈局(物理返回鍵邏輯類似);
tip:地圖選點中標註當前位置的小紅點不是調用百度地圖控件生成的,而是直接在一個FrameLayout 中同時放置了一個小紅點ImageView與一個百度地圖的MapView。
完整的頁面代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_topbar"
android:orientation="vertical"
tools:context="com.yongsha.market.my.activity.SelectJiedaoMapActivity">
<RelativeLayout
android:id="@+id/layout_login_topbar"
style="@style/TopbarStyle" >
<ImageView
android:id="@+id/img_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_margin="6dp"
android:src="@drawable/flight_title_back_normal" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="街道選擇"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/black"
android:textSize="@dimen/medium_text_size" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:layout_gravity="center_vertical"
android:src="@drawable/gps_grey"
android:layout_marginLeft="8dp"/>
<TextView
android:id="@+id/tv_selected_city"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="14sp"
android:ellipsize="end"
android:maxLines="1"
android:ems="3"
android:text="[城市]"
tools:text="阿壩藏族羌族自治州"
android:layout_marginLeft="3dp"/>
<EditText
android:id="@+id/et_jiedao_name"
android:background="@drawable/border_search"
android:padding="6dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:hint="請輸入街道名稱"
android:textSize="14sp"
android:gravity="center_vertical"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"/>
<View
android:layout_width="8dp"
android:layout_height="match_parent"/>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_map"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fitsSystemWindows="true"
android:orientation="vertical"
>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.baidu.mapapi.map.MapView
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:onClick="true" >
</com.baidu.mapapi.map.MapView>
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:src="@drawable/icon_loc"/>
</FrameLayout>
<ListView
android:id="@+id/rv_result"
android:background="#ffffff"
android:layout_marginTop="1dp"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:cacheColorHint="#00000000"
android:descendantFocusability="beforeDescendants"
android:fastScrollEnabled="true"
android:scrollbars="none"/>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fitsSystemWindows="true"
android:orientation="vertical"
android:visibility="gone">
<ListView
android:id="@+id/lv_search"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:cacheColorHint="#00000000"
android:descendantFocusability="beforeDescendants"
android:fastScrollEnabled="true"
android:scrollbars="none" />
</LinearLayout>
</LinearLayout>
三個重要概念
實現該功能我們必須要了解以下三個重要的概念、功能(標題可以直接點擊進入百度sdk的相關介紹頁面):
定位:
這個就不用細說了,進入頁面後我們應該首先將MapView顯示到用戶的當前位置,獲取用戶的城市信息,處理第一次地理編碼獲取用戶定位位置的POI信息;地理編碼:
當用戶拖拽地圖View時,我們獲取地圖中心點的經緯度信息,進行地理編碼,並從編碼信息的回調接口獲取到該位置的POI信息列表,用於展示在MapView下面的列表中;POI熱詞建議檢索:
當用戶在搜索框鍵入內容時,根據用戶當前所在城市或自行選擇的城市,發起POI熱詞建議檢索,並將檢索結果顯示到列表中;
實現步驟
瞭解了上述的三個重要的概念之後我們可以來整理一下思路開始一步步實現我們的頁面了(在文末我會放上GitHub上demo項目的地址)。
1. 實例化上述的三個類,併爲之註冊監聽器
//1、地圖、定位相關
mBaiduMap = mMap.getMap();
MapStatus mapStatus = new MapStatus.Builder().zoom(15).build();
MapStatusUpdate mMapStatusUpdate = MapStatusUpdateFactory.newMapStatus(mapStatus);
mBaiduMap.setMapStatus(mMapStatusUpdate);
// 地圖狀態改變相關監聽
mBaiduMap.setOnMapStatusChangeListener(this);
// 開啓定位圖層
mBaiduMap.setMyLocationEnabled(true);
// 定位圖層顯示方式
mCurrentMode = MyLocationConfiguration.LocationMode.NORMAL;
mBaiduMap.setMyLocationConfigeration(new MyLocationConfiguration(mCurrentMode, true, null));
mLocClient = new LocationClient(this);
// 註冊定位監聽 注意在最新版本中這個方法已經被標註爲廢棄
mLocClient.registerLocationListener(this);
// 定位選項
LocationClientOption option = new LocationClientOption();
/**
* coorType - 取值有3個: 返回國測局經緯度座標系:gcj02 返回百度墨卡託座標系 :bd09 返回百度經緯度座標系
* :bd09ll
*/
option.setCoorType("bd09ll");
// 設置是否需要地址信息,默認爲無地址
option.setIsNeedAddress(true);
// 設置是否需要返回位置語義化信息,可以在BDLocation.getLocationDescribe()中得到數據,ex:"在天安門附近",
// 可以用作地址信息的補充
option.setIsNeedLocationDescribe(true);
// 設置是否需要返回位置POI信息,可以在BDLocation.getPoiList()中得到數據
option.setIsNeedLocationPoiList(true);
/**
* 設置定位模式 Battery_Saving 低功耗模式 Device_Sensors 僅設備(Gps)模式 Hight_Accuracy
* 高精度模式
*/
option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);
// 設置是否打開gps進行定位
option.setOpenGps(true);
// 設置掃描間隔,單位是毫秒 當<1000(1s)時,定時定位無效
option.setScanSpan(1000);
// 設置 LocationClientOption
mLocClient.setLocOption(option);
// 開始定位
mLocClient.start();
// 2、實例化POI熱詞建議檢索,註冊搜索事件監聽
mSuggestionSearch = SuggestionSearch.newInstance();
mSuggestionSearch.setOnGetSuggestionResultListener(new OnGetSuggestionResultListener() {
@Override
public void onGetSuggestionResult(SuggestionResult suggestionResult) {
if (suggestionResult == null || suggestionResult.getAllSuggestions() == null) {
return;
}
mSuggestionInfos.clear();//清空數據列表
sugAdapter.clear();//清空列表適配器
List<SuggestionResult.SuggestionInfo> suggestionInfoList = suggestionResult.getAllSuggestions();
if (suggestionInfoList != null) {
for (SuggestionResult.SuggestionInfo info : suggestionInfoList) {
if (info.pt != null) {
//過濾沒有經緯度信息的
mSuggestionInfos.add(info);
sugAdapter.add(info.district + info.key);
}
}
}
sugAdapter.notifyDataSetChanged();
}
});
// 3、創建GeoCoder實例對象
geoCoder = GeoCoder.newInstance();
// 設置查詢結果監聽者 ####這裏很重要該回調接口有兩個方法
geoCoder.setOnGetGeoCodeResultListener(this);
sugAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line);
//4、熱詞建議檢索結果列表
mLvSearch.setAdapter(sugAdapter);
mLvSearch.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
SuggestionResult.SuggestionInfo info = mSuggestionInfos.get(i);
String str = "";
if (info.pt != null) {
str = " 經度:" + info.pt.longitude + " 緯度:" + info.pt.latitude;
}
Logger.d(info.district + info.key + str);
//你自己的業務
setResult(RESULT_OK, intent);
finish();
}
});
//5、搜索框輸入監聽, 當輸入關鍵字變化時,動態更新建議列表
mEtJiedaoName.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable arg0) {
}
@Override
public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
}
@Override
public void onTextChanged(CharSequence cs, int arg1, int arg2, int arg3) {
if (cs.length() <= 0) {
return;
}
/**
* 使用建議搜索服務獲取建議列表,結果在onSuggestionResult()中更新
*/
Logger.d(mSelectCity);
mSuggestionSearch.requestSuggestion((new SuggestionSearchOption())
.citylimit(true)
.keyword(cs.toString())
.city(mSelectCity));
}
});
2. 幾個重要的回調函數
BaiduMap.OnMapStatusChangeListener
, BDLocationListener
, OnGetGeoCoderResultListener
第一個回調用於監聽我們手指在MapView上移動時地圖狀態變化,其中包含4個方法,我們只需要用到其中的onMapStatusChangeFinish
這個一個方法即可:
@Override
public void onMapStatusChangeFinish(MapStatus mapStatus) {
// 獲取地圖最後狀態改變的中心點
LatLng cenpt = mapStatus.target;
Logger.d("最後停止點:" + cenpt.latitude + "," + cenpt.longitude);
//發起地理編碼,當轉化成功後調用onGetReverseGeoCodeResult()方法
geoCoder.reverseGeoCode(new ReverseGeoCodeOption().location(cenpt));
}
第二個回調是用於接收百度定位 SDK 獲取到的位置的,只包含一個方法,實現如下:
@Override
public void onReceiveLocation(BDLocation bdLocation) {
// 如果bdLocation爲空或mapView銷燬後不再處理新數據接收的位置
if (bdLocation == null || mBaiduMap == null) {
return;
}
MyLocationData data = new MyLocationData.Builder()// 定位數據
.accuracy(bdLocation.getRadius())// 定位精度bdLocation.getRadius()
.direction(bdLocation.getDirection())// 此處設置開發者獲取到的方向信息,順時針0-360
.latitude(bdLocation.getLatitude())// 經度
.longitude(bdLocation.getLongitude())// 緯度
.build();// 構建
mBaiduMap.setMyLocationData(data);// 設置定位數據
// 是否是第一次定位
if (isFirstLoc) {
isFirstLoc = false;
LatLng ll = new LatLng(bdLocation.getLatitude(), bdLocation.getLongitude());
MapStatusUpdate msu = MapStatusUpdateFactory.newLatLngZoom(ll, 18);
mBaiduMap.animateMapStatus(msu);
locationLatLng = new LatLng(bdLocation.getLatitude(), bdLocation.getLongitude());
// 獲取城市,待會用於POI熱詞建議檢索
mSelectCity = bdLocation.getCity();
if (mSelectCity.endsWith("市")) {
mSelectCity = mSelectCity.substring(0, mSelectCity.length() - 1);
}
mTvSelectedCity.setText(mSelectCity);
// 由於sdk會不斷的接收定位信息,所以顯示附近POI只需在第一次定位時發起反地理編碼請求(經緯度->地址信息)即可
ReverseGeoCodeOption reverseGeoCodeOption = new ReverseGeoCodeOption();
// 設置反地理編碼位置座標
reverseGeoCodeOption.location(locationLatLng);
geoCoder.reverseGeoCode(reverseGeoCodeOption);
}
}
第三個回調用於處理GeoCoder地理編碼的返回結果,該接口包含兩個方法,我們只需要使用其中的onGetReverseGeoCodeResult
,該接口表示反向編碼即經緯度 -> 地理位置的結果。
@Override
public void onGetReverseGeoCodeResult(ReverseGeoCodeResult reverseGeoCodeResult) {
final List<PoiInfo> poiInfos = reverseGeoCodeResult.getPoiList();
Logger.d("這裏的值:" + poiInfos);
if (poiInfos != null && !"".equals(poiInfos)) {
//地圖選點附近的POI信息
PoiAdapter poiAdapter = new PoiAdapter(mContext, poiInfos);
mRvResult.setAdapter(poiAdapter);
mRvResult.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
PoiInfo poiInfo = poiInfos.get(position);
Logger.d(poiInfo.address + " " + poiInfo.name);
//你的業務
setResult(RESULT_OK, intent);
finish();
}
});
}
}
3. POI熱詞建議檢索的 Adapter 與選點附近POI列表的 Adapter
/**
* POI熱詞建議檢索
*/
class PoiSearchAdapter extends BaseAdapter {
private Context context;
private List<PoiInfo> list;
private ViewHolder holder;
public PoiSearchAdapter(Context context, List<PoiInfo> appGroup) {
this.context = context;
this.list = appGroup;
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int location) {
return list.get(location);
}
@Override
public long getItemId(int arg0) {
return arg0;
}
public void addObject(List<PoiInfo> mAppGroup) {
this.list = mAppGroup;
notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
holder = new ViewHolder();
convertView = LayoutInflater.from(context).inflate(R.layout.activity_poi_search_item, null);
holder.mpoi_name = (TextView) convertView.findViewById(R.id.mpoiNameT);
holder.mpoi_address = (TextView) convertView.findViewById(R.id.mpoiAddressT);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.mpoi_name.setText(list.get(position).name);
holder.mpoi_address.setText(list.get(position).address);
// Log.i("yxx", "==1=poi===城市:" + poiInfo.city + "名字:" +
// poiInfo.name + "地址:" + poiInfo.address);
return convertView;
}
public class ViewHolder {
public TextView mpoi_name;// 名稱
public TextView mpoi_address;// 地址
}
}
/**
* 拖動檢索提示
*/
class PoiAdapter extends BaseAdapter {
private Context context;
private List<PoiInfo> pois;
private LinearLayout linearLayout;
PoiAdapter(Context context, List<PoiInfo> pois) {
this.context = context;
this.pois = pois;
}
@Override
public int getCount() {
return pois.size();
}
@Override
public Object getItem(int position) {
return pois.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.locationpois_item, null);
linearLayout = (LinearLayout) convertView.findViewById(R.id.locationpois_linearlayout);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
if (position == 0) {
holder.iv_gps.setImageDrawable(getResources().getDrawable(R.drawable.gps_orange));
holder.locationpoi_name.setTextColor(Color.parseColor("#FF9D06"));
holder.locationpoi_address.setTextColor(Color.parseColor("#FF9D06"));
} else {
holder.iv_gps.setImageDrawable(getResources().getDrawable(R.drawable.gps_grey));
holder.locationpoi_name.setTextColor(Color.parseColor("#4A4A4A"));
holder.locationpoi_address.setTextColor(Color.parseColor("#7b7b7b"));
}
PoiInfo poiInfo = pois.get(position);
holder.locationpoi_name.setText(poiInfo.name);
holder.locationpoi_address.setText(poiInfo.address);
return convertView;
}
class ViewHolder {
ImageView iv_gps;
TextView locationpoi_name;
TextView locationpoi_address;
ViewHolder(View view) {
locationpoi_name = (TextView) view.findViewById(R.id.locationpois_name);
locationpoi_address = (TextView) view.findViewById(R.id.locationpois_address);
iv_gps = (ImageView) view.findViewById(R.id.iv_gps);
}
}
}
至此我們已經完全的實現了該頁面的全部功能,對了,還有幾個點擊事件,如下所示:
@OnClick({R.id.img_back, R.id.et_jiedao_name,R.id.tv_selected_city})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.img_back:
if (!acStateIsMap) {
mLlMap.setVisibility(View.VISIBLE);
mLlSearch.setVisibility(View.GONE);
acStateIsMap = true;
} else {
this.setResult(Activity.RESULT_CANCELED);
finish();
}
break;
case R.id.et_jiedao_name:
if (acStateIsMap) {
mLlMap.setVisibility(View.GONE);
mLlSearch.setVisibility(View.VISIBLE);
acStateIsMap = false;
}
break;
case R.id.tv_selected_city:
//手動選擇城市
Intent i = new Intent(mContext, ChooseCityActivity.class);
i.putExtra("flag", "selectCity");
startActivityForResult(i,REQUEST_CODE_CITY);
break;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CITY && resultCode == Activity.RESULT_OK) {
//重新選擇了城市
mSelectCity = data.getStringExtra("city");//該字段用於POI熱詞建議檢索
mTvSelectedCity.setText(mSelectCity);
mSuggestionInfos.clear();
sugAdapter.clear();
sugAdapter.notifyDataSetChanged();
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
if (!acStateIsMap) {
mLlMap.setVisibility(View.VISIBLE);
mLlSearch.setVisibility(View.GONE);
acStateIsMap = true;
return false;
} else {
this.setResult(Activity.RESULT_CANCELED);
finish();
return true;
}
}
return super.onKeyDown(keyCode, event);
}
至此我們就完整的實現了這個功能了,代碼參考了很多網上其他大神的實現,再次表示感謝。我只是對實現邏輯進行了梳理與介紹,想必看完本文後你也已經很明瞭了每段代碼的爲什麼要這樣寫了。如果本文對您有一絲微小的幫助,請點贊、喜歡。