转载请标明出处: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的相关介绍页面):
定位:
这个就不用细说了,进入页面后我们应该首先将MapView显示到用户的当前位置,获取用户的城市信息,处理第一次地理编码获取用户定位位置的POI信息;
地理编码:
当用户拖拽地图View时,我们获取地图中心点的经纬度信息,进行地理编码,并从编码信息的回调接口获取到该位置的POI信息列表,用于展示在MapView下面的列表中;
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
| 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();
option.setCoorType("bd09ll");
option.setIsNeedAddress(true);
option.setIsNeedLocationDescribe(true);
option.setIsNeedLocationPoiList(true);
option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);
option.setOpenGps(true);
option.setScanSpan(1000);
mLocClient.setLocOption(option);
mLocClient.start();
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(); } });
geoCoder = GeoCoder.newInstance();
geoCoder.setOnGetGeoCodeResultListener(this); sugAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line);
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(); } }); 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; }
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); 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) { if (bdLocation == null || mBaiduMap == null) { return; } MyLocationData data = new MyLocationData.Builder() .accuracy(bdLocation.getRadius()) .direction(bdLocation.getDirection()) .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()); mSelectCity = bdLocation.getCity(); if (mSelectCity.endsWith("市")) { mSelectCity = mSelectCity.substring(0, mSelectCity.length() - 1); } mTvSelectedCity.setText(mSelectCity); 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)) { 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
|
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); 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"); 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); }
|
至此我们就完整的实现了这个功能了,代码参考了很多网上其他大神的实现,再次表示感谢。我只是对实现逻辑进行了梳理与介绍,想必看完本文后你也已经很明了了每段代码的为什么要这样写了。如果本文对您有一丝微小的帮助,请点赞、喜欢。