程序员人生 网站导航

[置顶] 模仿百度地图的LBS服务――离线地图篇 Part 2 (v 3.1.1)

栏目:综合技术时间:2014-12-09 08:20:19



1、前言



转载请标明出处:http://blog.csdn.net/wlwlwlwl015/article/details/41492031

这1篇blog写的真心不容易,我只想说我这类菜鸟去高仿百度地图去做LBS服务真心有点作死,期间本想放弃,做简单点算了,但不能说服自己。最后通过F6去1行1行的debug(新手朋友注意这是最好的解决问题的方式没有之1),最后成功完成了核心的功能。上1篇blog高仿了百度地图离线地图模块中的“城市列表”部份(模仿百度地图的LBS服务――离线地图篇 Part1),城市实现里“当前城市”、“热门城市”、“全国省市”数据信息的展现,那末本篇blog主要记录的就是如何进行下载了,一样的是高仿百度离线地图的“下载管理”模块。空话不多说,下面就分步骤进行逐一介绍了。



2、百度离线地图“下载管理”功能分析



老规矩我们先来看看百度地图离线地图中“下载管理”模块的界面和功能:


通过这上面两幅截图我们分析1下需要做的工作:

1.下载管理分为“正在下载”和“下载完成”两部份,所以整体界面应当分为2个ListView。

2.可以通过进度条实时看到下载任务的进度,下载中可以进行“暂停下载”和“删除”的操作,那末暂停的时候一定也能够履行“开始下载”的操作。具体细节大家可以通过操作百度地图来看。

3.已下载完成的地图提供了“查看地图”和“删除”的功能。


大体上功能就上面提到的这些,具体的细节在下面的代码中再看,下面就依照我的开发顺序来告知大家先做甚么、后做甚么、具体怎样做,对高手来讲高仿这个东东可能不算甚么,但是对我来讲确切难度挺大,整整两天,吃饭睡觉都在想细节,终究之所以实现了,是由于我首先会理清思路,先斟酌我需要做甚么并将任务拆分开,然后是这些步骤应当按甚么顺序去做,就这样1点1点做,1行1行debug,终究得以完成。所以下面我就列出开发步骤,再逐1说明。


Step 1 初始化

Step 2 编写离线地图事件通知接口及其回调方法中的代码

Step 3 分别编写“正在下载”和“下载完成”的布局和Adapter

Step 4 编写测试方法,即开始履行下载任务


Step 1 

上面这个说的可能还不够细节,但是没关系,我会在下面通过代码去逐一解释清楚。首先来看看初始化,先上代码(注意和上1篇是同1个Activity,所以重复代码就不贴了):

// 已下载的离线地图数据List private ArrayList<MKOLUpdateElement> isDoingUpdateMapList = null; // 正在下载的数据列表(包括下载中、暂停的) private ArrayList<MKOLUpdateElement> downLoadingMapList = new ArrayList<MKOLUpdateElement>(); // 已下载完成的数据列表 private ArrayList<MKOLUpdateElement> downLoadedMapList = new ArrayList<MKOLUpdateElement>(); // 已下载的离线地图数据Adapter和ListView private DownLoadingListView downLoadingListView; // 正在下载的ListView private DownLoadingAdapter mDownLoadingAdapter = new DownLoadingAdapter(); private DownLoadedListView downLoadedListView; // 下载完成的ListView private DownLoadedAdapter mDownLoadedAdapter = new DownLoadedAdapter();

上面的是声明部份,关于“正在下载”和“下载完成”一样是两个自定义的ListView去解决事件冲突问题,解决方案和上1篇blog1样,都是重写onMeasure方法。


一样的声明以后应当进行初始化工作,用来初始化之前的下载任务,比如:两个下载任务都下了1半暂停了,这里我们需要在“正在下载”的ListView中显示出来,参照百度地图应包括以下信息:城市名、数据包大小、下载状态、当前的下载进度等。下面这段代码一样位于initData()方法中:

// 初始化已下载的城市列表,并根据已下载和下载中进行分类 isDoingUpdateMapList = mOfflineMap.getAllUpdateInfo(); if (isDoingUpdateMapList == null) { isDoingUpdateMapList = new ArrayList<MKOLUpdateElement>(); } if (isDoingUpdateMapList.size() > 0) { for (MKOLUpdateElement element : isDoingUpdateMapList) { // 如果下载进度为100则应放入“已下载”的List if (element.ratio == 100) { downLoadedMapList.add(element); mDownLoadedAdapter = new DownLoadedAdapter(); } // 如果下载进入不为100则应放入“正在下载”的List if (element.ratio != 100) { downLoadingMapList.add(element); mDownLoadingAdapter = new DownLoadingAdapter(); } } }

第2行的getAllUpdateInfo()方法很好用,是这个下载模块的核心方法,官方的解释是:返回各城市离线地图更新信息。对这个解释我是觉得很笼统,不明白,通过我的使用我认为这个方法的作用就是:返回当前已下载的(包括开始下载的、暂停的、正在下载的和完成的)的所有数据信息,并且是以城市为单位的List集合。根据官方文档可以看出返回值类型是MKOLUpdateElement,这个类也正式封装了1个下载任务应有的所有关键信息,而在上面的代码中我正是通过element.ratio来分割List,由于ratio正是下载进度的意思:


所以当ratio为100的时候,我就将这个对象放到“下载完成”的ListView,否则说明还没下载完,就放到“正在下载”的ListView。这样我们就能够在进入利用以后看到之前未完成的、已完成的下载记录了。OK,初始化很简单,结束了。


Step 2 

初始化完成以后,现在就来讲道说道离线地图中唯1的1个监听:public interface MKOfflineMapListener

一样的来看看官方文档中对它的解释:

离线地图事件通知接口。该接口返回新安装离线地图、下载更新、数据版本更新等结果,用户需要实现该接口以处理相应事件。


不知道是否是我书读的少,总觉得官方的解释不够通俗,看了仍然不知道怎样用。但是官方对这个接口的回调方法还是解释的比较清楚的:

void onGetOfflineMapState(int type,int state)

type - 事件类型: MKOfflineMap.TYPE_NEW_OFFLINE, MKOfflineMap.TYPE_DOWNLOAD_UPDATE, MKOfflineMap.TYPE_VER_UPDATE.

state - 事件状态: 当type为TYPE_NEW_OFFLINE时,表示新安装的离线地图数目. 当type为TYPE_DOWNLOAD_UPDATE时,表示更新的城市ID.

上面的蓝色字体都是官方文档的原话,这里我们也清楚了这个回调方法的两个参数都表示甚么意思了。根据需求,我们需要实时监控下载进度并反馈到UI,所以这里我们只需要关注TYPE_DOWNLOAD_UPDATE这个类型的事件便可,而正好此时的state就表示更新城市的ID,那末这个监听的作用就很明显了,就是通过它来实时获得下载进度并更新我们界面上的ProgressBar便可。可能有些人不清楚甚么时候会触发这个监听,那末通过打印语句来视察控制台不难发现,当type为TYPE_DOWNLOAD_UPDATE时,只要下载状态产生变化,即会触发监听。文字解释比较费力,下面我贴上监听代码,大家可以开启1个下载任务(后面说),并像第9行1样通过打印来视察1下都有甚么变化:

mOfflineMap.init(new MKOfflineMapListener() { @Override public void onGetOfflineMapState(int type, int state) { // TODO Auto-generated method stub switch (type) { case MKOfflineMap.TYPE_DOWNLOAD_UPDATE: // 得到当前正在下载的城市的具体更新信息 MKOLUpdateElement update = mOfflineMap.getUpdateInfo(state); Log.e(TAG, update.cityName + " ," + update.ratio); if (update != null) { // 此监听器在下载任务进行期间大概每下载1%触发1次,注意是大概 List<MKOLUpdateElement> elements = downLoadingMapList; for (MKOLUpdateElement element : elements) { if (update.cityID == element.cityID) { element.ratio = update.ratio; if (update.ratio == 100) { // 当下载进度到100时,Item从“下载中”的List移动到“已下载的List” downLoadingMapList.remove(element); downLoadedMapList.add(element); mDownLoadedAdapter.notifyDataSetChanged(); } break; } } } mDownLoadingAdapter.notifyDataSetChanged(); break; case MKOfflineMap.TYPE_NEW_OFFLINE: // 有新离线地图安装 Log.e(TAG, "TYPE_NEW_OFFLINE"); break; case MKOfflineMap.TYPE_VER_UPDATE: // 版本更新提示 break; } } });

相信认真看了代码的朋友肯定也大致明白了个123吧,有问题可以留言,虽然我是新手,可我自己写的东西还是解释的清的。


Step 3 

下面就是最重要的适配器了,我们来分别看看“下载中”的Adapter和“下载完成”的Adapter:

// 下载管理――正在下载――适配器 class DownLoadingAdapter extends BaseAdapter { @Override public int getCount() { // TODO Auto-generated method stub return downLoadingMapList.size(); } @Override public Object getItem(int position) { // TODO Auto-generated method stub return downLoadingMapList.get(position); } @Override public long getItemId(int position) { // TODO Auto-generated method stub return position; } @Override public View getView(final int position, View convertView, ViewGroup parent) { // TODO Auto-generated method stub final MKOLUpdateElement element = downLoadingMapList.get(position); ViewHolder holder = null; if (convertView == null) { convertView = mInflater.inflate(R.layout.down_loading_item, null); holder = new ViewHolder(); holder.cityName = (TextView) convertView .findViewById(R.id.id_city_name); holder.dataPakSize = (TextView) convertView .findViewById(R.id.id_data_size); holder.downLoadState = (TextView) convertView .findViewById(R.id.id_down_state); holder.downLoadRatio = (TextView) convertView .findViewById(R.id.id_down_raito); holder.downLoadProgress = (ProgressBar) convertView .findViewById(R.id.id_down_progress); holder.downPullIcon = (ImageButton) convertView .findViewById(R.id.id_expand_down_icon); holder.upPullIcon = (ImageButton) convertView .findViewById(R.id.id_expand_up_icon); holder.pauseDownBtn = (Button) convertView .findViewById(R.id.id_pause_down_btn); holder.startDownBtn = (Button) convertView .findViewById(R.id.id_start_down_btn); holder.deleteMapBtn = (Button) convertView .findViewById(R.id.id_delete_map); holder.btnGroup = (LinearLayout) convertView .findViewById(R.id.id_btn_group); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.cityName.setText(element.cityName); holder.dataPakSize.setText(NumberFormatUtil .dataSizeFormatter(element.serversize) + "M"); String stateInfo = ""; // 如果是正在下载的状态 if (element.status == MKOLUpdateElement.DOWNLOADING) { stateInfo = "正在下载"; holder.startDownBtn.setVisibility(View.GONE); holder.pauseDownBtn.setVisibility(View.VISIBLE); holder.downLoadState.setTextColor(Color.BLUE); } //如果是暂停的状态 if (element.status == MKOLUpdateElement.SUSPENDED) { stateInfo = "已暂停"; holder.downLoadState.setTextColor(Color.RED); holder.startDownBtn.setVisibility(View.VISIBLE); holder.pauseDownBtn.setVisibility(View.GONE); } holder.downLoadState.setText(stateInfo); //设置当前进度的百分数 holder.downLoadRatio.setText(element.ratio + "%"); //设置ProgressBar的进度 holder.downLoadProgress.setProgress(element.ratio); // 暂停下载 holder.pauseDownBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub int cityId = element.cityID; mOfflineMap.pause(cityId); Toast.makeText(OfflineActitivty.this, "暂停下载" + element.cityName + "离线地图:", Toast.LENGTH_SHORT).show(); isDoingUpdateMapList = mOfflineMap.getAllUpdateInfo(); if (isDoingUpdateMapList == null) { isDoingUpdateMapList = new ArrayList<MKOLUpdateElement>(); } if (isDoingUpdateMapList.size() > 0) { downLoadingMapList.clear(); for (MKOLUpdateElement element : isDoingUpdateMapList) { if (element.ratio != 100) { downLoadingMapList.add(element); } } } mDownLoadingAdapter.notifyDataSetChanged(); } }); // 开始下载 holder.startDownBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub int cityId = element.cityID; mOfflineMap.start(cityId); Toast.makeText(OfflineActitivty.this, "开始下载" + element.cityName + "离线地图:", Toast.LENGTH_SHORT).show(); isDoingUpdateMapList = mOfflineMap.getAllUpdateInfo(); if (isDoingUpdateMapList == null) { isDoingUpdateMapList = new ArrayList<MKOLUpdateElement>(); } if (isDoingUpdateMapList.size() > 0) { downLoadingMapList.clear(); for (MKOLUpdateElement element : isDoingUpdateMapList) { if (element.ratio != 100) { downLoadingMapList.add(element); } } } mDownLoadingAdapter.notifyDataSetChanged(); mDownLoadedAdapter.notifyDataSetChanged(); } }); // 删除地图 holder.deleteMapBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub int cityId = element.cityID; mOfflineMap.remove(cityId); Toast.makeText(OfflineActitivty.this, "已删除" + element.cityName + "离线地图:", Toast.LENGTH_SHORT).show(); downLoadingMapList.remove(position); mDownLoadingAdapter.notifyDataSetChanged(); } }); } } }); return convertView; } private class ViewHolder { TextView cityName; TextView dataPakSize; TextView downLoadState; TextView downLoadRatio; ProgressBar downLoadProgress; ImageButton downPullIcon; ImageButton upPullIcon; Button pauseDownBtn; Button startDownBtn; Button deleteMapBtn; LinearLayout btnGroup; } }


// 下载管理――已下载――适配器 class DownLoadedAdapter extends BaseAdapter { @Override public int getCount() { // TODO Auto-generated method stub return downLoadedMapList.size(); } @Override public Object getItem(int position) { // TODO Auto-generated method stub return downLoadedMapList.get(position); } @Override public long getItemId(int position) { // TODO Auto-generated method stub return position; } @Override public View getView(final int position, View convertView, ViewGroup parent) { // TODO Auto-generated method stub final MKOLUpdateElement element = downLoadedMapList.get(position); ViewHolder holder = null; if (convertView == null) { convertView = mInflater .inflate(R.layout.down_loaded_item, null); holder = new ViewHolder(); holder.cityName = (TextView) convertView .findViewById(R.id.id_city_name); holder.isHasNewData = (TextView) convertView .findViewById(R.id.id_is_has_new); holder.dataPakSize = (TextView) convertView .findViewById(R.id.id_data_pak_size); holder.deleteMapBtn = (Button) convertView .findViewById(R.id.id_btn_delete_map); holder.seeMapDetailBtn = (Button) convertView .findViewById(R.id.id_see_map_detail); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.cityName.setText(element.cityName); holder.dataPakSize.setText(NumberFormatUtil .dataSizeFormatter(element.serversize) + "M"); // 删除地图 holder.deleteMapBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub // 删除已下载的离线Map int cityId = element.cityID; mOfflineMap.remove(cityId); Toast.makeText(OfflineActitivty.this, "已删除" + element.cityName + "离线地图:", Toast.LENGTH_SHORT).show(); downLoadedMapList.remove(position); mDownLoadedAdapter.notifyDataSetChanged(); } }); // 查看地图 holder.seeMapDetailBtn .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub Intent intent = new Intent(); intent.putExtra("x", element.geoPt.longitude); intent.putExtra("y", element.geoPt.latitude); intent.setClass(OfflineActitivty.this, BaseMapActivity.class); startActivity(intent); } }); return convertView; } private class ViewHolder { private TextView cityName; private TextView isHasNewData; private TextView dataPakSize; private Button deleteMapBtn; private Button seeMapDetailBtn; } }

可以看到由于“下载中”有开始和暂停两个按钮,所以比“下载完成”稍微麻烦1些,而下载完成以后便可以查看地图,即根据经纬坐标去加载改城市的地图,代码很简单就是官方Demo中的那个BaseMapActivity。item的布局也很简单,模仿百度地图的样式拼凑1下就行了,后面会贴上动态效果图,其实到这里离线地图模块的核心功能都已完成,最后看看如何通过点击Item去开启1个下载任务吧。


Step 4 

通过上面的3个步骤就已完成了离线地图的所有准备工作了,下面只剩下点击列表项开启下载任务了,仍然是参考百度地图,简单的流程是这样的:

点击任意列表中的任意项(包括热门城市的ListView、全国省市的ExpandableListView的子项),如果被点击的城市没有下载,那末新开1个下载任务在“正在下载”的列表,如果被点击的城市“正在下载”或“已暂停”,那末只需用切换到“下载管理列表”便可,最后如果被点击的城市“已下载完成”,那末一样的只是切换1下便可。


下面贴上剩余全部代码,包括初始化ListView和添加Item点击:

// 初始化ListView private void initListView() { // 热门城市 hotCitieslistView = (HotCitiesListView) findViewById(R.id.id_hotcities_lv); mHotCityAdapter = new HotCityAdapter(); hotCitieslistView.setAdapter(mHotCityAdapter); // 全国省市 allCitieslistView = (NationalCitiesListView) findViewById(R.id.id_allcities_exp_lv); mNationalCityAdapter = new NationalCityAdapter(); allCitieslistView.setAdapter(mNationalCityAdapter); // 下载管理――下载中 downLoadingListView = (DownLoadingListView) findViewById(R.id.id_download_manager_lv); downLoadingListView.setAdapter(mDownLoadingAdapter); // 下载管理――下载完成 downLoadedListView = (DownLoadedListView) findViewById(R.id.id_download_manager_lv_2); downLoadedListView.setAdapter(mDownLoadedAdapter); // 设置热门城市的Item点击事件 hotCitieslistView .setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // // TODO Auto-generated method stub OfflineMapItemBean offlineMapItemBean = mHotCityDatas .get(position); int cityId = offlineMapItemBean.getCityId(); String cityName = offlineMapItemBean.getCityName(); MKOLUpdateElement element = mOfflineMap .getUpdateInfo(cityId); // 如果进度为0,则下载。否则仅仅切换过去 if (element == null) { // 开始下载 mOfflineMap.start(cityId); offlineMapItemBean .setDownloadStatus(DownLoadStatus.DOWNLOADING); // 切换到下载管理 tb.setChecked(false); llayout1.setVisibility(View.VISIBLE); llayout2.setVisibility(View.GONE); Toast.makeText(OfflineActitivty.this, "开始下载" + cityName + "离线地图", Toast.LENGTH_SHORT).show(); // 更新界面显示 isDoingUpdateMapList = mOfflineMap .getAllUpdateInfo(); if (isDoingUpdateMapList == null) { isDoingUpdateMapList = new ArrayList<MKOLUpdateElement>(); } // 现在就有更新信息了 element = mOfflineMap.getUpdateInfo(cityId); downLoadingMapList.add(element); mDownLoadingAdapter.notifyDataSetChanged(); mHotCityAdapter.notifyDataSetChanged(); } else { // 仅仅跳转 // 切换到下载管理 tb.setChecked(false); llayout1.setVisibility(View.VISIBLE); llayout2.setVisibility(View.GONE); } } }); // 设置全国省市的Item点击事件 allCitieslistView.setOnChildClickListener(new OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { OfflineMapItemBean offlineMapItemBean = allCityDatas .get(groupPosition).getChildCities().get(childPosition); int cityId = offlineMapItemBean.getCityId(); String cityName = offlineMapItemBean.getCityName(); // Toast.makeText(OfflineActitivty.this, cityId+","+cityName, // Toast.LENGTH_SHORT).show(); MKOLUpdateElement element = mOfflineMap.getUpdateInfo(cityId); // 如果进度为0,则下载。否则仅仅切换过去 if (element == null) { // 开始下载 mOfflineMap.start(cityId); offlineMapItemBean .setDownloadStatus(DownLoadStatus.DOWNLOADING); // 切换到下载管理 tb.setChecked(false); llayout1.setVisibility(View.VISIBLE); llayout2.setVisibility(View.GONE); Toast.makeText(OfflineActitivty.this, "开始下载" + cityName + "离线地图:", Toast.LENGTH_SHORT) .show(); // 更新界面显示 isDoingUpdateMapList = mOfflineMap.getAllUpdateInfo(); if (isDoingUpdateMapList == null) { isDoingUpdateMapList = new ArrayList<MKOLUpdateElement>(); } // 现在就有更新信息了 element = mOfflineMap.getUpdateInfo(cityId); downLoadingMapList.add(element); mDownLoadingAdapter.notifyDataSetChanged(); mHotCityAdapter.notifyDataSetChanged(); } else { // 仅仅跳转 // 切换到下载管理 tb.setChecked(false); llayout1.setVisibility(View.VISIBLE); llayout2.setVisibility(View.GONE); } return false; } }); }

最后做1些说明,包括我整体代码的不足和重点需要注意的地方

1.肯定有的朋友发现我在写“热门城市”的列表的时候还通过自定义的OfflineMapItemBean对数据进行了封装,在后面做“下载列表”的时候压根就没有再封装了,而是直接用SDK中的MKOLUpdateElement对象去取值了。这块是我做的不好,由于下载列表要拆分成“正在下载”和“已完成”两部份,每次获得更新对象再遍历拆分感觉也不是个办法,所以干脆就不封装了,我也没有想到更好的办法去处理,有更好的方法的朋友可以给我指导12。

2.当ScrollView嵌套ListView以后item的点击事件是失效的,包括ExpandableListView的子item也是1样。对这个我选择了在item的布局文件中添加1个属性:android:descendantFocusability="blocksDescendants"来解决的,但这不是个好办法,听说会让ViewHolder失效,问了其他朋友说是让ScrollView中的onTouch返回false之类的,我尝试了没有到达预期效果,如果哪位大神知道最优的解决方案还请给小弟指导12,感激不尽。

3.由于项目周期紧,还有很多小效果没来得及实现,比如:下载状态(已暂停、已完成等)应当在“下载管理”和“城市列表”中能实时的同步的更新等,但是整体上看效果还是挺不错的吧,下面就贴上效果图:



由于在摹拟器上调试时没法开始下载(offlineMap.start方法调用以后MKOLUpdateElement返回的更新对象的status始终是WAITING的状态,而在真机调试时调用start以后更新对象的status会立刻变成DOWNING,摹拟器的网络也没有问题,不知道是否是BUG),所以这里只贴上两张真机运行截图好了,UI比较粗糙,但是核心功能都没问题,到了这里离线地图的全部内容就已算是记录终了了。



3、总结



写到这里关于我们项目中的LBS服务就已全部介绍终了了,在做地图模块的同时学到了许多新的东西,在这里也要感谢鸿洋大神对我的帮助,目前准备和同事1起再做1个以LBS为主的APP,等做好以后可能还会写1个博客吧,这个APP的点子不错,目前暂且先不透漏,透漏了也没几个人会看到,哈哈。路漫漫其修远兮,吾将上下而求索,我的Android之路才刚刚开始,今后还应当更加努力!加油!Raito!

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐