1. 程式人生 > >Android 特色開發,基於位置的服務

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 所示。

圖 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 所示。

圖 11.4

        只需要填寫“*”號的那部分內容就足夠了,接下來點選提交。驗證郵箱後你就已經成為了一名百度開發者了。接著點選去建立應用,然後選擇我的應用,會看到如圖 11.6 所示的介面。

圖 11.6

        這裡會顯示所有你申請過的 API Key,由於這是一個剛剛註冊的賬號,所以目前只有一個系統預設的 API Key。接下來點選建立應用就可以去申請新的 API Key 了,應用名稱可以隨便填,應用型別選擇 Android SDK,如圖 11.7 所示。

圖 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

4.2  讓地圖顯示出來

        現在正是趁熱打鐵的好時機,新建一個 BaiduMapTest 專案,並將包命名為 com.example.baidumaptest。在開始編碼之前,我們還需要先將百度地圖 Android 版的 SDK 準備好,下載地址是:http://developer.baidu.com/map/sdkandev-download.htm,然後點選全部下載按鈕就可以了。

        下載完成後對壓縮包解壓,應該可以看到其中有三個壓縮包(最新的只剩一個 libs 檔案夾了)。其中 Docs 包中含有百度地圖的使用文件,Sample 包中含有一個使用百度地圖的工程樣例,Lib 包中含有使用百度地圖所必須依賴的庫檔案。解壓 Lib 包,這裡面就是我所需要的一切了,如圖 11.10 所示。


圖 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 所示。

圖 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 所示。

圖 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 所示。

圖 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 所示。

圖 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() 方法呼叫一直報空指標異常

摘自《第一行程式碼》