第 11 章
11.1 基于位置的服務簡介
基于位置的服務(Location Based Service)簡稱LBS,主要的工作原理就是利用無線電通訊網絡或GPS等定位方式來確定出移動設備所在的位置。
基于位置的服務所圍繞的核心就是要先確定出用戶所在的位置。通常有兩種技術方式可以實現:一種是通過GPS定位,一種是通過網絡定位。GPS定位的工作原理是基于手機內置的GPS硬件直接和衛星交互來獲取當前的經緯度信息,這種定位方式精確度非常高,但缺點是只能在室外使用,室內基本無法接收到衛星的信號。網絡定位的工作原理是根據手機當前網絡附近的三個基站進行測速,以此計算出手機和每個基站之間的距離,再通過三角定位確定出一個大致的位置,這種定位方式精確度一般,但優點是在室內室外都可以使用。
11.2 申請API Key
要想在自己的應用程序里使用百度的LBS功能,首先需要申請一個API Key.
訪問[]http://lbsyun.baidu.com/apiconsole/key。
這個地址,點擊創建應用就可以去申請API Key了,應用名稱可以隨便填,應用類型選擇Android SDK,啟用服務保持默認即可。
這個發布版SHA1和開發版SHA1又是什么東西呢?這是我們申請API Key 所必須填寫的一個字段,它指的是打包程序時所用簽名文件的SHA1指紋,可以通過Android Studio查看到。
打開Android Studio中的任意一個項目,點擊右側工具欄的Gradle---項目名---:app---Tasks---android
這里展示了一個Android Studio項目中所有內置的Gradle Tasks,其中signingReport這個Task就可以用來查看簽名文件信息。雙擊signingReport。
其中,E2:FB:B5:1F:CA:38:8D:40:EB:47:30:E0:28:0D:DB:C3:5A:9D:05:B6就是我們所需的SHA1指紋了,另外需要注意,目前我們使用的是debug.keystore文件所生成的指紋,這是Android自動生成的一個用于測試的簽名文件。而當你的應用程序發布時還需要創建一個正式的簽名文件,如果要得到他的指紋,可以在cmd中輸入如下命令:
keytool -list -v -keystore <簽名文件路徑>
現在得到的這個SHA1指紋實際上是一個開發版的SHA1指紋,不過因為暫時我們還沒有一個發布版的SHA1指紋,因此這兩個值都填成一樣就可以了。然后輸入包名后,提交就可以得到API Key了。
jPeDkML7MGusQ7CUUaZhR9YETfA8Ux42就是申請到的API Key,有了它就可以進行后續的LBS開發工作了。
11.3 使用百度地圖
11.3.1 準備LBS SDK
在開始編碼之前,我們還需要先將百度LBS開放平臺的SDK準備好。地址[]http://lbsyun.baidu.com/sdk/download?selected=mapsdk_basicmap,mapsdk_searchfunction,mapsdk_lbscloudsearch,mapsdk_calculationtool,mapsdk_radar
本章中我們會用到基礎地圖和定位功能這兩個SDK,將它們勾選上。
下載完成后對該壓縮包解壓,其中會有libs目錄,這里面的內容就是我們所需要的一切了。
libs目錄下的內容又分為兩部分,BaiduLBS_Android.jar這個文件是Java層要使用到的,其他子目錄下的so文件是Native層要用到的。so文件是用C/C++語言進行編寫,然后再用NDK編譯出來的。
首先觀察一下當前的項目結構,你會發現app模塊下面有一個libs目錄,這里就是用來存放所有的Jar文件的,我們將BaiduLBS_Android.Jar復制到這里。
接下來展開src/main目錄,右擊該目錄-->New-->Directory,再創建一個名為jniLibs的目錄,這里就是專門用來存放so文件的,然后把壓縮包里面的其他目錄直接復制到這里。
雖然所有新創建的項目中,app/build.gradle文件都會默認配置一下這段聲明:
dependencies
{
compile fileTree(dir: 'libs', include: ['*.jar'])
......
}
這表示將libs目錄下所有以.jar結尾的文件添加到當前項目的引用中。但是由于我們是直接將Jar包復制到libs目錄下的,并沒有修改gradle文件,因此不會彈出我們平時熟悉的Sync Now提示。這個時候必須手動點擊一下Android Studio頂部工具欄中的Sync按鈕,不然項目將無法引用到Jar包中提供的任何接口。
點擊Sync按鈕之后,libs目錄下的jar文件就會多出一個向右的箭頭,這就表示項目已經能引用到這些Jar包了。
11.3.2 確定自己位置的經緯度
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.lbstest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.GET_TASKS" />
<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.ACCESS_FINE_LOCATION"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data
android:name="com.baidu.lbsapi.API_KEY"
android:value="jPeDkML7MGusQ7CUUaZhR9YETfA8Ux42"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service android:name="com.baidu.location.f"
android:enabled="true"
android:process=":remote">
</service>
</application>
</manifest>
這里首先添加了很多行權限申明,每一個權限都是百度LBS SDK內部要用到的。然后在<application>標簽的內部添加了一個<meta-data>標簽,這個標簽的android:name部分是固定的,必須填com.baidu.lbsapi.API_KEY,android:value部分則應該填入我們上一節申請到的API Key。最后再注冊一個LBS SDK中的服務,不用對這個服務的名字感到疑惑,因為百度LBS SDK中的代碼都是混淆過的。
public class MainActivity extends AppCompatActivity
{
public LocationClient mLocationClient;
private TextView positionText;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mLocationClient = new LocationClient(getApplicationContext());
mLocationClient.registerLocationListener(new MyLocationListener());
setContentView(R.layout.activity_main);
positionText = (TextView) findViewById(R.id.position_text_view);
List<String> permissionList = new ArrayList<>();
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission
.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)
{
permissionList.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
if (ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission
.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED)
{
permissionList.add(Manifest.permission.READ_PHONE_STATE);
}
if (ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission
.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
{
permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
if (!permissionList.isEmpty())
{
String [] permissions = permissionList.toArray(new String[permissionList.size()]);
ActivityCompat.requestPermissions(MainActivity.this,permissions,1);
}
else
{
requestLocation();
}
}
private void requestLocation()
{
initLocation();
mLocationClient.start();
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults)
{
switch (requestCode)
{
case 1:
if (grantResults.length > 0)
{
for (int result : grantResults)
{
if (result != PackageManager.PERMISSION_GRANTED)
{
Toast.makeText(MainActivity.this, "必須同意所有權限才能使用本程序",
Toast.LENGTH_SHORT).show();
finish();
return;
}
}
requestLocation();
}
else
{
Toast.makeText(MainActivity.this, "發生未知錯誤", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
public class MyLocationListener implements BDLocationListener
{
@Override
public void onReceiveLocation(BDLocation bdLocation)
{
StringBuilder currentPosition = new StringBuilder();
currentPosition.append("維度:").append(bdLocation.getLatitude())
.append("\n");
currentPosition.append("經線:").append(bdLocation.getLongitude())
.append("\n");
currentPosition.append("定位方式: ");
if (bdLocation.getLocType() == BDLocation.TypeGpsLocation)
{
currentPosition.append("GPS");
}
else if (bdLocation.getLocType() == BDLocation.TypeNetWorkLocation)
{
currentPosition.append("網絡");
}
positionText.setText(currentPosition);
}
@Override
public void onConnectHotSpotMessage(String s, int i)
{
}
}
private void initLocation()
{
LocationClientOption option = new LocationClientOption();
option.setScanSpan(5000);
mLocationClient.setLocOption(option);
}
@Override
protected void onDestroy()
{
super.onDestroy();
mLocationClient.stop();
}
}
在onCreate()方法中,我們首先創建了一個LocationClient的實例,LocationClient的構建函數接收一個Context參數,這里調用getApplicationContext()方法來獲取一個全局的Context參數并傳入。然后調用LocationClient的registerLocationListener()方法來注冊一個定位監聽器,當獲取到位置信息的時候,就會回調這個定位監聽器。
由于我們在AndroidManifest.xml中申明了很多權限,其中ACCESS_COARSE_LOCATION,ACCESS_FINE_LOCATION,READ_PHONE_STATE,WRITE_EXTERNAL_STORAGE這四個權限是需要進行運行時權限處理的,不過由于ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION都屬于通一個權限組,因此兩者只需要申請其一就可以了。那么怎樣才能在運行時一次性申請三個權限呢?這里我們使用了一種新的用法,首先創建一個空的List集合,然后依次判斷這三個權限有沒有被授權,如果沒被授權就添加到List集合中,最后將List轉換成數組,再調用ActivityCompat.requestPermissions()方法一次性申請。
onRequestPermissionsResult()方法中對權限申請結果的邏輯處理也和之前有所不同,這次我們通過一個循環將申請的每個權限都進行了判斷,如果有任何一個權限被拒絕,那么就直接調用finish()方法關閉當前程序,只有當所有權限都被用戶同意了,才會調用requestLocation()方法開始地理位置定位。
requestLocation()方法中的代碼比較簡單,只是調用了一下LocationClient的start()方法就能開始定位了。定位的結果會回調到我們前面我們注冊的監聽器中,也就是MyLocationListener。觀察一下MyLocationListener的onReceiveLocation()方法中,在這里我們通過BDLocation的getLatitude()方法獲取當前位置的維度,通過getLongitude()方法獲取當前位置的精度,通過getLocType()方法獲取當前的定位方式,最終將結果組裝成一個字符串,顯示到TextView上面。
在initLocation()方法中我們創建了一個LocationClientOption對象,然后調用它的setScanSpan()方法來設置更新的間隔。這里傳入了5000,表示每5秒鐘會更新一下當前的位置。
最后要記得,在活動被銷毀的時候一定要調用LocationClient的stop()方法來停止定位不然程序會持續在后臺不停地進行定位,從而嚴重消耗手機的電量。
11.3.4 選擇定位模式
GPS定位功能必須要由用戶主動去啟用才行,不然任何應用程序都無法使用GPS獲取到手機當前的位置信息。
private void initLocation()
{
LocationClientOption option = new LocationClientOption();
option.setLocationMode(LocationClientOption.LocationMode.Device_Sensors);
mLocationClient.setLocOption(option);
}
我們在initLocation()方法中對百度LBS SDK的定位模式進行指定,一共有三種模式可選:Hight_Accuracy,Battery_Saving和Device_Sennors。
Hight_Accuracy:表示高精確度模式,會在GPS信號正常的情況下優先使用GPS定位,在無法接收GPS信號的時候使用網絡定位。
Battery_Saving:表示節電模式,只會使用網絡進行定位,。
Device_Sennors:表示傳感器模式,只會使用GPS定位,其中Hight_Accuracy是默認的模式,也就是說,我們即使不修改任何代碼,只要拿到手機走到室外去,讓手機可以接收到GPS信號,就會自動切換到GPS定位模式了。
11.3.4 看得懂的位置信息
private void initLocation()
{
LocationClientOption option = new LocationClientOption();
//option.setLocationMode(LocationClientOption.LocationMode.Device_Sensors);
option.setScanSpan(5000);
option.setIsNeedAddress(true);
mLocationClient.setLocOption(option);
}
首先在initLocation()方法中,我們調用了LocationClientOption的setIsNeedAddress()方法,并傳入true,這就表示我們需要獲取當前位置詳細的地址信息。
currentPosition.append("國家: ").append(bdLocation.getCountry())
.append("\n");
currentPosition.append("省: ").append(bdLocation.getProvince())
.append("\n");
currentPosition.append("市: ").append(bdLocation.getCity())
.append("\n");
currentPosition.append("區: ").append(bdLocation.getDistrict())
.append("\n");
currentPosition.append("街道: ").append(bdLocation.getStreet())
.append("\n");
在MyLocationListener的onReceiveLocation()方法就可以獲取到各種豐富的地址信息了。調用getCountry()方法可以得到當前所造國家。調用getProvince()方法可以得到當前所在省份,以此類推。另外還有一點需要注意,由于獲取地址信息一定需要用到網絡,因此即使我們將定位模式指定成了Device_Sensors,也會自動開啟網絡定位功能。
11.4 使用百度地圖
11.4.1 讓地圖顯示出來
<com.baidu.mapapi.map.MapView
android:id="@+id/bmapView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.baidu.mapapi.map.MapView>
MapView是由百度提供的自定義控件,所以在使用它的時候需要將完整的包名加上。
public class MainActivity extends AppCompatActivity
{
private MapView mapView;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
````
SDKInitializer.initialize(getApplicationContext());
setContentView(R.layout.activity_main);
mapView = (MapView) findViewById(R.id.bmapView);
````
}
```
@Override
protected void onResume()
{
super.onResume();
mapView.onResume();
}
@Override
protected void onPause()
{
super.onPause();
mapView.onPause();
}
@Override
protected void onDestroy()
{
super.onDestroy();
mLocationClient.stop();
mapView.onDestroy();
}
}
首先需要調用SDKInitializer的initialize()方法來進行初始化操作,initialize()方法接收一個Context參數,這里我們調用getApplicationContext()方法來獲取一個全局的Context參數并傳入。注意初始化操作一定要在setContentView()方法前調用,不然的話就會出錯。接下來我們調用findViewById()方法獲取到了MapView的實例,這個實例在后面的功能當中還會用到。
另外還需要重寫onResume(),onPause()和onDestroy()這三個方法,在這里對MapView進行管理,以保證資源能夠及時的得到釋放。
11.4.2 移動到我的位置
百度LBS SDK的API中提供了一個BaiduMap類,它是地圖的總控制器,調用MapView的getMap()方法就能獲取到BaiduMap的實例。
BaiduMap baiduMap = mapView.getMap();
有了BaiduMap后,我們就能對地圖進行各種各樣的操作了,比如設置地圖的縮放級別以及將地圖移動到某一個經緯度上。
百度地圖將縮放級別的取值范圍限定在3~19之間,其中小數點位的值也是可以取得,值越大,地圖顯示的信息就越精細。
MapStatusUpdate update = MapStatusUpdateFactory.zoomTo(12.5f);
baiduMap.animateMapStatus(update);
其中MapStatusUpdateFactory的zoomTo()方法接收一個float型的參數就是用于設置縮放級別的,這里我們傳入12.5f。zoomTo()方法返回一個MapStatusUpdate對象,我們把這個對象傳入BaiduMap的animateMapStatus()方法即可完成縮放功能。
讓地圖移動到某一經緯度上,這就需要借助LatLng類了,其實LatLng并沒有什么太多的用法,主要就是用于存放經緯度值得,它的構造方法接收兩個參數,第一個參數是緯度值,第二個參數是經度值。之后調用MapStatusUpdate的newLatLng()方法將LatLng對象傳入,newLatLng()方法返回的也是一個MapStatusUpdate對象,我們再把這個對象傳入BaiduMap的animateMapStatus()方法當中,就可以將地圖移動到指定的經緯度上了。
LatLng ll new LatLng(39.915,116.4.4);
MapStatusUpdate update = MapStatusUpdateFactory.newLatLng(ll);
baiduMap.animateMapStatus(update);
public class MainActivity extends AppCompatActivity
{
private LocationClient mLocationClient;
private MapView mapView;
private BaiduMap baidumap;
private boolean isFirstLocate = true;
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mLocationClient = new LocationClient(getApplicationContext());
mLocationClient.registerLocationListener(new MyLocationListener());
SDKInitializer.initialize(getApplicationContext());
setContentView(R.layout.activity_main);
mapView = (MapView) findViewById(R.id.bmapView);
baidumap = mapView.getMap();
}
private void requestLocation()
{
//initLocation();
mLocationClient.start();
}
private void initLocation()
{
LocationClientOption option = new LocationClientOption();
//option.setScanSpan(5000);
//option.setLocationMode(LocationClientOption.LocationMode.Device_Sensors);
mLocationClient.setLocOption(option);
}
private void navigateTo(BDLocation location)
{
if (isFirstLocate)
{
LatLng ll = new LatLng(location.getLatitude(),location.getLongitude());
Log.d(TAG, "navigateTo: "+location.getLatitude()+"w"+location.getLongitude());
MapStatusUpdate update = MapStatusUpdateFactory.newLatLng(ll);
baidumap.animateMapStatus(update);
update = MapStatusUpdateFactory.zoomTo(16f);
baidumap.animateMapStatus(update);
isFirstLocate = false;
}
}
public class MyLocationListener implements BDLocationListener
{
@Override
public void onReceiveLocation(BDLocation bdLocation)
{
if (bdLocation.getLocType() == BDLocation.TypeGpsLocation
|| bdLocation.getLocType() == BDLocation.TypeNetWorkLocation)
{
Log.d(TAG, "requestLocation: "+"3333333333333332");
navigateTo(bdLocation);
}
}
@Override
public void onConnectHotSpotMessage(String s, int i)
{
}
}
@Override
protected void onResume()
{
super.onResume();
mapView.onResume();
}
@Override
protected void onPause()
{
super.onPause();
mapView.onPause();
}
@Override
protected void onDestroy()
{
super.onDestroy();
mLocationClient.stop();
mapView.onDestroy();
}
}
我們主要是新加了一個navigateTo()方法。這個方法中的代碼也很好理解,先是將BDLocation對象中的地理位置信息取出并封裝到LatLng對象中,然后調用MapStatusUpdateFactory的newLatLng()方法并將LatLng對象傳入,接著將返回的MapStatusUpdate對象作為參數傳入到BaiduMap的animateStatus()方法中,我們將縮放級別設置成了16,另外還有一點需要注意,上訴代碼當中我們使用了一個isFirstLocate變量,這個變量的作用是為了防止多次調用animateMapStatus()方法,因為將地圖一定到我們當前的位置只需要在程序第一次定位的時候調用一次就可以了。
11.4.3 讓"我"顯示在地圖上
百度LBS SDK當中提供了一個MyLocationData.Builder類,這個類是用來封裝設備當前所在位置的,我們只需將經緯度信息傳入到這個類的相應方法當中就可以了。
MyLocationData.Builder locationBuilder = new MyLocationData.Builder();
locationBuilder.latitude(39.915);
locationBuilder.longitude(116.404);
MyLocationData.Builder類還提供了一個builder()方法,當我們把要封裝的信息都設置完成之后,只需要調用它的build()方法,就會生成一個MyLocationData的實例,然后再將這個實例傳入到BaiduMap的setMyLocationData()方法當中,就可以讓設備當前的位置顯示在地圖上。
MyLocationData location locationData = locationBuilder.build();
baiduMap.setMyLocationData(locationData);
mapView = (MapView) findViewById(R.id.bmapView);
baidumap = mapView.getMap();
baidumap.setMyLocationEnabled(true);
private void navigateTo(BDLocation location)
{
if (isFirstLocate)
{
LatLng ll = new LatLng(location.getLatitude(),location.getLongitude());
Log.d(TAG, "navigateTo: "+location.getLatitude()+"w"+location.getLongitude());
MapStatusUpdate update = MapStatusUpdateFactory.newLatLng(ll);
baidumap.animateMapStatus(update);
update = MapStatusUpdateFactory.zoomTo(16f);
baidumap.animateMapStatus(update);
isFirstLocate = false;
}
MyLocationData.Builder locationBuilder = new MyLocationData.Builder();
locationBuilder.latitude(location.getLatitude());
locationBuilder.longitude(location.getLongitude());
MyLocationData locationData = locationBuilder.build();
baidumap.setMyLocationData(locationData);
}
@Override
protected void onDestroy()
{
super.onDestroy();
mLocationClient.stop();
mapView.onDestroy();
baidumap.setMyLocationEnabled(false);
}
在navigateTo()方法中,我們添加了MyLocationData的構建邏輯,將Location中包含的經度和緯度分別封裝到了MyLocationData.Builder當中,最后把MyLocationData設置到BaiduMap的setMyLocationData()方法當中。注意這段邏輯必須寫在isFirstLocate這個if條件語句的外面,因為讓地圖移動到我們當前的位置只需要在第一次定位的時候調用,但是設備在地圖上顯示的位置卻應該是隨著設備的移動而實時改變的。
另外,根據百度地圖的限制,如果我們想要實現這一功能,一定要事先調用BaiduMap的setMyLocationEnabled()方法將此功能開啟,否則設備的位置將無法在地圖上顯示。而在程序退出的時候,也要記得將此功能給關閉掉。