第 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()方法将此功能开启,否则设备的位置将无法在地图上显示。而在程序退出的时候,也要记得将此功能给关闭掉。