一、前言
转载请标明出处:http://blog.csdn.net/wlwlwlwl015/article/details/41247455
写这一篇博客的时候,我的心情是非常焦躁的。想给项目中的地图模块加上离线Map的功能,之前鸿洋大神写过一篇离线地图的blog,就模仿那个代码写了一个Demo,在写的过程中我发现很多地方还是不理解,觉着看着人家写好的东西去实现还搞不懂,那我还做什么安卓,转念一想,毕竟我正式自学安卓也就2个多月吧,有一天没一天的看看书,现在让我搞地图开发可能是有些吃力,我很多基本的东西还没有学,但是,我不能这样为自己找借口!没有行或者不行,只有努力或者不努力!再复杂的东西也可以拆分开来,一一攻破,所以我让自己冷静下来,开始写这篇博客,从初学者的角度去理解需求,实现功能,一遍不行就两遍,两遍不行就N遍,我承认我不聪明,技术也不咋地,但是我相信可以用努力去弥补一切!
二、百度离线地图的实现过程分析
当不知道一个东西怎么做的时候,我会选择去模仿一个成熟的产品,在模仿的过程中分析它的优点和缺点,并站在用户角度去感受,之后再分析功能,设计界面,一步一步的去实现。在前两篇blog中我一直是模仿百度地图去做的,所以本篇用到的离线地图自然也是模仿百度地图的离线地图去做了。百度地图离线功能的界面中,整体上看可以分为“下载管理”和“城市列表”两部分,首先我们看一下“下载管理”部分:
根据图中的标注来确定一下这一部分我们需要做的事情:
1.显示所有已下载的城市地图。
2.若有某城市离线地图有更新包的话,需要显示提示信息。
3.有更新包和没有更新包的城市需要区分开。
4.可以点击按钮来管理已下载的具体某城市的离线地图。
5.可以下载更新包进行某城市的离线地图更新。
6.可以按城市删除已下载的离线地图。
再看一下城市列表部分:
根据上图中的标注我们来确定一下“城市列表”部分需要做的事情:
1.需要显示当前城市、热门城市以及全国的离线地图信息,并标明是否下载。
2.在全国的部分中,列表显示条目以省为单位,点击某省可以展开其所有城市的离线地图信息,并标明是否下载。
最后,根据我们项目的实际需求,“下载管理”中的“查看地图”功能暂且不需要,更新包下载酌情保留,全国的地图酌情保留,其余的应当都要实现。这里的“酌情”保留主要是两部分原因:
1.官方是否直接提供接口。
2.开发成本是否过较大。
对于第一点确实没有办法,百度提供了什么我们才能用什么。对于第二点这里主要指的是人力成本,毕竟我只算个安卓新手,我们的项目也只是集成了较为简单的地图服务,也就是到上篇博客为止的路线规划(模仿百度地图的LBS服务——路线规划篇),但这里我既然做了离线地图这块,必定会根据实际需求和我的个人能力去权衡,尽量把它做好做到位,也算是对我的一次锻炼。
三、界面布局实现
下面我就开始逐步实现需求了,首先是界面布局的搭建。简单来看,“下载管理”和“城市列表”分别各自放了一个ListView,并且可以通过上面的按钮实现来回切换的效果,百度地图上应该是通过ViewPager+Fragment去实现的,但以我目前掌握的知识,我打算通过控制布局的隐藏和显示来实现这个效果,暂且不做滑动切换了。这个很简单也没什么说的,就是通过控制ToggleButton去设置View.VISIBLE或View.GONE,看一下效果:
下面需要考虑的就是从哪开始做了,当用户第一次装APP的时候,“下载管理”肯定是空的,而城市列表则应当显示出“当前城市”、“热门城市”以及“全国”的离线地图信息来提供用户选择下载,具体的Item项应包括城市名、离线包的大小以及下载状态。所以接下来我就根据百度SDK提供的离线地图API先去实现“城市列表”这一功能。
四、“城市列表”模块的分析与实现
首先来分析一下需要做的事情:
界面上整体分为3部分,分别是:当前城市、热门城市、全国。当前城市只有一项,而“热门城市”和“全国”应当都是以ListView的形式来展示数据,并且每一个item项后面都有一个下载图标,点击下载就会开启一个线程去执行下载任务,当下载完成时,城市列表中的那个已下载的item项就无法再点击下载,点击后该项会出现在“下载管理”中。将这一过程拆分开来看,首先要做的应当是显示城市列表了,每一个列表项包括城市名和数据包的大小。下面开始贴代码,毋庸置疑首先需要做的是初始化,包括View初始化和OfflineMap的初始化:
// 初始化 private void initView() { // 初始化View llayout1 = (LinearLayout) findViewById(R.id.llayout_download_manage); llayout2 = (LinearLayout) findViewById(R.id.llayout_city_list); tb = (ToggleButton) findViewById(R.id.tb_check_mode); tb.setChecked(false); tb.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { // TODO Auto-generated method stub if (isChecked) { llayout1.setVisibility(View.GONE); llayout2.setVisibility(View.VISIBLE); } else { llayout1.setVisibility(View.VISIBLE); llayout2.setVisibility(View.GONE); } } }); // 初始化离线地图 mOfflineMap = new MKOfflineMap(); mOfflineMap.init(new MKOfflineMapListener() { @Override public void onGetOfflineMapState(int type, int state) { // TODO Auto-generated method stub } }); }
这里需要注意一点,实例化MKOfflineMap对象之后必须进行初始化(调用init(MKOfflineMapListener)方法)之后才能调用MKOfflineMap对象的实例方法,否则会报错,尽管MKOfflineMapListener的回调方法中可以不写任何代码,但是这个初始化的过程是必不可少的。初始化工作完成后,下面需要做的就是初始化数据,并将数据通过ListView展示。首先是初始化数据的代码:
// 初始化数据 private void initData() { //获取到热门城市列表 ArrayList<MKOLSearchRecord> hotCityList = mOfflineMap.getHotCityList(); for (MKOLSearchRecord cityRecord : hotCityList) { //自定义类用于封装离线地图的相关信息 OfflineMapCityBean bean = new OfflineMapCityBean(); bean.setCityId(cityRecord.cityID); //城市ID bean.setCityName(cityRecord.cityName); //城市名称 bean.setMapDataPakSize(cityRecord.size); //数据包大小(单位是字节) mHotCityDatas.add(bean); } }
自定义对象很简单,无非就是封装一些需要用到的属性,我这里暂时只放了3个属性:城市ID、城市名、数据包大小。
现在有了数据,那么就可以去构建ListView了,下面是ListView的自定义适配器:
// 热门城市列表的适配器 class HotCityAdapter extends BaseAdapter { @Override public int getCount() { // TODO Auto-generated method stub return mHotCityDatas.size(); } @Override public Object getItem(int position) { // TODO Auto-generated method stub return mHotCityDatas.get(position); } @Override public long getItemId(int position) { // TODO Auto-generated method stub return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { // TODO Auto-generated method stub OfflineMapCityBean bean = mHotCityDatas.get(position); ViewHolder holder = null; if (convertView == null) { convertView = mInflater .inflate(R.layout.offline_map_item, null); holder = new ViewHolder(); holder.cityName = (TextView) convertView .findViewById(R.id.id_city_name); holder.dataPakSize = (TextView) convertView .findViewById(R.id.id_data_pak_size); holder.downloadIbtn = (ImageButton) convertView .findViewById(R.id.id_down_load_ibtn); holder.downloadState = (TextView) convertView .findViewById(R.id.id_down_load_state); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.cityName.setText(bean.getCityName()); holder.dataPakSize.setText(NumberFormatUtil.dataSizeFormatter(bean .getMapDataPakSize()) + "M"); return convertView; } private class ViewHolder { TextView cityName; TextView dataPakSize; ImageButton downloadIbtn; TextView downloadState; } }
这里使用ViewHolder优化了ListView的加载效率,使得findViewByXxx只执行一次。由于MKOLSearchRecord的size属性返回的数据包的大小单位是字节,所有需要通过NumberFormatUtil这个自定义工具类转换成兆字节即可,最后给ListView设置适配器即可显示HotCities列表了:
// 初始化ListView private void initListView() { listView2 = (ListView) findViewById(R.id.lv_listview_2); mHotCityAdapter = new HotCityAdapter(); listView2.setAdapter(mHotCityAdapter); }
下面我们看一下运行后效果:
做完了“热门城市”的列表之后,我们还差“全国城市”和“我的城市”的列表,模仿百度离线地图的话,应该选用ExpandableListView来实现省市的部分,而“热门城市”依然使用一个ListView,整体放在一个ScrollView中,"当前城市"用一个TextView即可。
关于ScrollView嵌套ListView以及子类的问题也值得一提,作为新手我自然也栽了跟头,下面是我在做的过程中遇到的两个问题:
Question One
这个错误很好解决,因为提示很明显,ScrollView只能维护一个子元素。所以我们应当把所有的控件都放进一个布局中即可,比如LinearLayout。
Question Two
当ListView或ExpandableListView被嵌套进ScrollView时,只能显示一行Item。这也是一个老问题了,遇到的朋友肯定很清楚,没遇到的朋友写一下试试就算遇到了。查一下资料就可以发现解决方案有很多种,造成问题的主要原因就是ListView的高度无法正常设定的问题。我选择的解决方案是自定义可适应ScrollView的ListView,通过重写onMeasure方法,达到对ScrollView适配的效果。由于ListView和ExpandableListView很相似,所以我这里只贴上自定义的ListView的相关代码:
package com.xw.baidumkofflinemapdemo; import android.content.Context; import android.util.AttributeSet; import android.widget.ListView; public class HotCitiesListView extends ListView { public HotCitiesListView(Context context) { super(context); // TODO Auto-generated constructor stub } public HotCitiesListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // TODO Auto-generated constructor stub } public HotCitiesListView(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, expandSpec); } }
很简单吧,声明构造方法并重写onMeasure方法即可。由于上面没有给任何布局代码,所以这里一次性贴出全部的布局文件代码:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ToggleButton android:id="@+id/tb_check_mode" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginBottom="10dp" android:layout_marginTop="10dp" android:background="@drawable/selector_is_download" android:textOff="" android:textOn="" /> <!-- 下载管理 --> <LinearLayout android:id="@+id/llayout_download_manage" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="vertical" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="#CCCCCC" android:paddingLeft="15dp" android:text="下载完成" android:textColor="#FFFFFF" /> <ListView android:id="@+id/lv_listview_1" android:layout_width="fill_parent" android:layout_height="fill_parent" > </ListView> </LinearLayout> <!-- 城市列表 --> <LinearLayout android:id="@+id/llayout_city_list" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="vertical" android:visibility="gone" > <ScrollView android:id="@+id/id_scroll_sv" android:layout_width="match_parent" android:layout_height="match_parent" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="#CCCCCC" android:paddingLeft="15dp" android:text="当前城市" android:textColor="#FFFFFF" /> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <TextView android:id="@+id/id_city_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="8dp" android:text="西安市" android:textColor="#ff000000" android:textSize="18sp" /> <TextView android:id="@+id/id_down_load_state" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="3dp" android:layout_toRightOf="@id/id_city_name" android:text="(正在下载)" android:textColor="#0066FF" android:textSize="15sp" /> <ImageButton android:id="@+id/id_down_load_ibtn" android:layout_width="58px" android:layout_height="58px" android:layout_alignParentRight="true" android:layout_marginLeft="8dp" android:src="@drawable/ibtn_down_load_black" /> <TextView android:id="@+id/id_data_pak_size" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toLeftOf="@id/id_down_load_ibtn" android:text="20.7M" android:textColor="#ff000000" android:textSize="18sp" /> </RelativeLayout> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="#CCCCCC" android:paddingLeft="15dp" android:text="热门城市" android:textColor="#FFFFFF" /> <!-- 热门城市的ListView --> <com.xw.baidumkofflinemapdemo.HotCitiesListView android:id="@+id/id_hotcities_lv" android:layout_width="match_parent" android:layout_height="wrap_content" > </com.xw.baidumkofflinemapdemo.HotCitiesListView> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="#CCCCCC" android:paddingLeft="15dp" android:text="全国" android:textColor="#FFFFFF" /> <!-- 全国省市的ListView --> <com.xw.baidumkofflinemapdemo.NationalCitiesListView android:id="@+id/id_allcities_exp_lv" android:layout_width="match_parent" android:layout_height="wrap_content" > </com.xw.baidumkofflinemapdemo.NationalCitiesListView> </LinearLayout> </ScrollView> </LinearLayout> </LinearLayout>
对了,最后不要忘记一点,在Activity或Fragment中初始化ScrollView之后需要手动把ScrollView滚动至最顶端:
sv = (ScrollView) findViewById(R.id.id_scroll_sv); sv.smoothScrollTo(0, 0);
好了,现在ScrollView嵌套ListView就没有问题了,下面就是数据的问题,同热门城市的数据类似,百度SDK也提供了返回所有支持离线地图的省市接口(getOfflineCityList())下面就贴上初始化全国数据的代码:
// 获取全国城市列表 ArrayList<MKOLSearchRecord> allCityList = mOfflineMap .getOfflineCityList(); if (allCityList != null) { for (MKOLSearchRecord cityRecord : allCityList) { OfflineMapItemBean bean = new OfflineMapItemBean(); // 如果是省 if (cityRecord.cityType == 1) { bean.setCityId(cityRecord.cityID); bean.setCityName(cityRecord.cityName); ArrayList<MKOLSearchRecord> childcities = cityRecord.childCities; List<OfflineMapItemBean> cities = new ArrayList<OfflineMapItemBean>(); for (MKOLSearchRecord city : childcities) { OfflineMapItemBean bean2 = new OfflineMapItemBean(); bean2.setCityId(city.cityID); bean2.setCityName(city.cityName); bean2.setMapDataPakSize(city.size); cities.add(bean2); } bean.setChildCities(cities); allCityDatas.add(bean); } } }
通过MKOLSearchRecord的cityType属性可以得知当前城市的城市类型:
0-->全国
1-->省
2-->市
我这里只获取了所有省的数据,具体可以根据自己的需求变更。细心的朋友可以发现我在OfflineMapItemBean这个实体中又加了一个属性(本来叫OfflineMapCityBean,后来rename了)childCities,我之前也说了会根据需求继续添加字段,这也就是自定义一个实体Bean去封装地图Item数据的好处,我这里正是需要用一个List来保存一个省的所有城市的信息。这样的话,如果是省,它的childCities属性不为NULL,如果是市,那么它的childCities属性必然为NULL,这也和官方文档中对cityType的解释相对应:如果是省份,可以通过childCities得到子城市列表。
有了布局,有了数据,那么最后一步依然是准备适配器了。ListView还好说,ExpandableListView的适配器还是有那么一点麻烦的,下面就贴上它的适配器代码:
// 全国省市的适配器 class NationalCityAdapter extends BaseExpandableListAdapter { @Override public int getGroupCount() { // TODO Auto-generated method stub return OfflineActitivty.this.allCityDatas.size(); } @Override public int getChildrenCount(int groupPosition) { // TODO Auto-generated method stub return OfflineActitivty.this.allCityDatas.get(groupPosition) .getChildCities().size(); } @Override public Object getGroup(int groupPosition) { // TODO Auto-generated method stub return OfflineActitivty.this.allCityDatas.get(groupPosition); } @Override public Object getChild(int groupPosition, int childPosition) { // TODO Auto-generated method stub return OfflineActitivty.this.allCityDatas.get(groupPosition) .getChildCities().get(childPosition); } @Override public long getGroupId(int groupPosition) { // TODO Auto-generated method stub return groupPosition; } @Override public long getChildId(int groupPosition, int childPosition) { // TODO Auto-generated method stub return childPosition; } @Override public boolean hasStableIds() { // TODO Auto-generated method stub return true; } @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { // TODO Auto-generated method stub String data = allCityDatas.get(groupPosition).getCityName(); ViewHolder holder = null; if (convertView == null) { convertView = mInflater.inflate( android.R.layout.simple_list_item_1, null); holder = new ViewHolder(); holder.provenceName = (TextView) convertView .findViewById(android.R.id.text1); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.provenceName.setText(data); return convertView; } @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { // TODO Auto-generated method stub String data = allCityDatas.get(groupPosition).getChildCities().get(childPosition).getCityName(); int data_pakSize = allCityDatas.get(groupPosition).getChildCities() .get(childPosition).getMapDataPakSize(); String new_data_pakSize = NumberFormatUtil .dataSizeFormatter(data_pakSize) + "M"; ViewHolder holder = null; if (convertView == null) { convertView = mInflater.inflate(R.layout.offline_map_item, null); holder = new ViewHolder(); holder.cityName = (TextView) convertView .findViewById(R.id.id_city_name); holder.dataPakSize = (TextView) convertView .findViewById(R.id.id_data_pak_size); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.cityName.setText(data); holder.dataPakSize.setText(new_data_pakSize); return convertView; } @Override public boolean isChildSelectable(int groupPosition, int childPosition) { // TODO Auto-generated method stub return true; } private class ViewHolder { TextView provenceName; TextView cityName; TextView dataPakSize; ImageButton downloadIbtn; TextView downloadState; } }
好了,基本上所有代码都贴完了,最后看一下运行效果:
OK第一阶段差不多完工了,打开手机中的百度地图—>离线地图—>城市列表,看看模仿的相似度还凑活吧,就是界面略丑了。。
有了“城市列表”的所有数据,那么接下来就剩下如何去下载了,包括添加任务到“下载管理”、启动/暂停下载、离线地图的删除、离线地图的更新以及离线地图的实时下载进度查看等等。由于篇幅过长,所以剩下的部分放到下一篇blog了。
五、总结
写到这里关于离线地图的第一部分就算结束了,在这这篇blog的过程中我学到了很多东西,包括以下内容:
1.ScrollView嵌套ListView如何处理。
2.ExpandableListView及其适配器的使用。
3.通过ViewHolder优化自定义适配器。
4.百度地图SDK的省市数据分级处理。
blog开头写到我的心情很焦躁,但是现在我的心情很平静,也很开心,模仿的同时学到了这么多新的东西,作为一个安卓新手来说确实是一件值得高兴的事情,同时也坚定了我学习安卓的兴趣和决心。现在是周六下午4点10分,大家都回去了,一个人安安静静的在公司写程序感觉也蛮好的。我相信,只要肯坚持、肯努力,终有一天我也会成为一名很棒的程序员,加油小灯!