Android 高仿百度地图的LBS服务——离线地图篇 Part 1 (v 3.1.1)

版权声明:本文为博主原创文章,转载请标明出处(http://blog.csdn.net/wlwlwlwl015)Thanks. https://blog.csdn.net/wlwlwlwl015/article/details/41247455



一、前言



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

猜你喜欢

转载自blog.csdn.net/wlwlwlwl015/article/details/41247455