Android 特色开发,基于位置的服务

        现在你已经学会了非常多的 Android 技能,并且通过这些技能你完全可以编写出相当不错的应用程序了。不过从现在开始,我们将要学习一些全新的 Android 技术,这些技术有别于传统的 PC 或 Web 领域的应用技术,是只有在移动设备上才能实现的。

        说到只有在移动设备上才能实现的技术,很容易就让人联想到基于位置的服务(Location Based Service)。由于移动设备相比于电脑可以随身携带,我们通过地理定位的技术就可以随时得知自己所在的位置,从而围绕这一点开发出很多有意思的应用。我们就讲针对这一点展开进行讨论,学习一下基于位置的服务究竟是如何实现的。

1.  基于位置的服务简介

        基于位置的服务简称 LBS,这个技术随着移动互联网的兴起,在最近的几年里十分火爆。其实它本身并不是什么时髦的技术,主要的工作原理就是利用无线电通讯网络 GPS 等定位方式来确定出移动设备所在的位置,而这种定位技术早在很多年前就已经出现了。

        那为什么 LBS 技术直到最近几年才开始流行呢?这主要是因为,在过去移动设备的功能极其有限,即使定位到了设备所在的位置,也就仅仅只是定位到了而已,我们并不能在位置的基础上进行一些其他的操作。而现在就大大不同了,有了 Android 系统作为载体,我们可以利用定位出的位置进行许多丰富多彩的操作。比如说天气预报程序可以根据用户所在的位置自动选择城市,发微博的时候我们可以向朋友们晒一下自己在哪里,不认识路的时候随时打开地图就可以查询路线,等等等等。

2.  找到自己的位置

        归根结底,其实基于位置的服务所围绕的核心就是要确定出自己所在的位置,这在 Android 中并不困难,主要借助LocatinManager这个类就可以实现了。下面我们首先学习一下 LocationManager 的基本用法,然后再通过一个例子来尝试获取一下自己当前的位置。

        另外需要注意,本章中所写的代码建议你都在手机上运行,DDMS 虽然也提供了在模拟器中模拟地理位置的功能,但在手机上得到真实的位置数据,你的感受会更加深刻。

2.1  LocationManager 的基本用法

        毫无疑问,要想使用 LocationManager 就必须要先获取到它的实例,我们可以调用 Context 的 getSystemService() 方法获取到。getSystemService() 方法接收一个字符串参数用于确定获取系统的哪个服务,这里传入 Context.LOCATION_SERVICE 即可。因此,获取 LocationManager 的实例就可以写成:

LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

        接着我们需要选择一个位置提供器来确定设备当前的位置Android 中一般有三种位置提供器可供选择,GPS_PROVIDERNETWORK_PROVIDERPASSIVE_PROVIDER。其中前两种使用的比较多,分别表示使用 GPS 定位和使用网络定位。这两种定位方式各有特点,GPS 定位的精准度比较高,但是非常耗电,而网络定位的精准度稍差,但耗电量比较少。我们应该根据自己的实际情况来选择使用哪一种位置提供器,当位置精度要求非常高的时候,最好使用 GPS_PROVIDER,而一般情况下,使用 NETWORK_PROVIDER 会更加划算

        需要注意的是,定位功能必须要由用户主动去启用才行,不然任何应用程序都无法获取到手机当前的位置信息。进入手机的设置=》定位服务,其中第一个选项表示允许使用网络的方式来对手机进行定位,第二个选项表示允许使用 GPS 的方式来对手机进行定位,如图 11.1 所示。

图 11.1

        你并不需要担心一旦启用了这几个选项后,手机的电量就会直线下滑,这些选项只是表明你已经同意让应用程序来对你的手机进行定位了,但只有当定位操作真正开始的时候才会影响到手机的电量。下面我们就来看一看,如何才能真正地开始定位操作。

        将选择好的位置提供器传入到 getLastKnownLocation() 方法中,就可以得到一个 Location 对象,如下所示:

String provider = LocationManager.NETWORK_PROVIDER;
Location location = locationManager.getLastKnownLocation(provider);

        这个 Location 对象中包含了经度、纬度、海拔等一系列的位置信息,然后从中取出我们所关心那部分数据即可。

        如果有些时候你想让定位的精度尽量高一些,但又不确定 GPS 定位的功能是否已经启用,这个时候就可以先判断一下有哪些位置提供器可用,如下所示:

List<String> providerList = locationManager.getProviders(true);

        可以看到,getProviders() 方法接收一个布尔型参数,传入 true 就表示只有启用的位置提供器才会被返回。之后再从 providerList 中判断是否包含 GPS 定位的功能就行了。

        另外,调用 getLastKnownLocation() 方法虽然可以获取到设备当前的位置信息,但是用户是完全有可能带着设备随时移动的,那么我们怎样才能在设备位置发生改变的时候获取到最新的位置信息呢?不用担心,LocatinManager 还提供了一个 requestLocationUpdates() 方法,只要传入一个 LocationListener 的实例,并简单配置几个参数就可以实现上述功能了,写法如下:

locationManager.requestLocationUpdates(provider, 5000, 10,
	new LocationListener() {
	@Override
	public void onStatusChanged(String provider, int status, Bundle extras) {
	}

	@Override
	public void onProviderEnabled(String provider) {
	}

	@Override
	public void onProviderDisabled(String provider) {
	}

	@Override
	public void onLocationChanged(Location location) {
	}
});

        这里 requestLocationUpdates() 方法接收四个参数,第一个参数是位置提供器的类型第二个参数是监听位置变化的时间间隔,以毫秒为单位第三个参数是监听位置变化的距离间隔,以米为单位第四个单位则是 LocationListener 监听器。这样的话,LocationManager每隔 5 秒钟会检测一下位置的变化情况,当移动距离超过 10 米的时候,就会调用 LocationListener 的 onLocationChanged() 方法,并把新的位置信息作为参数传入。

        好了,关于 LocationManager 的用法基本就是这么多,下面我们就通过一个例子来尝试一下吧。

2.2  确定自己位置的经纬度

        通过上一小节的学习,你会发现 LocationManager 的用法并不复杂,那么本小节中我们来编写一个可以获取当前位置经纬度的程序吧。

        新建一个 LocationTest 项目,修改 activity_main.xml 中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/position_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

        布局文件中的内容实在是太简单了,只有一个 TextView 控件,用于稍后显示设备位置的经纬度信息。

        然后修改 MainActivity 中的代码,如下所示:

public class MainActivity extends Activity {

	private TextView positionTextView;

	private LocationManager locationManager;

	private String provider;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		positionTextView = (TextView) findViewById(R.id.position_text_view);
		locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
		// 获取所有可用的位置提供器
		List<String> providerList = locationManager.getProviders(true);
		if (providerList.contains(LocationManager.GPS_PROVIDER)) {
			provider = LocationManager.GPS_PROVIDER;
		} else if (providerList.contains(LocationManager.NETWORK_PROVIDER)) {
			provider = LocationManager.NETWORK_PROVIDER;
		} else {
			// 当没有可用的位置提供器时,弹出Toast提示用户
			Toast.makeText(this, "No location provider to use",
					Toast.LENGTH_SHORT).show();
			return;
		}
		Location location = locationManager.getLastKnownLocation(provider);
		if (location != null) {
			// 显示当前设备的位置信息
			showLocation(location);
		}
		locationManager.requestLocationUpdates(provider, 5000, 1,
				locationListener);
	}

	protected void onDestroy() {
		super.onDestroy();
		if (locationManager != null) {
			// 关闭程序时将监听器移除
			locationManager.removeUpdates(locationListener);
		}
	}

	LocationListener locationListener = new LocationListener() {

		@Override
		public void onStatusChanged(String provider, int status, Bundle extras) {
		}

		@Override
		public void onProviderEnabled(String provider) {
		}

		@Override
		public void onProviderDisabled(String provider) {
		}

		@Override
		public void onLocationChanged(Location location) {
			// 更新当前设备的位置信息
			showLocation(location);
		}
	};

	private void showLocation(final Location location) {
		String currentPosition = "latitude is " + location.getLatitude() + "\n"
				+ "longitude is " + location.getLongitude();
		positionTextView.setText(currentPosition);
	}
}

        这里并没有什么复杂的逻辑,基本全是我们在上一小节中学到的知识。在 onCreate() 方法中首先是获取到了 LocationManager 的实例,然后调用 getProviders() 方法去得到所有可用的位置提供器,接下来再调用 getLastKnownLocation() 方法就可以获取到记录当前位置信息的 Location 对象了,这里我们将 Location 对象传入到 showLocation() 方法中,经度和纬度的值就会显示到 TextView 上了。然后为了要能监测到位置信息的变化,下面又调用了 requestLocationUpdates() 方法来添加一个位置监听器,设置时间间隔是 5 秒,距离是 1 米,并在 onLocationChanged() 方法中实时更新 TextView 上显示的经纬度信息。最后当程序关闭时,我们还需要调用 removeUpdates() 方法来将位置监听器移除,以保证不会继续耗费手机的电量

        另外,获取设备当前的位置信息也是要声明权限的,因此还需要修改 AndroidManifest.xml 中的代码,如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.locationtest"
    android:versionCode="1"
    android:versionName="1.0" >
    ......
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    ......
</manifest>

        现在运行一下程序,就可以看到手机当前位置的经纬度信息了,如图 11.2 所示。

Android 特色开发,基于位置的服务_第1张图片

图 11.2

        之后如果你拿着手机随处移动,就可以看到界面上的经纬度信息是会变化的(机型:Mx4Pro 没有看到变化的效果,且开启 GPS 后定位不到~,只能用网络定位)。由此证实,我们的程序确实已经在正常工作了。

3.  反向地理编码,看得懂的位置信息

        话说回来,刚才我们虽然成功获取到了设备当前位置的经纬度信息,但遗憾的是,这种经纬度一般人是根本看不懂的,相信谁也无法立刻答出南纬 25 都、东经 148 度是什么地方吧?为了能够更加直观地阅读位置信息,本节中我们就来学习一下,如何通过反向地理编码,将经纬值转换成看得懂的位置信息

3.1  Geocoding API 的用法

        其实 Android 本身就提供了地理编码的 API,主要是使用GeoCoder这个类来实现的。它可以非常简单地完成正向和反向的地理编码功能,从而轻松地将一个经纬值转换成看得懂的位置信息。

        不过,非常遗憾的是,GeoCoder 长期存在一些较为严重的 bug,在反向地理编码的时候会有一定的概率不能解析出位置的信息,这样就无法保证位置解析的稳定性,因此我们不得不去寻找 GeoCoder 的替代方案。

        还算比较幸运,谷歌又提供了一套 Geocoding API,使用它的话一可以完成反向地理编码的工作,只不过它的用法稍微复杂了一些,但稳定性要比 GeoCoder 强得多。本小节中我们只是学习一下 Geocoding API 的简单用法,更详细的用法请参考官方文档:https://developers.google.com/maps/documentation/geocoding/。

        Geocoding API 的工作原理并不神秘,其实就是利用了我们前面学习的 HTTP 协议。在手机端我们可以向谷歌的服务器发起一条 HTTP 请求,并将经纬度的值作为参数一同传递过去,然后服务器会帮我们将这个经纬值转换成看得懂的位置信息,再将这些信息返回给手机端,最后手机端去解析服务器返回的信息,并进行处理就可以了。

        Geocoding API 中规定了很多借口,其中反向地理编码的接口如下:

http://maps.googleapis.com/maps/api/geocode/json?latlng=40.714224,-73.961452&sensor=true_or_false

        我们来仔细看下这个接口的定义,其中 http://maps.googleapis.com/maps/api/geocode/ 是固定的,表示接口的连接地址。json 表示希望服务器能够返回 JSON 格式的数据,这里也可以指定成 xml。latlng=40.714224,-73.961452 表示传递给服务器去解码的经纬值是北纬 40.714224,西经 73.96145 度。sensor=true_or_false 表示这条请求是否来自于某个设备的位置传感器,通常指定成 false 即可

        如果发送 http://maps.googleapis.com/maps/api/geocode/json?latlng=40.714224,-73.961452&sensor=false 这样一条请求给服务器,我们将会得到一段非常长的 JSON 格式的数据,其中会包括如下部分内容:

"formatted_address" : "277 Bedford Avenue, 布鲁克林纽约州 11211美国"

        从这段内容中我们就可以看出北纬 40.714224 度,西经 73.96145 度对应的地理位置是在哪里了。如果你想查看服务器返回的完整数据,在浏览器中访问上面的网址即可。

        这样的话,使用 Geocoding API 进行反向地址编码的工作原理你就已经搞清楚了,那么难点其实就在于如何从服务器返回的数据中解析出我们想要的那部分信息了。而 JSON 格式数据的解析方式我们早在上一章中就牢牢地掌握了,因此我相信这个问题一定是难不倒你的。下面我们就来完善一下 LocationTest 这个程序,给它加入反向地理编码的功能吧。

3.2  对经纬度进行解析

        使用 Geocoding API 进行反向地理编码的流程相信你已经很清楚了,我们先要发送一个 HTTP 请求给谷歌的服务器,然后再对返回的 JSON 数据进行解析。发送 HTTP 请求的方式我们准备使用 HttpClient,解析 JSON 数据的方式使用 JSONObject。修改 MainActivity 中的代码,如下所示:

public class MainActivity extends Activity {

	public static final int SHOW_LOCATION = 0;
	......
	private void showLocation(final Location location) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					// 组装反向地理编码的接口地址
					StringBuilder url = new StringBuilder();
					url.append("http://maps.googleapis.com/maps/api/geocode/json?latlng=");
					url.append(location.getLatitude()).append(",")
							.append(location.getLongitude());
					url.append("&sensor=false");
					HttpClient httpClient = new DefaultHttpClient();
					HttpGet httpGet = new HttpGet(url.toString());
					// 在请求消息头中指定语言,保证服务器会返回中文数据
					httpGet.addHeader("Accept-Language", "zh-CN");
					HttpResponse httpResponse = httpClient.execute(httpGet);
					if (httpResponse.getStatusLine().getStatusCode() == 200) {
						HttpEntity entity = httpResponse.getEntity();
						String response = EntityUtils.toString(entity, "utf-8");
						JSONObject jsonObject = new JSONObject(response);
						// 获取results节点下的位置信息
						JSONArray resultArray = jsonObject.getJSONArray("results");
						if (resultArray.length() > 0) {
							JSONObject subObject = resultArray.getJSONObject(0);
							// 取出格式化后的位置信息
							String address = subObject.getString("formatted_address");
							Message message = new Message();
							message.what = SHOW_LOCATION;
							message.obj = address;
							handler.sendMessage(message);
						}
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}).start();
	}

	private Handler handler = new Handler() {

		public void handleMessage(Message msg) {
			switch (msg.what) {
			case SHOW_LOCATION:
 				String currentPosition = (String) msg.obj;
				positionTextView.setText(currentPosition);
				break;
			default:
				break;
			}
		}

	};

}

        观察 showLocation() 方法,由于我们要在这里发起网络请求,因此必须开启一个子线程。在子线程中首先是通过 StringBuilder 组装了一个反向地理编码接口地址的字符串,然后使用 HttpClient 去请求这个地址就好了。注意在 HttpGet 中我们还添加了一个消息头,消息头中将语言类型指定为简体中文,不然服务器会默认返回英文的位置信息。

        接下来就是对服务器返回的 JSON 数据进行解析了。由于一个经纬度的值又可能包含了好几条街道,因此服务器通常会返回一组位置信息,这些信息都是存放在 results 结点下的。在得到了这些位置信息后只需要取其中的第一条就可以了,通常这也是最接近我们位置的那一条。之后就可以从 formatted_address 结点中取出格式化后的位置信息了,这种位置信息你就完全可以看得懂了。

        不过别忘了,目前我们还是在子线程当中的,因此在这里无法直接将得到的位置信息显示到 TextView 上。但这个问题也一定难不倒你了,使用异步消息处理机制就可以轻松解决。

        由于这里我们使用到了网络功能,因此还需要在 AndroidManifest.xml 中添加权限声明,如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.locationtest"
    android:versionCode="1"
    android:versionName="1.0" >
    ......
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<pre name="code" class="html">    <uses-permission android:name="android.permission.INTERNET" />
......
</manifest>
 
 

       在这个例子中我们只是对服务器返回的 JSON 数据进行了最简单的解析,位置信息是作为整体取出的,其实你还可以进行更精确的解析,将国家名、城市名、街道名、甚至邮政编码等作为独立的信息取出,更加有趣的功能就等着你自己去进行研究了。

4.  使用百度地图

        现在手机地图的应用真的可以算得上是非常广泛了,和 PC 上的地图相比,手机地图能够随时随地进行查看,并且轻松构建出行路线,使用起来明显更加方便。但是你有没有相关,其实我们在自己的应用程序里也是可以加入地图功能的。

        在手机地图领域做得最好的就当数谷歌地图和百度地图了,并且这两种地图都提供了丰富的 API,使得任何开发者都可以轻松地讲地图功能引入到自己的应用程序当中。只不过谷歌地图在 2013 年 3 月的时候全面停用了第一版的 API Key,而第二版的 API Key 在中国使用的时候又有诸多限制,因此这里我们就不准备使用谷歌地图了。相比之下,百度地图的使用就没有任何限制,而且用法也非常方便,那么它自然就称为我们本节的主题了。

4.1  申请 API Key

        要想在自己的应用程序里加入百度地图的功能,首先必须申请一个 API Key。你得拥有一个百度账号才能进行申请,我相信大多数人早就已经拥有了吧?如果你还没有的话,赶快去注册一个吧。

        下面我们需要注册成为一名百度开发者。登录你的百度账号,并打开 http://developer.baidu.com/user/reg 这个网址,在这里填写一些注册信息即可,如图 11.4 所示。

Android 特色开发,基于位置的服务_第2张图片

图 11.4

        只需要填写“*”号的那部分内容就足够了,接下来点击提交。验证邮箱后你就已经成为了一名百度开发者了。接着点击去创建应用,然后选择我的应用,会看到如图 11.6 所示的界面。

Android 特色开发,基于位置的服务_第3张图片

图 11.6

        这里会显示所有你申请过的 API Key,由于这是一个刚刚注册的账号,所以目前只有一个系统默认的 API Key。接下来点击创建应用就可以去申请新的 API Key 了,应用名称可以随便填,应用类型选择 Android SDK,如图 11.7 所示。

Android 特色开发,基于位置的服务_第4张图片

图 11.7

        这里的数字签名指的是我们打包程序时所用 keystore 的 SHA1 指纹,可以在 Eclipse 中查看到。点击 Eclipse 导航栏的 【Windows】=》【Preferences】=》【Android】=》【Build】,界面如图 11.8 所示。

图 11.8

        其中,C1:55:1C:6E:7B:57:63:23:D5:02:0A:40:6A:30:85:6D:38:FC:EC:CA 就是我们所需的 SHA1 指纹了,当然你的 Eclipse 中显示的指纹肯定和我是不一样的。另外需要注意,目前我们使用的是debug.keystore 所生成的指纹,这是 Android 自动生成的一个用于测试的 keystore。而当你的应用程序发布时还需要创建一个正式的 keystore,如果要得到它的指纹,就需要在 cmd 中输入如下命令:

keytool -list -v -keystore <keystore文件名>

        然后输入正确的密码就可以了。创建 keystore 的方法我们将在后面学习。

        那么数字签名的值已经得到,虽然目前我们的应用程序还不存在,但可以先将包名预定下来,比如就交 com.example.baidumaptest。我们将这两个值填入到图 11.7 的输入框中,然后点击确定。这样的话就已经申请成功了,如图 11.9 所示。

图 11.9

        其中,c1HhKtG7hEz4629i2XVegPDOmEETGucu 就是申请到的 API Key,有了它就可以进行后续的地图开发工作了,那么我们马上开始吧。

4.2  让地图显示出来

        现在正是趁热打铁的好时机,新建一个 BaiduMapTest 项目,并将包命名为 com.example.baidumaptest。在开始编码之前,我们还需要先将百度地图 Android 版的 SDK 准备好,下载地址是:http://developer.baidu.com/map/sdkandev-download.htm,然后点击全部下载按钮就可以了。

        下载完成后对压缩包解压,应该可以看到其中有三个压缩包(最新的只剩一个 libs 文件夹了)。其中 Docs 包中含有百度地图的使用文档,Sample 包中含有一个使用百度地图的工程样例,Lib 包中含有使用百度地图所必须依赖的库文件。解压 Lib 包,这里面就是我所需要的一切了,如图 11.10 所示。

Android 特色开发,基于位置的服务_第5张图片

图 11.10

        baidumapapi_v2_3_1.jar 和 libBaiduMapSDK_v2_3_1.so 这两个文件都是使用百度地图所必不可少的。现在将 baidumapapi_v2_3_1.jar 拷贝到项目的 libs 目录下,然后在 libs 目录下新建一个 armeabi 目录,病菌 libBaiduMapSDK_v2_3_1.so 拷贝到新建的目录下,如图 11.11 所示。

Android 特色开发,基于位置的服务_第6张图片

图 11.11

        libs 目录你已经知道了是专门用于存放第三方 jar 包的地方,而 armeabi 目录则是专门用于存放 so 文件的地方。so 文件是用 C/C++ 语言进行编写,然后再用 NDK 编译出来的。libBaiduMapSDK_v2_3_1.so 这个文件已经由百度帮我们编译好了,因此直接放到 armeabi 目录下就可以使用了。

        接下来修改 activity_main.xml 中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.baidu.mapapi.map.MapView
        android:id="@+id/map_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clickable="true" />

</LinearLayout>

        在布局文件中我们只是放置了一个 MapView,并让它填充满整个屏幕。这个 MapView 是由百度提供的自定义控件,所以在使用它的时候需要将完整的包名加上。

        然后修改 MainActivity 中的代码,如下所示:

public class MainActivity extends Activity {

	private BMapManager manager;

	private MapView mapView;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		manager = new BMapManager(this);
		// API Key需要替换成你自己的
		manager.init("c1HhKtG7hEz4629i2XVegPDOmEETGucu", null);
		setContentView(R.layout.activity_main);
		mapView = (MapView) findViewById(R.id.map_view);
		mapView.setBuiltInZoomControls(true);
	}
	
	@Override
	protected void onResume() {
		mapView.onResume();
		if (manager != null) {
			manager.start();
		}
		super.onResume();
	}

	@Override
	protected void onPause() {
		mapView.onPause();
		if (manager != null) {
			manager.stop();
		}
		super.onPause();
	}
	
	@Override
	protected void onDestroy() {
		mapView.destroy();
		if (manager != null) {
			manager.destroy();
			manager = null;
		}
		super.onDestroy();
	}
}

        可以看到,这里的代码也是非常简单。首先需要创建一个 BMapManager 对象,然后调用它的 init() 方法进行初始化操。init() 方法接收两个参数,第一个参数就是在上一小节中我们申请到的 API Key,第二个参数传入 null 即可。注意初始化操作一定要在 setContentView() 方法前调用,不然的话就会出错。接下来我们获取到了 MapView 的实例,然后调用它的setBuiltInZoomControls() 方法并传入 true,表示启用内置的缩放控制功能

        另外还需要重写 onResume()、onPause() 和 onDestroy() 这三个方法,在这里对百度地图的 API 进行管理,以保证资源能够及时地得到释放。

        到此为止,我们的代码都十分简练,但下面的部分就十分繁杂了。相信你已经猜到,使用百度地图也需要在 AndroidManifest.xml 中声明权限的,不过不同于以往,这次我们要声明好多个权限才能保证百度地图的所有功能都可以正常使用。修改 AndroidManifest.xml 中的代码,如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.baidumaptest"
    android:versionCode="1"
    android:versionName="1.0" >
    ......
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_GPS" />
    ......
</manifest>

        其中虽然有一些权限在我们当前的例子中是用不到的,但全部添加进来也不见得是一件坏事,这样就不会时不时因为权限不足的问题导致程序崩溃了。

        现在运行一下程序,百度地图就应该成功显示出来了,如图 11.12 所示。

Android 特色开发,基于位置的服务_第7张图片

图 11.12

        由于我们启用了内置的缩放控制功能,因此屏幕的右下角会有两个用于放大和缩小的按钮,除此之外,使用多点触控的方式也可以对地图进行缩放

4.3  定位到我的位置

        地图是成功显示出来了,但也许这并不是你想要的。因为这是一张世界地图的全貌,而你可能希望看到更加精细的地图信息,比如说自己所在位置的周边环境。显然,通过缩放的方式来慢慢找到自己的位置是一种很愚蠢的做法。那么本小节我们就来学习一下,如何才能在地图中快速定位到自己的位置。

        百度地图的 API 中提供了一个 MapController类,它是地图的总控制器,调用 MapView getController() 方法就能获取到 MapController 的实例,如下所示:

MapController controller = mapView.getController();

        有了 MapController 后,我们就能对地图进行各种各样的操作了,比如设置地图的缩放级别就可以这样写:

controller.setZoom(12);

        其中 12 就表示一个缩放级别,其取值范围是 3 到 19,级别越高,地图显示的信息就越精细。

        那么怎样才能让地图定位到某一个经纬度上呢?这就需要借助 GeoPoint 类了。其实 GeoPoint 并没有什么太多的用法,主要就是用于存放经纬度值的,它的构造方法接收两个参数,第一个参数是纬度值,第二个参数是经度值。但是需要注意,GeoPoint 是以纬度为单位的,因此我们还要把经纬度的值乘以 10 的 6 次方再传给 GeoPoint。之后调用 MapController 的 setCenter() 方法,并把 GeoPoint 的实例传入就可以了,写法如下:

GeoPoint point = new GeoPoint((int) (39.915 * 1E6),(int) (116.404 * 1E6));
controller.setCenter(point);

        上述代码就实现了将地图定位到北纬 39.915 度、东经 116.404 度这个位置的功能。

        了解了这些知识之后,接下来再去实现在地图中快速定位自己位置的功能就变得非常简单了。首先我们可以利用在 11.2 节中所学的定位技术来获得自己当前位置的经纬度,之后将经纬度的值传入到 GeoPoint 的构造方法中,再调用 MapController 的 setCenter() 方法来设置地图的中心点就完成了。

        那么下面我们就来继续完善 BaiduMapTest 这个项目,加入定位到我的位置这个功能。

        修改 MainActivity 中的代码,如下所示:

public class MainActivity extends Activity {
	......
	private LocationManager locationManager;

	private String provider;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		manager = new BMapManager(this);
		// API Key需要替换成你自己的
		manager.init("c1HhKtG7hEz4629i2XVegPDOmEETGucu", null);
		setContentView(R.layout.activity_main);
		mapView = (MapView) findViewById(R.id.map_view);
		mapView.setBuiltInZoomControls(true);
		locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
		// 获取所有可用的位置提供器
		List<String> providerList = locationManager.getProviders(true);
		if (providerList.contains(LocationManager.GPS_PROVIDER)) {
			provider = LocationManager.GPS_PROVIDER;
		} else if (providerList.contains(LocationManager.NETWORK_PROVIDER)) {
			provider = LocationManager.NETWORK_PROVIDER;
		} else {
			// 当没有可用的位置提供器时,弹出Toast提示用户
			Toast.makeText(this, "No location provider to use",
					Toast.LENGTH_SHORT).show();
			return;
		}
		Location location = locationManager.getLastKnownLocation(provider);
		if (location != null) {
			navigateTo(location);
		}
	}

	private void navigateTo(Location location) {
		MapController controller = mapView.getController();
		// 设置缩放级别
		controller.setZoom(16);
		GeoPoint point = new GeoPoint((int) (location.getLatitude() * 1E6),
				(int) (location.getLongitude() * 1E6));
		// 设置地图中心点
		controller.setCenter(point);
	}
	......
}

        这里大部分的代码你应该非常熟悉了,在获取到了 Location 对象后,我们将它传入到了 navigateTo() 方法中。那么 navigateTo() 方法又做了什么呢?其实也非常简单,先是获取到了 MapController 的实例,并将地图的缩放级别设置成 16,然后把 Location 中存储的经纬度值乘以 10 的 6 次方后传入 GeoPoint 的构造函数,最后调用了 setCenter() 方法。

        现在重新运行一下程序,结果如图 11.13 所示。

Android 特色开发,基于位置的服务_第8张图片

图 11.13

        怎么样?这时地图上的信息看起来就要比世界地图丰富得多了吧。

4.4  使用覆盖物来增加更多功能

        除了普通的地图展示之外,百度地图还提供了一种叫做覆盖物的功能,所有叠加或覆盖到地图上的内容都被统称为地图覆盖物,如标注、矢量图形元素、定位图标等。覆盖物拥有自己的地理坐标,当我们拖动或缩放地图时,它们会自动进行相应地移动

        百度地图提供了很多种类型的覆盖物,开发人员可以根据自己的实际需求来选择使用哪些覆盖物,本小节中我们就来学习其中的两种。

        在百度地图所有的覆盖物种,最常用的就是 MyLocationOverlay 了,它主要的作用是可以在地图中添加一个图层,以标注出设备当前的位置。而 MyLocationOverlay 的用法也是非常简单,我们直接就在 BaiduMapTest 项目的基础上继续编写了,修改 MainActivity 中的代码,如下所示:

public class MainActivity extends Activity {
	......
	private void navigateTo(Location location) {
		MapController controller = mapView.getController();
		// 设置缩放级别
		controller.setZoom(16);
		GeoPoint point = new GeoPoint((int) (location.getLatitude() * 1E6),
				(int) (location.getLongitude() * 1E6));
		// 设置地图中心点
		controller.setCenter(point);
		MyLocationOverlay myLocationOverlay = new MyLocationOverlay(mapView);
		LocationData locationData = new LocationData();
		// 指定我的位置
		locationData.latitude = location.getLatitude();
		locationData.longitude = location.getLongitude();
		myLocationOverlay.setData(locationData);
		mapView.getOverlays().add(myLocationOverlay);
                // 刷新使新增覆盖物生效
          mapView.refresh();
 }
	......
}

        可以看到,这里首先是创建了一个 MyLocationOverlay 的实例,然后通过 LocationData 对象指定了当前的经纬度数据,并调用 setData() 方法将 LocationData 存放到了 MyLocationOverlay 中。之后通过 MapView 的 getOverlays() 方法可以得到一个用于管理覆盖物的集合,再调用 add() 方法将 MyLocationOverlay 这个覆盖物添加到集合中。最后,还需要调用一下 MapView 的 refresh() 方法使新增的覆盖物生效。

        就是这么简单,现在重新运行一下程序,结果如图 11.14 所示。

Android 特色开发,基于位置的服务_第9张图片

图 11.14

        这样的话,用户就可以非常清晰地看出自己当前是在哪里了。

        MyLocationOverlay 的用法确实非常简单,那么下面我再学习一下 PopupOverlay 这种覆盖物的用法吧。相比于 MyLocationOverlay,PopupOverlay 允许我们自己指定覆盖物上显示的图片,并且还可以响应图片的点击事件,每个 PopupOverlay 上最多可以显示三张图片。

        那么为了要尝试一下 PopupOverlay 的用法,我就实现准备好了三张图片存放在 drawable 目录下,分别命名为 left.png、middle.png 和 right.png。然后修改 MainActivity 中的代码,如下所示:

public class MainActivity extends Activity {
	......
	private void navigateTo(Location location) {
		MapController controller = mapView.getController();
		// 设置缩放级别
		controller.setZoom(16);
		GeoPoint point = new GeoPoint((int) (location.getLatitude() * 1E6),
				(int) (location.getLongitude() * 1E6));
		// 设置地图中心点
		controller.setCenter(point);
		MyLocationOverlay myLocationOverlay = new MyLocationOverlay(mapView);
		LocationData locationData = new LocationData();
		// 指定我的位置
		locationData.latitude = location.getLatitude();
		locationData.longitude = location.getLongitude();
		myLocationOverlay.setData(locationData);
		mapView.getOverlays().add(myLocationOverlay);
		// 刷新使新增覆盖物生效
		mapView.refresh();
		PopupOverlay pop = new PopupOverlay(mapView, new PopupClickListener() {
			@Override
			public void onClickedPopup(int index) {
				// 响应图片的点击事件
				Toast.makeText(MainActivity.this,
						"You clicked button " + index, Toast.LENGTH_SHORT)
						.show();
			}
		});
		// 创建一个长度为3的Bitmap数组
		Bitmap[] bitmaps = new Bitmap[3];
		try {
			// 将三张图片读取到内存中
			bitmaps[0] = BitmapFactory.decodeResource(getResources(),
					R.drawable.left);
			bitmaps[1] = BitmapFactory.decodeResource(getResources(),
					R.drawable.middle);
			bitmaps[2] = BitmapFactory.decodeResource(getResources(),
					R.drawable.right);
		} catch (Exception e) {
			e.printStackTrace();
		}
		pop.showPopup(bitmaps, point, 18);
	}
        ......
}

        令人高兴的是,PopupOverlay 的用法也非常简单,总共没几行代码。首先同样是需要创建一个 PopupOverlay 的实例,注意在构造方法的参数里面可以传入一个 PopupClickListener 的实现,它是用于处理图片的点击事件的,简单起见,我们只是在图片被点击的时候弹出一个 Toast。接下来创建了一个长度为 3 的 Bitmap 数组,然后调用 BitmapFactory 的 decodeResource() 方法来加载 left、middle 和 right 这三张图片,并把它们存放到 Bitmap 数组当中。最后调用 PopupOverlay 的showPopup() 方法将这个覆盖物显示出来,showPopup() 方法接收三个参数,第一个参数是前面创建的 Bitmap 数组第二个参数是一个用于指定地理位置的 GeoPoint 对象第三个参数是覆盖物在垂直方法上的偏移距离

        现在重新运行一下代码,应该就会看到有一个 popup 窗口显示在我的位置上方,并且窗口上的图片都是可以点击的。(mx4Pro机型测试:showPopup() 方法调用一直报空指针异常

摘自《第一行代码》


你可能感兴趣的:(android,位置,百度地图)