一、前言
转载请标明出处: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分,大家都回去了,一个人安安静静的在公司写程序感觉也蛮好的。我相信,只要肯坚持、肯努力,终有一天我也会成为一名很棒的程序员,加油小灯!