通过百度地图实现仿美团外卖的地图选点确定收货地址

转载请标明出处:https://blog.csdn.net/u011133887/article/details/80372616
吐槽自己:好长的标题啊

这个功能想必大家都很熟悉,但是网上搜索到的几篇文章要么是大段的代码看的头晕,要么是不求甚解的复制粘贴,今天我们从布局到实现原理一步步分析,让你也能完成一个仿美团外卖的地址选择页面。

本文项目 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。

完整的页面代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
<?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的相关介绍页面):

  1. 定位
    这个就不用细说了,进入页面后我们应该首先将MapView显示到用户的当前位置,获取用户的城市信息,处理第一次地理编码获取用户定位位置的POI信息;

  2. 地理编码
    当用户拖拽地图View时,我们获取地图中心点的经纬度信息,进行地理编码,并从编码信息的回调接口获取到该位置的POI信息列表,用于展示在MapView下面的列表中;

  3. POI热词建议检索
    当用户在搜索框键入内容时,根据用户当前所在城市或自行选择的城市,发起POI热词建议检索,并将检索结果显示到列表中;

实现步骤

了解了上述的三个重要的概念之后我们可以来整理一下思路开始一步步实现我们的页面了(在文末我会放上GitHub上demo项目的地址)。

1. 实例化上述的三个类,并为之注册监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//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这个一个方法即可:

1
2
3
4
5
6
7
8
@Override
public void onMapStatusChangeFinish(MapStatus mapStatus) {
// 获取地图最后状态改变的中心点
LatLng cenpt = mapStatus.target;
Logger.d("最后停止点:" + cenpt.latitude + "," + cenpt.longitude);
//发起地理编码,当转化成功后调用onGetReverseGeoCodeResult()方法
geoCoder.reverseGeoCode(new ReverseGeoCodeOption().location(cenpt));
}

第二个回调是用于接收百度定位 SDK 获取到的位置的,只包含一个方法,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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 ,该接口表示反向编码即经纬度 -> 地理位置的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 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;// 地址


}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* 拖动检索提示
*/
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);
}
}
}

至此我们已经完全的实现了该页面的全部功能,对了,还有几个点击事件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@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);
}

至此我们就完整的实现了这个功能了,代码参考了很多网上其他大神的实现,再次表示感谢。我只是对实现逻辑进行了梳理与介绍,想必看完本文后你也已经很明了了每段代码的为什么要这样写了。如果本文对您有一丝微小的帮助,请点赞、喜欢。