基于位置的服务简称 LBS,主要的工作原理就是利用无线电通讯网络或 GPS 等定位方式来确定出移动设备所在的位置
基于位置的服务所围绕的核心就是要先确定出用户所在的位置。通常有两种 技术方式可以实现:一种是通过 GPS 定位,一种是通过网络定位。
GPS 定位的工作原理是基于 手机内置的 GPS 硬件直接和卫星交互来获取当前的经纬度信息,这种定位方式精确度非常高,但缺点是只能在室外使用,室内基本无法接收到卫星的信号。
网络定位的工作原理是根据手机当 前网络附近的三个基站进行测速,以此计算出手机和每个基站之间的距离,再通过三角定位确定出一个大概的位置,这种定位方式精确度一般,但优点是在室内室外都可以使用。
Android 对这两种定位方式都提供了相应的 API 支持,但是由于Google 的网络服务在中国不可访问,导致网络定位方式 API 失效。而 GPS 定位虽然不需要网络,但是必须要在室外才可以使用,因此在室内开发的时候会遇到不管使用哪种定位方式都无法成功定位的情况。
基于以上原因,决定使用国内第三方公司的 SDK。学习一下百度在 LBS 方面提供的功能。
1.申请 API Key
要想在自己的应用程序里使用百度的 LBS 功能,首先必须申请一个 API Key。你得拥有一个百度账号才能进行申请
登录你的百度账号,并打开 http://developer.baidu.com/user/reg 这个网址,在这里填写一些注册信息即可
然后访问这个https://lbsyun.baidu.com/apiconsole/key/create#/home网址查看我的应用,创建一个应用.
应用名称可以随便填,应用类型选择 Android SDK,启用服务保持默认即可
这个发布版 SHA1 和开发版 SHA1 是我们申请 API Key 所必须填写的一个字段,它指的是打包程序时所用签名文件的 SHA1 指纹,可以通过 Android Studio 查看到。打开 Android Studio 中的任意一个项目,点击右侧工具栏的 Gradle→项目名→:app→Tasks→ android,(我这里没有)
这里展示了一个 Android Studio 项目中所有内置的 Gradle Tasks,其中 signingReport 这个 Task 就可以用来查看签名文件信息。双击 signingReport,
/
添加keytool环境变量,C:\Program Files\Java\jdkxxxx\bin添加这个路径至环境变量中的path中
然后在C:\Users\Administrator\.android目录下打开cmd
输入keytool -list -v -keystore debug.keystore
输入android
会有一个SHA1后面的便是我们所需的 SHA1 指纹了
这是 Android 自动生成的一个用于测试的签名文件。而当你的应用程序发布时还需要创建一个正式的签名文件,
如果要得到它的指纹,可以在 cmd 中输入如下命令:
keytool -list -v –keystore <签名文件路径>
然后输入正确的密码就可以了。
现在得到的这个 SHA1 指纹实际上是一个开发版的 SHA1 指纹,不过因为暂时我们还没有一个发布版的 SHA1 指纹,因此这两个值都填成一样的就可以了。
最后包名选项就叫 com.example.lbstest,这样所有的内容就都填写完整了
接下来点击提交,应用就应该创建成功了,
其中,访问应用(AK)就是申请到的 API Key,有了它就可以进行后续的 LBS 开发工作了,
2.使用百度定位
新建一个 LBSTest 项目,包名应该就会自动被命名为 com.example.lbstest。
1.准备 LBS SDK
在开始编码之前,我们还需要先将百度 LBS 开放平台的 SDK 准备好,下载地址是: https://lbsyun.baidu.com/index.php?title=sdk/download&action#selected=location_all。本章中我们会用到基础地图和定位功能这两个 SDK,将它们勾选上,然后点击“开发包”“JAR”下载按钮即可
下载完成后对该压缩包解压,其中会有一个 libs 目录,这里面的内容就是我们所需要的一切了
libs 目录下的内容又分为两部分,BaiduLBS_Android.jar 这个文件是 Java 层要使用到的,其 他子目录下的 so 文件是 Native 层要用到的。so 文件是用 C/C++语言进行编写,然后再用 NDK 编译出来的。当然这里我们并不需要去编写 C/C++的代码,因为百度都已经做好了封装,但是我们需要将 libs 目录下的每一个文件都放置到正确的位置。
首先观察一下当前的项目结构,你会发现 app 模块下面有一个 libs 目录,这里就是用来存放 所有的 Jar 包的,我们将 BaiduLBS_Android.jar 复制到这里
(我的没有,我新建了一个libs文件夹放了进去)
右键这个jar选择最下面的add as library
接下来展开 src/main 目录,右击该目录→New→Directory,再创建一个名为 jniLibs 的目录, 这里就是专门用来存放 so 文件的,然后把压缩包里的其他所有目录直接复制到这里,
在AndroidManifest.xml中的<application标签内添加<meat-data
<meta-data android:name="com.baidu.lbsapi.API_KEY" android:value="yourkey"/>
yourkey是你在百度地图申请的AK
2.确定自己位置的经纬度
修改activity_main.xml
<TextView
android:id="@+id/position_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
只有一个 TextView 控件,用于稍后显示当前位置的经纬度信息
然后修改 AndroidManifest.xml 文件中的代码
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
AndroidManifest.xml 文件改动比较多,我们来仔细阅读一下。可以看到,这里首先添加了很 多行权限声明,每一个权限都是百度 LBS SDK 内部要用到的。
然后在标签的内部添加了一个标签,这个标签的 android:name 部分是固定的,必须填 com.baidu. lbsapi.API_KEY,android:value 部分则应该填入我们在 11.2 节申请到的 API Key。(前面加过了)
最后,还需要再注册一个 LBS SDK 中的服务,不用对这个服务的名字感到疑惑,因为百度 LBS SDK 中的代码都是混淆过的。
接下来修改 MainActivity 中的代码
package com.example.lbstest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.location.LocationListener;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.baidu.location.BDLocation;
import com.baidu.location.BDLocationListener;
import com.baidu.location.LocationClient;
import com.baidu.mapapi.map.BaiduMap;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
public LocationClient mLocationClient;
private TextView positionText;
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
positionText = findViewById(R.id.tv);
try {
mLocationClient = new LocationClient(getApplicationContext());
} catch (Exception e) {
e.printStackTrace();
}
mLocationClient.registerLocationListener(new MyLocationListener());
List<String> permissionList = new ArrayList<>();
if (ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(android.Manifest.permission.ACCESS_FINE_LOCATION);
}
if (ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.
permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(android.Manifest.permission.READ_PHONE_STATE);
}
if (ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.
permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {
permissionList.add(android.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(){
mLocationClient.start();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 1:
if (grantResults.length > 0) {
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "必须同意所有权限才能使用本程序",
Toast.LENGTH_SHORT).show();
finish();
return;
}
}
requestLocation();
} else {
Toast.makeText(this, "发生未知错误", Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
}
}
public class MyLocationListener implements BDLocationListener {
@Override
public void onReceiveLocation(BDLocation bdLocation) {
runOnUiThread(new Runnable() {
@Override
public void run() {
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);
}
});
}
}
}
在 onCreate()方法中,首先创建了一个 LocationClient 的实例, LocationClient 的构建函数接收一个 Context 参数,调用 getApplicationContext()方法来获取一个全局的 Context 参数并传入。
然后调用 LocationClient 的 registerLocationListener()方法来注册一个定位监听器,当获取到位置信息的时候,就会回调这个定位监听器。
接下来看运行时权限的用法,由于我们在 AndroidManifest.xml 中声明了很多权限, 其中 ACCESS_COARSE_LOCATION、ACCESS_ FINE_LOCATION、READ_PHONE_STATE、WRITE_EXTERNAL_STORAGE 这 4 个权限是需要进行运行时权限处理的,不过由于 ACCESS_COARSE_LOCATION 和 ACCESS_FINE_LOCATION 都属于同一个权限组,因此两者只要申请其一就可以了。
这里使用了一种新的用法,首先创建一个空的 List 集合,然后依次判断这 3 个权限有没有被授权, 如果没被授权就添加到 List 集合中,最后将 List 转换成数组,再调用 ActivityCompat.requestPermissions()方法一次性申请。
onRequestPermissionsResult()方法中对权限申请结果的逻辑处理我们通过一个循环将申请的每个权限都进行了判断,如果有任何一个权限被拒绝, 那么就直接调用 finish()方法关闭当前程序,只有当所有权限都被用户同意了,才会调用 requestLocation()方法开始地理位置定位。 requestLocation()方法中的代码比较简单,只是调用了一下 LocationClient 的 start() 方法就能开始定位了。定位的结果会回调到我们前面注册的监听器MyLocationListener
观察一下 MyLocationListener 的 onReceiveLocation()方法中,在这里通过BDLocation 的 getLatitude()方法获取当前位置的纬度,通过 getLongitude()方法获 取当前位置的经度,通过 getLocType()方法获取当前的定位方式,最终将结果组装成一个字符串,显示到 TextView 上面。
报错了,显示mLocationClient是null,这说明赋值时报错了直接跳过了赋值,要在try前面添加
LocationClient.setAgreePrivacy(true);
我傻了,必须打开定位才能成功定位,但是打开定位后显示的定位方式是网络
暂时先继续下去吧
在默认情况下,调用 LocationClient 的 start()方法只会定位一次,如果我们正在快速移动中,怎样实时更新当前的位置?百度 LBS SDK 提供了一系列的设置方法, 来允许我们更改默认的行为,修改 MainActivity 中的代码
private void requestLocation() {
initLocation();
mLocationClient.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
mLocationClient.stop();
}
private void initLocation(){
LocationClientOption option = new LocationClientOption();
option.setScanSpan(5000);
mLocationClient.setLocOption(option);
}
增加了一个 initLocation()方法,在 initLocation()方法中我们创建了一个 LocationClientOption 对象,然后调用它的 setScanSpan()方法来设置更新的间隔。这里传 入了 5000,表示每 5 秒钟会更新一下当前的位置
在活动被销毁的时候一定要调用 LocationClient 的 stop()方法来停止定位, 不然程序会持续在后台不停地进行定位,从而严重消耗手机的电量
很好,没有用,我从房间一头走到另一头等了很久经纬度并没有改变,更改网络为wifi与流量也并不会改变,我不清楚原因
3.选择定位模式
GPS 定位功能必须要由用户主动去启用才行,不然任何应用程序都无法使用 GPS 获 取到手机当前的位置信息
不知道为什么我们的手机必须打开gps功能才能进行定位
高精确度模式表示允许使用 GPS、无线网络、蓝牙或移动网络来进行定位,节电模式 表示仅允许使用无线网络、蓝牙或移动网络来进行定位,而仅限设备模式表示仅允许使用 GPS 来进行定位。也就是说,如果我们想要使用 GPS 定位功能,这里必须要选择高精确度模式,或者仅限设备模式。
开启了 GPS 定位功能之后,再回来看一下代码。
可以在 initLocation()方法中对百度 LBS SDK 的定位模式进行指定,一共有 3 种模式可选:Hight_Accuracy、Battery_Saving 和 Device_Sensors。Hight_Accuracy 表示高精确度模式,会在 GPS 信号正常的情况下优先使用 GPS 定位,在无法接收 GPS 信号的时候使用网络定位。Battery_Saving 表示节电模式,只会使用网络进行定位。Device_Sensors 表示传感器模式,只会使用 GPS 进行定位。其中,Hight_Accuracy 是 默认的模式,也就是说,我们即使不修改任何代码,只要拿着手机走到室外去,让手机可以接收到 GPS 信号,就会自动切换到 GPS 定位模式了.
当然我们也可以强制指定只使用 GPS 进行定位,修改 MainActivity 中的代码
option.setLocationMode(LocationClientOption.LocationMode.Device_Sensors);
没用,甚至关闭了网络就会什么都不显示,就好像这个设置根本没有设置进去一样
<service
android:name="com.baidu.location.f"
android:enabled="true"
android:process=":remote"/>
需要在Manifest.xml里添加这个,刚开始还是网络,过了一会就变成gps了
但是关闭位置服务之后一段时间再打开就似乎无法再次获取位置了
4.看得懂的位置信息
经纬度的值一般人是根本看不懂的,相信谁也无法立刻答出南纬 25 度、东经 148 度是什么地方
为了能够更加直观地阅读,我们还需要学习一下如何获取看得懂的位置信息
修改 MainActivity 中的代码
private void initLocation(){
LocationClientOption option = new LocationClientOption();
option.setScanSpan(1000);
option.setIsNeedAddress(true);
option.setLocationMode(LocationClientOption.LocationMode.Device_Sensors);
mLocationClient.setLocOption(option);
mLocationClient.start();
}
currentPosition.append("纬度:").append(bdLocation.getLatitude()).append("\n");
currentPosition.append("经线:").append(bdLocation.getLongitude()).append("\n");
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");
currentPosition.append("定位方式:");
if (bdLocation.getLocType() == BDLocation.TypeGpsLocation) {
currentPosition.append("GPS");
} else if (bdLocation.getLocType() == BDLocation.TypeNetWorkLocation) {
currentPosition.append("网络");
}
positionText.setText(currentPosition);
在 initLocation()方法中,我们调用了 LocationClientOption 的 setIsNeedAddress() 方法,并传入 true,这就表示我们需要获取当前位置详细的地址信息。 接下来在 MyLocationListener 的 onReceiveLocation()方法就可以获取到各种丰富的地址 信息了,调用 getCountry()方法可以得到当前所在国家,调用 getProvince()方法可以得到当 前所在省份,以此类推。
获取地址信息一定需要用到网络,我们将定位模式指定成了 Device_Sensors,也会自动开启网络定位功能
3.使用百度地图
1.让地图显示出来
我们已经将 LBS SDK 全部准备好了,其中就包括了地图功能,因此这里就 不用再去下载百度地图的 SDK 了。 那么我们直接在 LBSTest 项目的基础上进行开发,修改 activity_main.xml 中的代码
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"/>
<com.baidu.mapapi.map.MapView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/bmapView"
android:clickable="true"/>
这里在布局文件中新放置了一个 MapView 控件,并让它填充满整个屏幕。这个 MapView 是 由百度提供的自定义控件,所以在使用它的时候需要将完整的包名加上。另外,之前用于显示定 位信息的 TextView 现在暂时用不到了,我们将它的 visibility 属性指定成 gone,让它在界面 上隐藏起来
接下来修改 MainActivity 中的代码
private MapView mapView;
SDKInitializer.setAgreePrivacy(getApplicationContext(),true);//一定在其他SDK调用之前,表示同意隐私政策
SDKInitializer.initialize(getApplicationContext());
mapView = findViewById(id.bmapView);
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
}
@Override
protected void onDestroy() {
mLocationClient.stop();
mapView.onDestroy();
super.onDestroy();
}
首先调用 SDKInitializer 的 initialize()方法来进行初始化操作,initialize()方法接收一个 Context 参数,这里我们调用 getApplicationContext()方法来获取一个全局的 Context 参数并传入。
初始化操作一定要在 setContentView()方法前调用,不然的话就会出错。接下来调用 findViewById()方法获取到了 MapView 的实例
重写 onResume()、onPause()和 onDestroy()这 3 个方法,在这里对 MapView 进行管理,以保证资源能够及时地得到释放。
如果是黑的
请使以下三条代码紧靠onCreate方法最上方
super.onCreate(savedInstanceState);
SDKInitializer.setAgreePrivacy(getApplicationContext(),true);
SDKInitializer.initialize(getApplicationContext());
2.移动到我的位置
百度 LBS SDK 的 API 中提供了一个 BaiduMap 类,它是地图的总控制器,调用 MapView 的 getMap()方法就能获取到 BaiduMap 的实例
加入“移动到我的位置”这个功能。修改 MainActivity 中的代码
private BaiduMap baiduMap;
private boolean isFirstLocate = true;
SDKInitializer.setAgreePrivacy(getApplicationContext(),true);
mapView = findViewById(id.bmapView);
SDKInitializer.initialize(getApplicationContext());
baiduMap = mapView.getMap();
private void navigateTo(BDLocation location){
if (isFirstLocate){
LatLng ll = new LatLng(location.getLatitude(),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) {
navigateTo(bdLocation);
}
}
}
这里并没有新增多少代码,主要是加入了一个 navigateTo()方法。
这个方法先是将 BDLocation 对象中的地理位置信息取出并封装到 LatLng 对象中,
调用 MapStatusUpdateFactory 的 newLatLng()方法并将 LatLng 对象传入,
将返回的 MapStatusUpdate 对象作为参数传入到 BaiduMap 的 animateMapStatus()方法当中,
这里为了让地图信息可以显示得更加丰富一些,我们将缩放级别设置成了 16。
上述代码当中我们使用了一个 isFirstLocate 变量,这个变量的作用是为了防止多次调用 animateMapStatus()方法,因为将地图移动到我们当前的位置只需要在程序第一次定位的时候调用一次就可以了。
写好了 navigateTo()方法之后,当定位到设备当前位置的时候,在 onReceiveLocation()方法中直接把 BDLocation 对象传给 navigateTo()方法,这样就能够让地图移动到设备所在的位置了
3.让“我”显示在地图上
百度 LBS SDK 当中提供了一个 MyLocationData.Builder 类,这个类是用来封装设备当前 所在位置的,我们只需将经纬度信息传入到这个类的相应方法当中就可以了
MyLocationData.Builder 类还提供了一个 build()方法,当我们把要封装的信息都设置完 成之后,只需要调用它的 build()方法,就会生成一个 MyLocationData 的实例,然后再将这个实例传入到 BaiduMap 的 setMyLocationData()方法当中,就可以让设备当前的位置显示在地图上了,
修改 MainActivity 中的代码
mapView = findViewById(id.bmapView);
baiduMap = mapView.getMap();
baiduMap.setMyLocationEnabled(true);
private void navigateTo(BDLocation location){
if (isFirstLocate){
LatLng ll = new LatLng(location.getLatitude(),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() {
mLocationClient.stop();
mapView.onDestroy();
baiduMap.setMyLocationEnabled(false);
super.onDestroy();
}
在 navigateTo()方法中添加了 MyLocationData 的构建逻辑,将 Location 中包含的经度和纬度分别封装到了 MyLocationData.Builder 当中,最后把 MyLocationData 设置到了 BaiduMap 的 setMyLocationData()方法当中。
这段逻辑必须写在 isFirstLocate 这个 if 条件语句的外面,因为让地图移动到我们当前的位置只需要在第一次定位的时候执行,但是设备在地图上显示的位置却应该是随着设备的移动而实时改变的。
根据百度地图的限制,如果我们想要使用这一功能,一定要事先调用 BaiduMap 的 setMyLocationEnabled()方法将此功能开启,否则设备的位置将无法在地图上显示。
在程序退出的时候,也要记得将此功能给关闭掉。