admin 管理员组文章数量: 887019
高德地图API使用详解
- 前言
- 正文
- 一、创建应用
- ① 获取PackageName
- ② 获取调试版安全码SHA1
- ③ 获取发布版安全码SHA1
- 二、配置Android Studio工程
- ① 导入SDK
- ② 配置AndroidManifest.xml
- ③ 隐私政策设置
- 三、获取当前定位信息
- ① 版本判断
- ② 动态权限请求
- ③ 初始化定位
- ④ 获取定位结果
- 四、显示地图
- 五、显示当前定位地图
- 六、地图设置
- ① 修改自定义定位图标
- ② 设置缩放等级
- ③ 开启室内地图
- ④ 地图控件设置
- 七、获取POI数据
- 八、地图点击长按事件
- ① 逆地理编码
- ② 地理编码
- ③ 添加标点Marker
- ④ 删除标点Marker
- ⑤ 绘制动画效果Marker
- ⑥ Marker的点击和拖拽事件
- ⑦ 绘制 InfoWindow
- ⑧ InfoWindow的点击事件
- ⑨ 改变地图中心点
- 九、出行路线规划
- ① 准备工作
- ② 步行路线规划
- ③ 骑行路线规划
- ④ 驾车路线规划
- ⑤ 公交路线规划
- ⑥ 步行路线详情
- ⑦ 骑行路线详情
- ⑧ 驾车路线详情
- ⑨ 公交路线详情
- ⑩ 手动输入目的地和出发地
- 十、源码
- 尾声
前言
为了丰富对地图、定位的了解,在写了百度、腾讯的地图定位之后,终于到了高德地图了。我个人平时日常使用最多的定位软件就是高德地图。所以这篇文章,就带你来初步了解使用。此版本比较老,建议查看新版本文章:Android 高德地图API(新版)
正文
在此之前呢,默认都没有账号,因此需要先注册一个账号才行,点击高德开放平台进入主页面。
点击右上角的注册按钮,然后选择注册开发者的类型,有个人开发者、企业开发者。
如果你已经工作了可以选择企业开发者,只不过需要填写的资料要多一些,这里我选择成为个人开发者,点击。
勾选上这个选项,然后点击确认注册开发者,进入下一步,填写个人资料。
填写个人资料,实名认证,请注意实名认证,一个身份只能认证一个开发者账号,后面个人主页和开发者简介属于选填的。
点击下一步就注册完成了。然后重新进入高德开放平台登录你刚才注册的账号,之后进入控制台选择应用管理,进入我的应用。
一、创建应用
然后点击创建新应用按钮
填写应用信息,然后点击新建。
建好应用之后需要添加key,点击添加。
添加key需要你的应用的一些信息,你可以和我的这个图一样。
从上面的图来看还需要三个值,发布版安全码SHA1、调试版安全码SHA1、PackageName。
① 发布版安全码SHA1就是你的应用发布正式版本时的安全码,常规是使用jks秘钥来生成release包。
② 调试版安全码SHA1就是你的应用通过usb直接运行在手机或者虚拟机时的安全码,同一个项目在不同的电脑上运行,这个安全码各不相同。
③ PackageName 就是你的应用包名。
① 获取PackageName
下面我将在一一获取这三个值,保留当前的网页,然后打开你的Android Studio,新建一个名为GaodeMapDemo的项目,如下图所示
注意这个包名,现在你就可以复制这个包名到刚才的网页中的输入框中,位置如下图所示,key的包名需要和你的应用包名保持一致。
然后回到AS,点击Finish完成你的项目的创建。
② 获取调试版安全码SHA1
下面来获取调试版安全码SHA1
创建好项目之后,注意你的AS的右侧边栏,点击Gradle → app → android → signingReport,最后双击signingReport。
当你的AS版本为4.2.1时,你会发现这里好像有点不一样了,找不到signingReport了,这是AS更新之后默认这个功能关掉了,需要去手动打开它。在设置里面
如下图这里默认勾选上了,现在把这根勾选上的去掉。
然后Sync,或者点击这个小图标。
然后你的项目的右侧边栏就会出现这个熟悉的task了。
然后你会看到Run下会出现如下图类似的信息。
而当你的Android Studio是Android Studio Dolphin | 2021.3.1以及以上版本时,你就需要使用下面这种方式来获取开发版SHA1。你需要先在环境变量中配置Java JDK版本,然后再使用指令,配置JDK的话就用11或以上版本就可以了,JDK的安装配置就不用我再多说了吧。下面进行调试版SHA1的获取,Win + R ,输入cmd,进入命令窗口。
输入
cd .android
回车
先切换到.android目录下,然后输入
keytool -list -v -keystore debug.keystore
回车之后会让你输入密钥,默认的密钥就是android,你输入的时候是不可见的,光标也不会有反应,你只管输入就行,输入完回车就能看到SHA1了,如下图所示。
debug就是调试版,那么它的SHA1就是调试版安全码SHA1,这正是我所需要的。下面将它复制到刚才的网页中,如下图所示,别放错位置了。
最后来获取发布版安全码SHA1。
③ 获取发布版安全码SHA1
这个稍微有一些麻烦,因此需要先创建一个jks文件才行。
点击Build 选择点击Generate Signed Bundle / APK…
选择APK,然后Next。
然后是配置,这里需要填写jks的路径,但是我没有这个jks,因此点击**Create new…**按钮去创建一个。
首先要指定这个jks的文件存放路径和文件名。
这里我存放在D盘下的APK文件夹中,然后设置jks的名字为GaodeMapDemo,然后点击OK。
输入jks的密码和key的密码,最后点击OK。
会弹出这样一个窗口,不用管它,点击OK。(新版本没有这个弹窗了)
勾选记住密码,然后点击Next。
选择release,然后两个都勾选上,最后点击Finish。
当你看到AS的右下角出现这一提示时,就说明你的release包已经生成好了。
在你的AS中查看这个apk,你可以复制它通过电脑QQ发给你的手机,然后在手机上直接打开安装。
然后你就能看到熟悉的Hello World!接下来进入Android Studio的Terminal工具输入如下图所示的命令和jks存放路径。
//如果你前面的步骤和我一模一样的话,这里你就可以复制粘贴,不一样的话就修改jks的路径就可以了
keytool -list -v -keystore D:\APK\GaodeMapDemo.jks
输入密码,输入时不可见。输入完直接回车,然后你会看到如下图类似的信息。
复制到网页中,位置如下图所示,点击提交。
这个key就生成好了。
二、配置Android Studio工程
这里我们需要用到高德的SDK,点击 开发支持 → Android 地图SDK。
点击相关下载,找到合包下载,选择3D地图合包。
里面包含了3D地图SDK、搜索SDK、定位SDK,相对来说比较的全面,点击下载。下载到本地后解压,解压之后如下所示:
① 导入SDK
复制这些文件到你的工程的libs下,如果没有找到libs目录,就将项目结构从Android切换到Project就能找到了,放进去之后检查一下你的app的build.gradle中的dependencies{}闭包下有没有这样一样代码:
implementation fileTree(dir: 'libs', include: ['*.jar'])
在之前的老版本Android Studio中这句话是默认有的,但是出于不是所有项目都需要这一行代码,就在后续版本中去掉了,而我们导入sdk就需要这样一行代码,现在将它添加到dependencies{}闭包下,如图所示。
然后你可以Sync Now或者点击这个小象图标进行工程的资源配置同步。
同步完成之后,你可以看到你的这个jar现在是可以打开的。
不过有一部分人这样做了之后,jar包没有发生任何变化,那么你可以采取另一种方式,鼠标右键点击这个jar包,然后选择Add as Library…,之后会出现一个弹窗,默认是添加到你当前的app模块,你点击OK即可。然后等待同步,你就会发现你的jar包展开了。
然后打开你的app下的build.gradle文件,在android闭包下添加。
sourceSets {
main{
jniLibs.srcDirs = ['libs']
}
}
然后点击Sync进行再一次同步,下面进行AndroidManifest.xml的配置。
② 配置AndroidManifest.xml
打开AndroidManifest.xml,首先在application标签下添加定位服务
<!--定位service-->
<service android:name="com.amap.api.location.APSService"/>
然后添加在manifest标签下添加如下权限。
<!--用于访问网络,网络定位需要上网-->
<uses-permission android:name="android.permission.INTERNET" />
<!--用于读取手机当前的状态-->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!--用于写入缓存数据到扩展存储卡-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--用于申请调用A-GPS模块-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!--用于进行网络定位-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!--用于访问GPS定位-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!--用于获取运营商信息,用于支持提供运营商信息相关的接口-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--用于访问wifi网络信息,wifi信息会用于进行网络定位-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!--用于获取wifi的获取权限,wifi信息会用来进行网络定位-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
最后在application标签下添加高德的访问key
<!--设置高德Key-->
<meta-data android:name="com.amap.api.v2.apikey" android:value="d3347ee0f2928f9a0c199cae009ae7f7"/>
这个值和你创建的key的值一致,如下图所示则是我的key,你要使用自己的Key。
③ 隐私政策设置
随着工信部推行加强个人信息隐私的保护政策,各大SDK都需要相应增加一个API接口,确保用户是在知道会采集信息的情况下使用某一个功能,所以我们在使用高德的定位、地图、搜索功能时,需要先同意隐私政策,不通过则无法使用,一般是通过App启动之后出现一个弹窗,弹窗中你告知用户那些信息被采集,用到了那些SDK,作用是什么。并且要有隐私政策的链接地址,很麻烦。这里我们就简单一些,在com.llw.mapdemo包下新建一个MapApplication类,里面的代码如下:
public class MapApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Context context = this;
//定位隐私政策同意
AMapLocationClient.updatePrivacyShow(context,true,true);
AMapLocationClient.updatePrivacyAgree(context,true);
//地图隐私政策同意
MapsInitializer.updatePrivacyShow(context,true,true);
MapsInitializer.updatePrivacyAgree(context,true);
//搜索隐私政策同意
ServiceSettings.updatePrivacyShow(context,true,true);
ServiceSettings.updatePrivacyAgree(context,true);
}
}
我们在项目初始化的时候就通过这些隐私政策,这不是最终的做法,如果你上架应用的时候这么做就会被驳回。为了使这个MapApplication生效,我们需要在AndroidManifest.xml中配置它。比较简单,如下图所示。
下面我们可以去写代码了。
三、获取当前定位信息
现在设想一个情景,我想要一打开应用就获取到当前定位信息。那么基于这个目的下面来写代码。刚才在AndroidManifest.xml配置了比较多的权限,而在实际的使用过程中,Android6.0之后有一些权限是需要用户动态申请的,比如定位、获取手机状态、文件读写之类的。
因此首先得先判断当前是否需要动态请求权限,所以要根据Android的版本来判断。
① 版本判断
于是就可以在MainActivity中写入这样一个checkingAndroidVersion()方法。
/**
* 检查Android版本
*/
private void checkingAndroidVersion() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
//Android6.0及以上先获取权限再定位
}else {
//Android6.0以下直接定位
}
}
用于检查Android版本。6.0以下则直接定位,6.0及以上则动态申请权限,然后在权限通过的返回中进行定位,思路很清晰了,下面你要在onCreate中调用这个 checkingAndroidVersion() 方法。
下面来写这个动态权限的请求,这个其实也没有必要用原生的权限请求,网络上有很多优秀的框架可以帮助你快速实现,比如这个EasyPermission,在之前我是没有讲述过这个权限请求框架的。下面来看看。
② 动态权限请求
打开app下的build.gradle,在dependencies闭包下添加如下依赖:
//Google推荐的EasyPermission库
implementation 'pub.devrel:easypermissions:3.0.0'
如下图所示
然后点击AS右上角的Sync进行同步,同步完成之后,你就可以使用了。回到MainActivity中,
先增加一个成员变量
//请求权限码
private static final int REQUEST_PERMISSIONS = 9527;
新增requestPermission()方法。
/**
* 动态请求权限
*/
@AfterPermissionGranted(REQUEST_PERMISSIONS)
private void requestPermission() {
String[] permissions = {
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
if (EasyPermissions.hasPermissions(this, permissions)) {
//true 有权限 开始定位
showMsg("已获得权限,可以定位啦!");
} else {
//false 无权限
EasyPermissions.requestPermissions(this, "需要权限", REQUEST_PERMISSIONS, permissions);
}
}
这个方法中对权限进行了判断,有权限则可以定位,没有权限则请求权限
/**
* 请求权限结果
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//设置权限请求结果
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
通过重写onRequestPermissionsResult方法来获取权限授予的返回结果,然后再通过EasyPermissions.onRequestPermissionsResult设置,设置之后会重新调用requestPermission()方法,为什么呢?就因为这个注解**@AfterPermissionGranted(REQUEST_PERMISSIONS)**,然后又会判断是否有权限,有则进行之后的定位,下面可以来写定位的方法了。
简单写一个消息提示的方法,用于当前Activity的使用。
/**
* Toast提示
* @param msg 提示内容
*/
private void showMsg(String msg){
Toast.makeText(this,msg,Toast.LENGTH_SHORT).show();
}
下面进行定位的初始化
③ 初始化定位
首先在MainActivity中新增两个成员变量
//声明AMapLocationClient类对象
public AMapLocationClient mLocationClient = null;
//声明AMapLocationClientOption对象
public AMapLocationClientOption mLocationOption = null;
然后新增一个initLocation()方法。
/**
* 初始化定位
*/
private void initLocation() {
//初始化定位
try {
mLocationClient = new AMapLocationClient(getApplicationContext());
} catch (Exception e) {
e.printStackTrace();
}
if (mLocationClient != null) {
//设置定位回调监听
mLocationClient.setLocationListener(this);
//初始化AMapLocationClientOption对象
mLocationOption = new AMapLocationClientOption();
//设置定位模式为AMapLocationMode.Hight_Accuracy,高精度模式。
mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
//获取最近3s内精度最高的一次定位结果:
//设置setOnceLocationLatest(boolean b)接口为true,启动定位时SDK会返回最近3s内精度最高的一次定位结果。如果设置其为true,setOnceLocation(boolean b)接口也会被设置为true,反之不会,默认为false。
mLocationOption.setOnceLocationLatest(true);
//设置是否返回地址信息(默认返回地址信息)
mLocationOption.setNeedAddress(true);
//设置定位请求超时时间,单位是毫秒,默认30000毫秒,建议超时时间不要低于8000毫秒。
mLocationOption.setHttpTimeOut(20000);
//关闭缓存机制,高精度定位会产生缓存。
mLocationOption.setLocationCacheEnable(false);
//给定位客户端对象设置定位参数
mLocationClient.setLocationOption(mLocationOption);
}
}
这里的代码相比之前的版本中,增加了捕获异常信息和调用之前先判空,这是官方需要这么做的,然后在onCreate方法中进行调用。
注意initLocation()的调用在checkingAndroidVersion()之前,你可能会问为什么,那是因为你想定位,那么首先要先配置一些参数,配置好之后才能启动定位,否则会报错,常见的就是空指针异常。而检查版本之后当有权限时,就直接定位了,而把这个方法放到checkingAndroidVersion()里面去并不是很明智,因为你可能要调用两次,这样明显浪费资源,因此这就是为什么它要在checkingAndroidVersion()方法之前调用的原因。你想不到有这么多门道吧。
至于initLocation()方法本身我也没有什么好解释的,因为注释已经写的非常明白了,下面还有比较的最后一步,那就是定位的发起和定位结果的获取了。
④ 获取定位结果
首先要发起定位才能获取定位结果。
那么可以这样写。首先要在Android6.0以下直接启动定位。这次定位配置已经完毕,所以你完全不用担心会报错。
其次在权限允许之后进行定位。
现在定位已经发起了,那么定位结果怎么获取呢?不知道你有没有注意到我在initLocation()方法中的这行代码。
mLocationClient.setLocationListener(this);
注意到这里传入的是this,则表明通过当前Activity来实现定位结果的监听,那么可以通过实现AMapLocationListener,重写里面的onLocationChanged方法来获取定位结果。
onLocationChanged方法如下:
/**
* 接收异步返回的定位结果
*
* @param aMapLocation
*/
@Override
public void onLocationChanged(AMapLocation aMapLocation) {
if (aMapLocation != null) {
if (aMapLocation.getErrorCode() == 0) {
//地址
String address = aMapLocation.getAddress();
} else {
//定位失败时,可通过ErrCode(错误码)信息来确定失败的原因,errInfo是错误信息,详见错误码表。
Log.e("AmapError", "location Error, ErrCode:"
+ aMapLocation.getErrorCode() + ", errInfo:"
+ aMapLocation.getErrorInfo());
}
}
}
现在已经拿到了定位的结果了,为了更直观,所以需要显示在当前的Activity中,那么下面修改布局activity_main.xml。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
xmlns:app="http://schemas.android/apk/res-auto"
xmlns:tools="http://schemas.android/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_content"
android:padding="20dp"
android:textSize="18sp"
android:textColor="#000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
</LinearLayout>
然后回到MainActivity,新增加一个成员变量。
//内容
private TextView tvContent;
然后在onCreate中,绑定控件id。它在initLocation()方法之前
tvContent = findViewById(R.id.tv_content);
然后在onLocationChanged中显示
这个我解释一下这个用法
address == null ? "无地址" : address
这个是三目运算,address == null 表示这个条件是否成立,如果成立则返回 “无地址”,不成立则返回 address,它的意思和下面这个if else一样。
if(address == null){
tvContent.setText("无地址");
}else {
tvContent.setText(address);
}
熟练了你就知道怎么使用了。它会比你使用if else 来更简洁,不是吗?
OK,写了这么多代码也该要运行一下了吧。
可以看到第一次打开时提醒你需要权限,然后进行授权,授权后获得定位显示定位数据,之前我关闭当前应用,再一次进入,此时已经有了权限,则直接进行定位,然后显示定位数据。如果要显示经纬度的话还可以再修改一下。
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("纬度:" + latitude + "\n");
stringBuffer.append("经度:" + longitude + "\n");
stringBuffer.append("地址:" + address + "\n");
tvContent.setText(stringBuffer.toString());
再运行一下:
当然,获取定位数据的信息还不止这些,还有如下这些,感兴趣可以自行去尝试。
aMapLocation.getLocationType();//获取当前定位结果来源,如网络定位结果,详见定位类型表
aMapLocation.getAccuracy();//获取精度信息
aMapLocation.getCountry();//国家信息
aMapLocation.getProvince();//省信息
aMapLocation.getCity();//城市信息
aMapLocation.getDistrict();//城区信息
aMapLocation.getStreet();//街道信息
aMapLocation.getStreetNum();//街道门牌号信息
aMapLocation.getCityCode();//城市编码
aMapLocation.getAdCode();//地区编码
aMapLocation.getAoiName();//获取当前定位点的AOI信息
aMapLocation.getBuildingId();//获取当前室内定位的建筑物Id
aMapLocation.getFloor();//获取当前室内定位的楼层
aMapLocation.getGpsAccuracyStatus();//获取GPS的当前状态
还可以在获取信息之后,将定位给停止
//停止定位后,本地定位服务并不会被销毁
mLocationClient.stopLocation();
在页面销毁时同时销毁本地定位服务。
@Override
protected void onDestroy() {
super.onDestroy();
//销毁定位客户端,同时销毁本地定位服务。
mLocationClient.onDestroy();
}
四、显示地图
下面改变一下activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android/apk/res/android"
xmlns:app="http://schemas.android/apk/res-auto"
xmlns:tools="http://schemas.android/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:context=".MainActivity">
<!--地图-->
<com.amap.api.maps.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
我删除了TextView,改变了外部的父布局,进入到MainActivity中。按照下图进行改变,你可以将无用的代码删除掉。
然后增加地图生命周期的管理方法。
@Override
protected void onResume() {
super.onResume();
//在activity执行onResume时执行mMapView.onResume (),重新绘制加载地图
mapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
//在activity执行onPause时执行mMapView.onPause (),暂停地图的绘制
mapView.onPause();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//在activity执行onSaveInstanceState时执行mMapView.onSaveInstanceState (outState),保存地图当前的状态
mapView.onSaveInstanceState(outState);
}
下面重新运行一下,不出意外你就能看到地图了。
不出意外,你的页面会和我这个一毛一样,默认就是定位在北京。百度和腾讯也都是如此。
五、显示当前定位地图
很显然,默认的地图不能满足我们的要求,那么就需要开发者自行去设置了,比如我现在在深圳福田区,那么我就要定位到当前所在地这里才行,来看看要怎么做吧。
先在MainActivity中新增两个成员变量。
//地图控制器
private AMap aMap = null;
//位置更改监听
private OnLocationChangedListener mListener;
然后新增一个initMap()方法,用于初始化地图。
/**
* 初始化地图
* @param savedInstanceState
*/
private void initMap(Bundle savedInstanceState) {
mapView = findViewById(R.id.map_view);
//在activity执行onCreate时执行mMapView.onCreate(savedInstanceState),创建地图
mapView.onCreate(savedInstanceState);
//初始化地图控制器对象
aMap = mapView.getMap();
// 设置定位监听
aMap.setLocationSource(this);
// 设置为true表示显示定位层并可触发定位,false表示隐藏定位层并不可触发定位,默认是false
aMap.setMyLocationEnabled(true);
}
同样要在onCreate中调用。
同时,MainActivity要实现LocationSource。
然后重写里面的两个方法在activate()和deactivate(),代码如下:
/**
* 激活定位
*/
@Override
public void activate(OnLocationChangedListener onLocationChangedListener) {
mListener = onLocationChangedListener;
if (mLocationClient != null) {
mLocationClient.startLocation();//启动定位
}
}
/**
* 停止定位
*/
@Override
public void deactivate() {
mListener = null;
if (mLocationClient != null) {
mLocationClient.stopLocation();
mLocationClient.onDestroy();
}
mLocationClient = null;
}
然后只要在定位结果的回调中,进行数据源的更改就可以实现地图的定位了。
下面运行一下吧。
可以看到已经到了福田区,只不过这个比例尺有点小,你可以点击右侧的 + 按钮放大一些,就能看得清楚些。
六、地图设置
① 修改自定义定位图标
修改定位图标,这里就需要一个样式配置。在MainActivity中新增
//定位样式
private MyLocationStyle myLocationStyle = new MyLocationStyle();
然后在initMap()方法中配置。
// 自定义定位蓝点图标
myLocationStyle.myLocationIcon(BitmapDescriptorFactory.fromResource(R.drawable.gps_point));
// 自定义精度范围的圆形边框颜色 都为0则透明
myLocationStyle.strokeColor(Color.argb(0, 0, 0, 0));
// 自定义精度范围的圆形边框宽度 0 无宽度
myLocationStyle.strokeWidth(0);
// 设置圆形的填充颜色 都为0则透明
myLocationStyle.radiusFillColor(Color.argb(0, 0, 0, 0));
//设置定位蓝点的Style
aMap.setMyLocationStyle(myLocationStyle);
② 设置缩放等级
然后再修改缩放等级。
//设置最小缩放等级为16 ,缩放级别范围为[3, 20]
aMap.setMinZoomLevel(12);
最终设置如下图所示:
下面运行一下:
OK,这就改好了。
③ 开启室内地图
aMap.setMinZoomLevel(20);
//开启室内地图
aMap.showIndoorMap(true);
运行
④ 地图控件设置
//定义一个UiSettings对象
private UiSettings mUiSettings;
隐藏缩放按钮。
//实例化UiSettings类对象
mUiSettings = aMap.getUiSettings();
//隐藏缩放按钮
mUiSettings.setZoomControlsEnabled(false);
比例尺控件。
//显示比例尺 默认不显示
mUiSettings.setScaleControlsEnabled(true);
七、获取POI数据
POI (Point of Interest,兴趣点)。在地图表达中,一个 POI 可代表一栋大厦、一家商铺、一处景点等等。通过POI搜索,完成找餐馆、找景点、找厕所等等的功能。首先,先在app的build.gradle中添加依赖。
//Material库
implementation 'com.google.android.material:material:1.6.1'
然后修改activity_main.xml。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android/apk/res/android"
xmlns:app="http://schemas.android/apk/res-auto"
xmlns:tools="http://schemas.android/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!--地图-->
<com.amap.api.maps.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!--浮动按钮-->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_poi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:clickable="true"
android:onClick="queryPOI"
android:src="@drawable/icon_favorite_red"
android:visibility="gone"
app:backgroundTint="#FFF"
app:backgroundTintMode="screen"
app:hoveredFocusedTranslationZ="18dp"
app:pressedTranslationZ="18dp" />
</RelativeLayout>
浮动按钮中有一个图标,在res/drawable下新建一个icon_favorite_red.xml。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#EF5350"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M13.35,20.13c-0.76,0.69 -1.93,0.69 -2.69,-0.01l-0.11,-0.1C5.3,15.27 1.87,12.16 2,8.28c0.06,-1.7 0.93,-3.33 2.34,-4.29 2.64,-1.8 5.9,-0.96 7.66,1.1 1.76,-2.06 5.02,-2.91 7.66,-1.1 1.41,0.96 2.28,2.59 2.34,4.29 0.14,3.88 -3.3,6.99 -8.55,11.76l-0.1,0.09z" />
</vector>
回到MainActivity。
//POI查询对象
private PoiSearch.Query query;
//POI搜索对象
private PoiSearch poiSearch;
//城市码
private String cityCode = null;
//浮动按钮
private FloatingActionButton fabPOI;
然后在onCreate中。
fabPOI = findViewById(R.id.fab_poi);
然后在onLocationChanged进行赋值显示。
然后添加一个queryPOI()方法,这个方法对应了xml中浮动按钮的onClick的值。
/**
* 浮动按钮点击查询附近POI
*
* @param view
*/
public void queryPOI(View view) {
//构造query对象
query = new PoiSearch.Query("购物", "", cityCode);
// 设置每页最多返回多少条poiitem
query.setPageSize(10);
//设置查询页码
query.setPageNum(1);
//构造 PoiSearch 对象
poiSearch = new PoiSearch(this, query);
//设置搜索回调监听
poiSearch.setOnPoiSearchListener(this);
//发起搜索附近POI异步请求
poiSearch.searchPOIAsyn();
}
在这个方法里面对query和poiSearch进行配置,然后发起搜索附近POI异步请求。
下面就要实现PoiSearch.OnPoiSearchListener。
然后重写里面的onPoiSearched和onPoiItemSearched,方法如下:
/**
* POI搜索返回
*
* @param poiResult POI所有数据
* @param i
*/
@Override
public void onPoiSearched(PoiResult poiResult, int i) {
//解析result获取POI信息
//获取POI组数列表
ArrayList<PoiItem> poiItems = poiResult.getPois();
for (PoiItem poiItem : poiItems) {
Log.d("MainActivity", " Title:" + poiItem.getTitle() + " Snippet:" + poiItem.getSnippet());
}
}
/**
* POI中的项目搜索返回
*
* @param poiItem 获取POI item
* @param i
*/
@Override
public void onPoiItemSearched(PoiItem poiItem, int i) {
}
下面运行之后点击一下这个右下角的浮动按钮。
数据就打印出来了。
八、地图点击长按事件
实际开发中都会对地图的点击和长按做处理,比如点击某一个地方获取经纬度,下面来操作一下吧。
在initMap()方法中,添加对地图点击和长按的监听。然后实现AMap.OnMapClickListener和AMap.OnMapLongClickListener。
之后重写两个方法,如下所示。
/**
* 地图单击事件
* @param latLng
*/
@Override
public void onMapClick(LatLng latLng) {
showMsg("点击了地图,经度:"+latLng.longitude+",纬度:"+latLng.latitude);
}
/**
* 地图长按事件
* @param latLng
*/
@Override
public void onMapLongClick(LatLng latLng) {
showMsg("长按了地图,经度:"+latLng.longitude+",纬度:"+latLng.latitude);
}
可以看到我在点击和长按的监听中弹出Toast显示经纬度信息,这是通过LatLng对象获取的,下面运行一下。
可看到成功获取到了经纬度,但是这就够了吗?用户又看不懂,那么怎么样让用户知道自己点击的是那里呢?那就是要把经纬度进行一次转换,转换成实际的地址描述。在高德中这种坐标转地址称之为逆地理编码。
① 逆地理编码
上面已经说过了,逆地理编码就是将坐标转为地址,坐标刚才已经拿到了,就是经纬度,下面来转换一下吧,首先在MainActivity中创建两个对象。
//地理编码搜索
private GeocodeSearch geocodeSearch;
//解析成功标识码
private static final int PARSE_SUCCESS_CODE = 1000;
然后在initMap()中构建对象,然后设置监听。
这里同样要捕获异常,不然会报错的,之后实现这个GeocodeSearch.OnGeocodeSearchListener。
重写里面的两个方法。
/**
* 坐标转地址
* @param regeocodeResult
* @param rCode
*/
@Override
public void onRegeocodeSearched(RegeocodeResult regeocodeResult, int rCode) {
}
/**
* 地址转坐标
* @param geocodeResult
* @param rCode
*/
@Override
public void onGeocodeSearched(GeocodeResult geocodeResult, int rCode) {
}
一个是地址转坐标,一个是坐标转地址。当前我们需要将坐标转地址,所以目前重点关注这个onRegeocodeSearched方法。
既然是坐标转地址,那么肯定要先拿到坐标,刚才的地图点击的监听中我们已经拿到了坐标,于是你就可以写出这样的一个方法:
/**
* 通过经纬度获取地址
* @param latLng
*/
private void latlonToAddress(LatLng latLng) {
//位置点 通过经纬度进行构建
LatLonPoint latLonPoint = new LatLonPoint(latLng.latitude, latLng.longitude);
//逆编码查询 第一个参数表示一个Latlng,第二参数表示范围多少米,第三个参数表示是火系坐标系还是GPS原生坐标系
RegeocodeQuery query = new RegeocodeQuery(latLonPoint, 20, GeocodeSearch.AMAP);
//异步获取地址信息
geocodeSearch.getFromLocationAsyn(query);
}
通过经纬度构建LatLonPoint对象,然后构建RegeocodeQuery时,传入,并且输入另外两个参数,范围和坐标系。最后通过geocodeSearch发起一个异步的地址获取请求。
@Override
public void onRegeocodeSearched(RegeocodeResult regeocodeResult, int rCode) {
//解析result获取地址描述信息
if(rCode == PARSE_SUCCESS_CODE){
RegeocodeAddress regeocodeAddress = regeocodeResult.getRegeocodeAddress();
//显示解析后的地址
showMsg("地址:"+regeocodeAddress.getFormatAddress());
}else {
showMsg("获取地址失败");
}
}
然后在返回值中,进行判断处理,通过Toast显示地址信息。最后别忘了在地图的点击和长按监听中调用这个latlonToAddress()
方法。
下面运行一下。
② 地理编码
上面说了逆地理编码,下面来说说地理编码,地理编码就是地址转坐标,那么它的使用场景是怎么样的呢?比如说你到一个景点去游玩,不知道路线只知道景点名,那么这个时候通常你会在导航软件中输入这个景点名,然后搜索出前往的路线及搭乘的交通工具。此时,导航软件会将你输入的地址转成经纬度坐标,然后通过你当前的所在地坐标计算距离,获取两点之间的交通情况,然后规划路线,是不是脑瓜子嗡嗡的,怎么导航还有这么多门道吗?其实我说的还算简单了,里面的步骤还会有很多的细化过程,好了,当前的重点不是这个地理编码吗?下面我也模仿一下,通过输入框输入地址,然后得出它的经纬度坐标。
现在屏幕的空间已经不多了,所在在不影响地图显示的情况下,我打算改变一下样式。
首先修改activity_main.xml。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android/apk/res/android"
xmlns:app="http://schemas.android/apk/res-auto"
xmlns:tools="http://schemas.android/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimaryDark"
android:paddingTop="8dp"
android:paddingEnd="12dp"
android:paddingBottom="8dp">
<EditText
android:id="@+id/et_address"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/et_bg"
android:hint="请输入地址"
android:imeOptions="actionSearch"
android:paddingStart="12dp"
android:singleLine="true"
android:textColor="#000"
android:textSize="14sp" />
</androidx.appcompat.widget.Toolbar>
<!--地图-->
<com.amap.api.maps.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar" />
<!--浮动按钮-->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_poi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:clickable="true"
android:onClick="queryPOI"
android:src="@drawable/icon_favorite_red"
android:visibility="gone"
app:backgroundTint="#FFF"
app:backgroundTintMode="screen"
app:hoveredFocusedTranslationZ="18dp"
app:pressedTranslationZ="18dp" />
</RelativeLayout>
我去掉了默认的ActionBar,改用Toolbar,然后在Toolbar中放了一个输入框,修改键盘的回车为搜索文字,下面进去MainActivity。
//输入框
private EditText etAddress;
onCreate中。
实现EditText.OnKeyListener。
重写onKey方法。
/**
* 键盘点击
*
* @param v
* @param keyCode
* @param event
* @return
*/
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
//获取输入框的值
String address = etAddress.getText().toString().trim();
if (address == null || address.isEmpty()) {
showMsg("请输入地址");
}else {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
//隐藏软键盘
imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
// name表示地址,第二个参数表示查询城市,中文或者中文全拼,citycode、adcode
GeocodeQuery query = new GeocodeQuery(address,"深圳");
geocodeSearch.getFromLocationNameAsyn(query);
}
return true;
}
return false;
}
在键盘按键监听时,监听是点击回车键,同时判断是否为抬起,因为按键是两个动作,按下和抬起,如果不判断就出触发两次事件,然后判断输入是否为空,不为空则隐藏软键盘,构建GeocodeQuery对象,这里有一个地址,还有一个城市,而这个城市的值在实际开发中应该是从用户数据的地点一步一步进行排查,比如先从区/县进行,没有则到市,再没有则到省,然后是全国,获取最接近当前输入地址的所在区域。而这里就没有必要那么麻烦了,因此我就写死了值为深圳,因为重点不是这个城市的问题,而是地理编码的问题。
不过为了更灵活的使用,这里我们定义一个变量。
//城市
private String city;
然后在onLocationChanged方法中赋值。
//城市赋值
city = aMapLocation.getCity();
再去方法中改变“深圳”为 city,如下图。
这样就更灵活,也避免了一些不看文章直接运行源码,然后告诉我出问题找不到问题所在的尴尬局面。
下面进入到onGeocodeSearched方法。
@Override
public void onGeocodeSearched(GeocodeResult geocodeResult, int rCode) {
if (rCode == PARSE_SUCCESS_CODE) {
List<GeocodeAddress> geocodeAddressList = geocodeResult.getGeocodeAddressList();
if(geocodeAddressList!=null && geocodeAddressList.size()>0){
LatLonPoint latLonPoint = geocodeAddressList.get(0).getLatLonPoint();
//显示解析后的坐标
showMsg("坐标:" + latLonPoint.getLongitude()+","+latLonPoint.getLatitude());
}
} else {
showMsg("获取坐标失败");
}
}
代码也是一目了然,通过返回值获取编码地址列表,判断不为空并且大于0则取第一条数据,然后获取经纬度的值显示出来。运行效果图如下所示:
③ 添加标点Marker
通常使用地图是会对地图进行标注,添加标点。刚才通过点击地图获取到了经纬度,那么同样可以根据这个经纬度在地图上绘制标点。
那么其实也很简单,下面在onMapClick方法中添加如下代码:
//添加标点
aMap.addMarker(new MarkerOptions().position(latLng).snippet("DefaultMarker"));
运行一下看会怎么样。
但是你会添加标点也要会删除才行。
④ 删除标点Marker
修改一下布局的代码。
<!--浮动按钮 获取poi-->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_poi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:clickable="true"
android:onClick="queryPOI"
app:fabSize="mini"
android:src="@drawable/icon_favorite_red"
android:visibility="invisible"
app:backgroundTint="#FFF"
app:backgroundTintMode="screen"
app:hoveredFocusedTranslationZ="18dp"
app:pressedTranslationZ="18dp" />
<!--浮动按钮 清空marker-->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_clear_marker"
android:layout_width="wrap_content"
android:layout_above="@+id/fab_poi"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginEnd="20dp"
android:clickable="true"
android:onClick="clearAllMarker"
app:fabSize="mini"
android:src="@drawable/icon_clear"
app:backgroundTint="#FFF"
android:visibility="invisible"
app:backgroundTintMode="screen"
app:hoveredFocusedTranslationZ="18dp"
app:pressedTranslationZ="18dp" />
图标
这里我改变了浮动按钮的大小,然后增加了一个删除标点的按钮,当点击地图时显示这个浮动按钮,然后点击按钮时清空地图,当然这个清空要稍微麻烦一点,特别是你地图上有多个标点的时候。
//浮动按钮 清空地图标点
private FloatingActionButton fabClearMarker;
//标点列表
private List<Marker> markerList = new ArrayList<>();
写一个添加地图标点的方法。
/**
* 添加地图标点
*
* @param latLng
*/
private void addMarker(LatLng latLng) {
//显示浮动按钮
fabClearMarker.show();
//添加标点
Marker marker = aMap.addMarker(new MarkerOptions().position(latLng).snippet("DefaultMarker"));
markerList.add(marker);
}
在地图点击时调用。
然后新写一个clearAllMarker方法。
/**
* 清空地图Marker
*
* @param view
*/
public void clearAllMarker(View view) {
if (markerList != null && markerList.size()>0){
for (Marker markerItem : markerList) {
markerItem.remove();
}
}
fabClearMarker.hide();
}
清空标点,下面运行一下看看效果。
OK,就是这样的。
⑤ 绘制动画效果Marker
在addMarker方法中,添加如下代码:
//设置标点的绘制动画效果
Animation animation = new RotateAnimation(marker.getRotateAngle(),marker.getRotateAngle()+180,0,0,0);
long duration = 1000L;
animation.setDuration(duration);
animation.setInterpolator(new LinearInterpolator());
marker.setAnimation(animation);
marker.startAnimation();
添加位置如下图所示:
上面的代码要注意导包的问题,不是Android自带的包而是高德SDK里面的。
这段代码的意思就是配置一个旋转动画,然后设置旋转的角度和旋转所需要的时间,之后设置给marker。就可以,下面来看看效果吧。
这个动画是逆时针的,可以自己根据需要的效果进行更改。
当然可能这一个动画并不能满足你的需求,SDK中还提供了其他的,比如缩放动画、位移动画、透明度动画、渐变动画。它们都继承自Animation。
可以根据里面的参数进行配置然后达到你要的效果,那么就Marker的绘制动画效果就说到这,如果你有需要我用代码说明其他动画的需求,可以评论一下,我根据你的需求加上去。
⑥ Marker的点击和拖拽事件
先来看看Marker的点击事件,实现AMap.OnMarkerClickListener。
然后在initMap()方法中配置。
之后重写onMarkerClick方法:
/**
* Marker点击事件
* @param marker
* @return
*/
@Override
public boolean onMarkerClick(Marker marker) {
showMsg("点击了标点");
return true;
}
下面运行试一下:
点击事件就写好了,至于点击之后你要做什么,就看你的需求了。
下面就是拖拽事件了,实现AMap.OnMarkerDragListener。
依然在initMap中设置。
然后实现方法这里有三个方法需要重写。
/**
* 开始拖动
* @param marker
*/
@Override
public void onMarkerDragStart(Marker marker) {
Log.d(TAG,"开始拖动");
}
/**
* 拖动中
* @param marker
*/
@Override
public void onMarkerDrag(Marker marker) {
Log.d(TAG,"拖动中");
}
/**
* 拖动完成
* @param marker
*/
@Override
public void onMarkerDragEnd(Marker marker) {
Log.d(TAG,"拖动完成");
}
我在三个方法中都打印了日志,还有一步别忘记了,增加marker的可拖动属性,默认为false。
下面运行一下:
来看看日志。
OK,这样就可以了。
⑦ 绘制 InfoWindow
标点也是可以携带一些信息的,而这个信息可以由InfoWindow(信息窗体)展示处理出来。
首先应该显示出来这个infoWindow,上面我们写了这个Marker的点击事件,那么可以在点击的时候显示InfoWindow,再点击就显示。
现在addMarker方法中设置InfoWindow中信息的信息。
然后在onMarkerClick方法中,通过marker.isInfoWindowShown()判断当前Marker的InfoWindow是否显示,之后通过showInfoWindow来显示,hideInfoWindow来隐藏。
/**
* Marker点击事件
*
* @param marker
* @return
*/
@Override
public boolean onMarkerClick(Marker marker) {
//showMsg("点击了标点");
//显示InfoWindow
if (!marker.isInfoWindowShown()) {
//显示
marker.showInfoWindow();
} else {
//隐藏
marker.hideInfoWindow();
}
return true;
}
然后运行一下:
如果你希望在绘制Marker的时候就出现这个InfoWindow,你可以这样做。
自行运行一下即可。
刚才是使用了自带的样式,其实InfoWindow是可以自己定义样式的,首先添加两个图片。
建议在我的源码里面复制,直接在博客中保存图片会有问题,下面在layout下创建两个xml。
custom_info_contents.xml。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<ImageView
android:id="@+id/badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:adjustViewBounds="true" >
</ImageView>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:ellipsize="end"
android:singleLine="true"
android:textColor="#ff000000"
android:textSize="14dp"
android:textStyle="bold" />
<TextView
android:id="@+id/snippet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="#ff7f7f7f"
android:textSize="14dp" />
</LinearLayout>
</LinearLayout>
custom_info_window.xml。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@mipmap/custom_info_bubble"
android:orientation="horizontal" >
<ImageView
android:id="@+id/badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="5dp" >
</ImageView>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:ellipsize="end"
android:singleLine="true"
android:textColor="#ff000000"
android:textSize="14dp"
android:textStyle="bold" />
<TextView
android:id="@+id/snippet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="#ff7f7f7f"
android:textSize="14dp" />
</LinearLayout>
</LinearLayout>
下面回到MainActivity中,先实现AMap.InfoWindowAdapter。
然后在initMap添加InfoWindow适配器监听。
之后重写两个方法。
/**
* 修改内容
*
* @param marker
* @return
*/
@Override
public View getInfoContents(Marker marker) {
return null;
}
/**
* 修改背景
*
* @param marker
*/
@Override
public View getInfoWindow(Marker marker) {
return null;
}
下面将会一起修改,修改后的代码如下:
/**
* 修改内容
*
* @param marker
* @return
*/
@Override
public View getInfoContents(Marker marker) {
View infoContent = getLayoutInflater().inflate(
R.layout.custom_info_contents, null);
render(marker, infoContent);
return infoContent;
}
/**
* 修改背景
*
* @param marker
*/
@Override
public View getInfoWindow(Marker marker) {
View infoWindow = getLayoutInflater().inflate(
R.layout.custom_info_window, null);
render(marker, infoWindow);
return infoWindow;
}
/**
* 渲染
*
* @param marker
* @param view
*/
private void render(Marker marker, View view) {
((ImageView) view.findViewById(R.id.badge))
.setImageResource(R.drawable.icon_yuan);
//修改InfoWindow标题内容样式
String title = marker.getTitle();
TextView titleUi = ((TextView) view.findViewById(R.id.title));
if (title != null) {
SpannableString titleText = new SpannableString(title);
titleText.setSpan(new ForegroundColorSpan(Color.RED), 0,
titleText.length(), 0);
titleUi.setTextSize(15);
titleUi.setText(titleText);
} else {
titleUi.setText("");
}
//修改InfoWindow片段内容样式
String snippet = marker.getSnippet();
TextView snippetUi = ((TextView) view.findViewById(R.id.snippet));
if (snippet != null) {
SpannableString snippetText = new SpannableString(snippet);
snippetText.setSpan(new ForegroundColorSpan(Color.GREEN), 0,
snippetText.length(), 0);
snippetUi.setTextSize(20);
snippetUi.setText(snippetText);
} else {
snippetUi.setText("");
}
}
然后运行:
⑧ InfoWindow的点击事件
刚才的InfoWindow也是可以点击的,先实现AMap.OnInfoWindowClickListener。
同样在。
然后重写onInfoWindowClick方法。
/**
* InfoWindow点击事件
*
* @param marker
*/
@Override
public void onInfoWindowClick(Marker marker) {
showMsg("弹窗内容:标题:" + marker.getTitle() + "\n片段:" + marker.getSnippet());
}
运行效果如下:
这样就可以了。
⑨ 改变地图中心点
我们在实际使用中通常会有这样的操作,希望点击一下就可以移动到所在地,这其实是比较容易做到的,回顾我们现在是一进入地图就会定位到当前所在地,而当我点击地图上其他位置时,会增加一个标点,而我们要做的就是把这个标点作为地图中心,然后移动地图位置即可。
现在思路已经有了,很简单下面来写代码,代码也是很简单的,首先新增一个updateMapCenter方法,里面传递一个LatLng对象作为入参。方法如下:
/**
* 改变地图中心位置
* @param latLng 位置
*/
private void updateMapCenter(LatLng latLng) {
// CameraPosition 第一个参数: 目标位置的屏幕中心点经纬度坐标。
// CameraPosition 第二个参数: 目标可视区域的缩放级别
// CameraPosition 第三个参数: 目标可视区域的倾斜度,以角度为单位。
// CameraPosition 第四个参数: 可视区域指向的方向,以角度为单位,从正北向顺时针方向计算,从0度到360度
CameraPosition cameraPosition = new CameraPosition(latLng, 16, 30, 0);
//位置变更
CameraUpdate cameraUpdate = CameraUpdateFactory.newCameraPosition(cameraPosition);
//改变位置
aMap.moveCamera(cameraUpdate);
}
先通过CameraPosition配置一个中心位置对象,对象需要四个参数,在注释中已经说明了,然后通过CameraUpdate配置一个位置改变对象,传入刚才的cameraPosition。最后就是在地图上改变位置了。通过aMap.moveCamera()。这个方法比较简单,但别忘记了去调用,在onMapClick调用即可。
那么下面运行一下吧。
现在的确是移动过去了,不过好像是一闪而过,感觉用户的体验不是很好,而在使用高德地图APP的时候感觉很平滑的切换中心点,这个其实SDK中也提供了,你只需要把moveCamera改成animateCamera就可以了做到平滑移动,而不会显得很突兀了。这个animateCamera是一个多参方法,里面还可以传动画的时间,取消的回调等等。
那么我们只需要有一个动画就行了,这就很简单。
//带动画的移动
aMap.animateCamera(cameraUpdate);
下面运行一下吧。
嗯,可能GIF上看着效果不是特别的明显,在自己手机上去体验一下就知道了,这个地图平移动画还是很不错的。
九、出行路线规划
首先要搞清楚什么是路线规划,比如有两个地点,A和B。从A到B有多种方路线和交通工具可以选择,这就是路线规划。
那么平时常见的路线规划有哪些呢?步行、驾车、公交地铁等。这也是导航中使用最多的,那么下面先来说说这个步行出行路线规划,以下简称步行。
① 准备工作
这个路线规划我是打算单独放在一个Activity中,这样看起来会更加的清晰,因为我在MainActivity中已经写了很多的其他的功能的业务代码了,再加进去看起来好像就不是很容易去理解。也避免一个Activity打天下的尴尬局面。右键点击这个包名,然后New → Activity → Empty Activity
然后命名为RouteActivity,点击Finish完成Activity的创建。
创建好之后先做好准备工作,我先说说需要做好什么样的准备。一进入RouteActivity之后就要定位到当前所在地,这在前面我已经说过了,因此下面这一部分的代码我就不做讲解,如果你不理解是为什么,那么请从头看起。
首先修改activity_route.xml。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
xmlns:app="http://schemas.android/apk/res-auto"
xmlns:tools="http://schemas.android/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".RouteActivity">
<!--地图-->
<com.amap.api.maps.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
然后修改一下RouteActivity的代码:
package com.llw.mapdemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import com.amap.api.location.AMapLocation;
import com.amap.api.location.AMapLocationClient;
import com.amap.api.location.AMapLocationClientOption;
import com.amap.api.location.AMapLocationListener;
import com.amap.api.maps.AMap;
import com.amap.api.maps.LocationSource;
import com.amap.api.maps.MapView;
import com.amap.api.maps.UiSettings;
import com.amap.api.maps.model.BitmapDescriptorFactory;
import com.amap.api.maps.model.MyLocationStyle;
/**
* 路线规划
* @author llw
*/
public class RouteActivity extends AppCompatActivity implements
AMapLocationListener,LocationSource {
private static final String TAG = "RouteActivity";
//地图
private MapView mapView;
//地图控制器
private AMap aMap = null;
//声明AMapLocationClient类对象
public AMapLocationClient mLocationClient = null;
//声明AMapLocationClientOption对象
public AMapLocationClientOption mLocationOption = null;
//位置更改监听
private OnLocationChangedListener mListener;
//定义一个UiSettings对象
private UiSettings mUiSettings;
//定位样式
private MyLocationStyle myLocationStyle = new MyLocationStyle();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_route);
//初始化定位
initLocation();
//初始化地图
initMap(savedInstanceState);
//启动定位
mLocationClient.startLocation();
}
/**
* 初始化定位
*/
private void initLocation() {
//初始化定位
try {
mLocationClient = new AMapLocationClient(getApplicationContext());
} catch (Exception e) {
e.printStackTrace();
}
if (mLocationClient != null) {
mLocationClient.setLocationListener(this);
mLocationOption = new AMapLocationClientOption();
mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
mLocationOption.setOnceLocationLatest(true);
mLocationOption.setNeedAddress(true);
mLocationOption.setHttpTimeOut(20000);
mLocationOption.setLocationCacheEnable(false);
mLocationClient.setLocationOption(mLocationOption);
}
}
/**
* 初始化地图
*
* @param savedInstanceState
*/
private void initMap(Bundle savedInstanceState) {
mapView = findViewById(R.id.map_view);
mapView.onCreate(savedInstanceState);
//初始化地图控制器对象
aMap = mapView.getMap();
//设置最小缩放等级为16 ,缩放级别范围为[3, 20]
aMap.setMinZoomLevel(16);
//开启室内地图
aMap.showIndoorMap(true);
//实例化UiSettings类对象
mUiSettings = aMap.getUiSettings();
//隐藏缩放按钮 默认显示
mUiSettings.setZoomControlsEnabled(false);
//显示比例尺 默认不显示
mUiSettings.setScaleControlsEnabled(true);
// 自定义定位蓝点图标
myLocationStyle.myLocationIcon(BitmapDescriptorFactory.fromResource(R.drawable.gps_point));
//设置定位蓝点的Style
aMap.setMyLocationStyle(myLocationStyle);
// 设置定位监听
aMap.setLocationSource(this);
// 设置为true表示显示定位层并可触发定位,false表示隐藏定位层并不可触发定位,默认是false
aMap.setMyLocationEnabled(true);
}
@Override
public void onLocationChanged(AMapLocation aMapLocation) {
if (aMapLocation != null) {
if (aMapLocation.getErrorCode() == 0) {
//地址
String address = aMapLocation.getAddress();
//获取纬度
double latitude = aMapLocation.getLatitude();
//获取经度
double longitude = aMapLocation.getLongitude();
Log.d(TAG, aMapLocation.getCity());
Log.d(TAG,address);
//停止定位后,本地定位服务并不会被销毁
mLocationClient.stopLocation();
//显示地图定位结果
if (mListener != null) {
// 显示系统图标
mListener.onLocationChanged(aMapLocation);
}
} else {
//定位失败时,可通过ErrCode(错误码)信息来确定失败的原因,errInfo是错误信息,详见错误码表。
Log.e("AmapError", "location Error, ErrCode:"
+ aMapLocation.getErrorCode() + ", errInfo:"
+ aMapLocation.getErrorInfo());
}
}
}
@Override
public void activate(OnLocationChangedListener onLocationChangedListener) {
mListener = onLocationChangedListener;
if (mLocationClient == null) {
mLocationClient.startLocation();//启动定位
}
}
@Override
public void deactivate() {
mListener = null;
if (mLocationClient != null) {
mLocationClient.stopLocation();
mLocationClient.onDestroy();
}
mLocationClient = null;
}
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mapView.onSaveInstanceState(outState);
}
@Override
protected void onDestroy() {
super.onDestroy();
//销毁定位客户端,同时销毁本地定位服务。
if (mLocationClient != null) {
mLocationClient.onDestroy();
}
mapView.onDestroy();
}
}
然后我们需要从MainActivity中进入RouteActivity,因此修改一下activity_main.xml。
<!--浮动按钮 跳转路线Activity-->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_route"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/fab_poi"
android:layout_alignParentBottom="true"
android:layout_marginBottom="20dp"
android:clickable="true"
android:onClick="jumpRouteActivity"
app:fabSize="mini"
android:src="@drawable/icon_route"
app:backgroundTint="#FFF"
app:backgroundTintMode="screen"
app:hoveredFocusedTranslationZ="18dp"
app:pressedTranslationZ="18dp" />
你可能没有这个icon_route图标,建议去我的源码里面拿,当然我也可以贴出来。
这个浮动按钮会出现在获取poi浮动按钮的左边,然后我们在MainActivity中增加jumpRouteActivity方法,如下所示:
/**
* 进入路线规划
* @param view
*/
public void jumpRouteActivity(View view) {
startActivity(new Intent(this,RouteActivity.class));
}
然后就会跳转到RouteActivity。运行效果如下:
那么现在准备工作就做完了。下面正式进入到路线规划的代码编写。
② 步行路线规划
路线规划首先需要两个点,起点和终点。起点其实已经有了,那就是我们当前所在地,至于终点可以由用户来控制,比如我在当前所在位置,然后点击了地图上某一个地方,把这个地方作为终点,这样一想也是可行的。那么按照这个思路来写一下代码。首先我先在RouteActivity创建两个对象。
//起点
private LatLonPoint mStartPoint;
//终点
private LatLonPoint mEndPoint;
这里面会用到一些帮助方法,因此可以放到一个类里面进行管理,作为一个工具类。而工具类应该放到一个工具包下管理,在com.llw.mapdemo下新建一个util包,然后在这个包下新增ChString类,这个类是高德示例Demo里面的,我就直接拿过来了,代码如下:
package com.llw.mapdemo.util;
public class ChString {
public static final String Kilometer = "\u516c\u91cc";// "公里";
public static final String Meter = "\u7c73";// "米";
public static final String ByFoot = "\u6b65\u884c";// "步行";
public static final String To = "\u53bb\u5f80";// "去往";
public static final String Station = "\u8f66\u7ad9";// "车站";
public static final String TargetPlace = "\u76ee\u7684\u5730";// "目的地";
public static final String StartPlace = "\u51fa\u53d1\u5730";// "出发地";
public static final String About = "\u5927\u7ea6";// "大约";
public static final String Direction = "\u65b9\u5411";// "方向";
public static final String GetOn = "\u4e0a\u8f66";// "上车";
public static final String GetOff = "\u4e0b\u8f66";// "下车";
public static final String Zhan = "\u7ad9";// "站";
public static final String cross = "\u4ea4\u53c9\u8def\u53e3"; // 交叉路口
public static final String type = "\u7c7b\u522b"; // 类别
public static final String address = "\u5730\u5740"; // 地址
public static final String PrevStep = "\u4e0a\u4e00\u6b65";
public static final String NextStep = "\u4e0b\u4e00\u6b65";
public static final String Gong = "\u516c\u4ea4";
public static final String ByBus = "\u4e58\u8f66";
public static final String Arrive = "\u5230\u8FBE";// 到达
}
再在这个包下新增一个MapUtil类,来源于高德示例Demo,我只拿了目前用得到的方法,代码如下:
package com.llw.mapdemo.util;
import com.amap.api.maps.model.LatLng;
import com.amap.api.services.core.LatLonPoint;
import java.text.DecimalFormat;
/**
* 地图帮助类
* @author llw
*/
public class MapUtil {
/**
* 把LatLng对象转化为LatLonPoint对象
*/
public static LatLonPoint convertToLatLonPoint(LatLng latLng) {
return new LatLonPoint(latLng.latitude, latLng.longitude);
}
/**
* 把LatLonPoint对象转化为LatLon对象
*/
public static LatLng convertToLatLng(LatLonPoint latLonPoint) {
return new LatLng(latLonPoint.getLatitude(), latLonPoint.getLongitude());
}
public static String getFriendlyTime(int second) {
if (second > 3600) {
int hour = second / 3600;
int miniate = (second % 3600) / 60;
return hour + "小时" + miniate + "分钟";
}
if (second >= 60) {
int miniate = second / 60;
return miniate + "分钟";
}
return second + "秒";
}
public static String getFriendlyLength(int lenMeter) {
if (lenMeter > 10000) // 10 km
{
int dis = lenMeter / 1000;
return dis + ChString.Kilometer;
}
if (lenMeter > 1000) {
float dis = (float) lenMeter / 1000;
DecimalFormat fnum = new DecimalFormat("##0.0");
String dstr = fnum.format(dis);
return dstr + ChString.Kilometer;
}
if (lenMeter > 100) {
int dis = lenMeter / 50 * 50;
return dis + ChString.Meter;
}
int dis = lenMeter / 10 * 10;
if (dis == 0) {
dis = 10;
}
return dis + ChString.Meter;
}
}
下面回到RouteActivity中,首先对起点进行赋值
//设置起点
mStartPoint = convertToLatLonPoint(new LatLng(latitude, longitude));
通过经纬度,构建LatLng对象,然后将LatLng转为LatLonPoint。最后赋值给起点mStartPoint。
那么下面就是终点了。刚才说到终点通过点击地图时产生,那么既然要点击地图,自然要使当前RouteActivity实现AMap.OnMapClickListener接口。然后在initMap中,进行监听。
//地图点击监听
aMap.setOnMapClickListener(this);
之后重写onMapClick方法,然后将获取到的LatLng对象转为LatLonPoint,最后赋值给终点mEndPoint。
/**
* 点击地图
*/
@Override
public void onMapClick(LatLng latLng) {
//终点
mEndPoint = convertToLatLonPoint(latLng);
}
这样我们就拿到了起点和终点,下面就要去搜索路线了,搜索路线需要一个RouteSearch对象,在RouteActivity中创建。
//路线搜索对象
private RouteSearch routeSearch;
然后RouteActivity实现RouteSearch.OnRouteSearchListener接口,新增一个initRoute方法。在这个方法里面初始化,然后设置路线搜索监听,方法代码如下:
/**
* 初始化路线
*/
private void initRoute() {
try {
routeSearch = new RouteSearch(this);
} catch (AMapException e) {
e.printStackTrace();
}
routeSearch.setRouteSearchListener(this);
}
记得在onCreate方法中调用。
之后需要重写四个方法,如下所示:
@Override
public void onBusRouteSearched(BusRouteResult busRouteResult, int code) {
}
@Override
public void onDriveRouteSearched(DriveRouteResult driveRouteResult, int code) {
}
/**
* 步行规划路径结果
*
* @param walkRouteResult 结果
* @param code 结果码
*/
@Override
public void onWalkRouteSearched(WalkRouteResult walkRouteResult, int code) {
}
@Override
public void onRideRouteSearched(RideRouteResult rideRouteResult, int code) {
}
其中onWalkRouteSearched方法我标注了是步行搜索返回的结果。那么现在先不管这个结果,因为我们需要先去发起路线搜索的请求之后,才会有结果,这里新增一个startRouteSearch方法,代码如下:
/**
* 开始路线搜索
*/
private void startRouteSearch() {
//在地图上添加起点Marker
aMap.addMarker(new MarkerOptions()
.position(convertToLatLng(mStartPoint))
.icon(BitmapDescriptorFactory.fromResource(R.drawable.start)));
//在地图上添加终点Marker
aMap.addMarker(new MarkerOptions()
.position(convertToLatLng(mEndPoint))
.icon(BitmapDescriptorFactory.fromResource(R.drawable.end)));
//搜索路线 构建路径的起终点
final RouteSearch.FromAndTo fromAndTo = new RouteSearch.FromAndTo(
mStartPoint, mEndPoint);
//构建步行路线搜索对象
RouteSearch.WalkRouteQuery query = new RouteSearch.WalkRouteQuery(fromAndTo, RouteSearch.WalkDefault);
// 异步路径规划步行模式查询
routeSearch.calculateWalkRouteAsyn(query);
}
前面两句代码就是给起点和终点各增加一个标注,这里有两个图标,你应该没有,我这里贴一下:
最好去我的源码里面去拿。然后代码继续往下看就是通过起点和终点构建路径的起终点对象fromAndTo,再通过这个对象去构建步行路线搜索对象,最后通过routeSearch对象发起搜索请求。到此为止请求就写完了。我们在onMapClick中去调用它。
下面就该去处理搜索路线的返回了。返回后最重要的是对这个路线进行绘制,从哪里到哪里,绘制在地图上,之前高德的SDK中这一部分是不开放的,不过在地图SDK V4.1.3版本开始,就已经是开源的了,只不过你要到高德示例Demo中去寻找,为了减少你的工作量,我已经提前找好了,并且只拿我需要的。下面在com.llw.mapdemo下新增一个overlay包,这个包下新增三个类。
AMapServicesUtil.java
package com.llw.mapdemo.overlay;
/**
* 地图服务工具类
*/
import android.graphics.Bitmap;
import com.amap.api.maps.model.LatLng;
import com.amap.api.services.core.LatLonPoint;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
class AMapServicesUtil {
public static int BUFFER_SIZE = 2048;
public static byte[] inputStreamToByte(InputStream in) throws IOException {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] data = new byte[BUFFER_SIZE];
int count = -1;
while ((count = in.read(data, 0, BUFFER_SIZE)) != -1){
outStream.write(data, 0, count);
}
data = null;
return outStream.toByteArray();
}
public static LatLonPoint convertToLatLonPoint(LatLng latlon) {
return new LatLonPoint(latlon.latitude, latlon.longitude);
}
public static LatLng convertToLatLng(LatLonPoint latLonPoint) {
return new LatLng(latLonPoint.getLatitude(), latLonPoint.getLongitude());
}
public static ArrayList<LatLng> convertArrList(List<LatLonPoint> shapes) {
ArrayList<LatLng> lineShapes = new ArrayList<LatLng>();
for (LatLonPoint point : shapes) {
LatLng latLngTemp = AMapServicesUtil.convertToLatLng(point);
lineShapes.add(latLngTemp);
}
return lineShapes;
}
public static Bitmap zoomBitmap(Bitmap bitmap, float res) {
if (bitmap == null) {
return null;
}
int width, height;
width = (int) (bitmap.getWidth() * res);
height = (int) (bitmap.getHeight() * res);
Bitmap newbmp = Bitmap.createScaledBitmap(bitmap, width, height, true);
return newbmp;
}
}
RouteOverlay.java
package com.llw.mapdemo.overlay;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import com.amap.api.maps.AMap;
import com.amap.api.maps.CameraUpdateFactory;
import com.amap.api.maps.model.BitmapDescriptor;
import com.amap.api.maps.model.BitmapDescriptorFactory;
import com.amap.api.maps.model.LatLng;
import com.amap.api.maps.model.LatLngBounds;
import com.amap.api.maps.model.Marker;
import com.amap.api.maps.model.MarkerOptions;
import com.amap.api.maps.model.Polyline;
import com.amap.api.maps.model.PolylineOptions;
import com.llw.mapdemo.R;
/**
* 路线图层叠加
*/
public class RouteOverlay {
protected List<Marker> stationMarkers = new ArrayList<Marker>();
protected List<Polyline> allPolyLines = new ArrayList<Polyline>();
protected Marker startMarker;
protected Marker endMarker;
protected LatLng startPoint;
protected LatLng endPoint;
protected AMap mAMap;
private Context mContext;
private Bitmap startBit, endBit, busBit, walkBit, driveBit;
protected boolean nodeIconVisible = true;
public RouteOverlay(Context context) {
mContext = context;
}
/**
* 去掉BusRouteOverlay上所有的Marker。
* @since V2.1.0
*/
public void removeFromMap() {
if (startMarker != null) {
startMarker.remove();
}
if (endMarker != null) {
endMarker.remove();
}
for (Marker marker : stationMarkers) {
marker.remove();
}
for (Polyline line : allPolyLines) {
line.remove();
}
destroyBit();
}
private void destroyBit() {
if (startBit != null) {
startBit.recycle();
startBit = null;
}
if (endBit != null) {
endBit.recycle();
endBit = null;
}
if (busBit != null) {
busBit.recycle();
busBit = null;
}
if (walkBit != null) {
walkBit.recycle();
walkBit = null;
}
if (driveBit != null) {
driveBit.recycle();
driveBit = null;
}
}
/**
* 给起点Marker设置图标,并返回更换图标的图片。如不用默认图片,需要重写此方法。
* @return 更换的Marker图片。
* @since V2.1.0
*/
protected BitmapDescriptor getStartBitmapDescriptor() {
return BitmapDescriptorFactory.fromResource(R.drawable.amap_start);
}
/**
* 给终点Marker设置图标,并返回更换图标的图片。如不用默认图片,需要重写此方法。
* @return 更换的Marker图片。
* @since V2.1.0
*/
protected BitmapDescriptor getEndBitmapDescriptor() {
return BitmapDescriptorFactory.fromResource(R.drawable.amap_end);
}
/**
* 给公交Marker设置图标,并返回更换图标的图片。如不用默认图片,需要重写此方法。
* @return 更换的Marker图片。
* @since V2.1.0
*/
protected BitmapDescriptor getBusBitmapDescriptor() {
return BitmapDescriptorFactory.fromResource(R.drawable.amap_bus);
}
/**
* 给步行Marker设置图标,并返回更换图标的图片。如不用默认图片,需要重写此方法。
* @return 更换的Marker图片。
* @since V2.1.0
*/
protected BitmapDescriptor getWalkBitmapDescriptor() {
return BitmapDescriptorFactory.fromResource(R.drawable.amap_man);
}
protected BitmapDescriptor getDriveBitmapDescriptor() {
return BitmapDescriptorFactory.fromResource(R.drawable.amap_car);
}
protected void addStartAndEndMarker() {
startMarker = mAMap.addMarker((new MarkerOptions())
.position(startPoint).icon(getStartBitmapDescriptor())
.title("\u8D77\u70B9"));
// startMarker.showInfoWindow();
endMarker = mAMap.addMarker((new MarkerOptions()).position(endPoint)
.icon(getEndBitmapDescriptor()).title("\u7EC8\u70B9"));
// mAMap.moveCamera(CameraUpdateFactory.newLatLngZoom(startPoint,
// getShowRouteZoom()));
}
/**
* 移动镜头到当前的视角。
* @since V2.1.0
*/
public void zoomToSpan() {
if (startPoint != null) {
if (mAMap == null) {
return;
}
try {
LatLngBounds bounds = getLatLngBounds();
mAMap.animateCamera(CameraUpdateFactory
.newLatLngBounds(bounds, 100));
} catch (Throwable e) {
e.printStackTrace();
}
}
}
protected LatLngBounds getLatLngBounds() {
LatLngBounds.Builder b = LatLngBounds.builder();
b.include(new LatLng(startPoint.latitude, startPoint.longitude));
b.include(new LatLng(endPoint.latitude, endPoint.longitude));
return b.build();
}
/**
* 路段节点图标控制显示接口。
* @param visible true为显示节点图标,false为不显示。
* @since V2.3.1
*/
public void setNodeIconVisibility(boolean visible) {
try {
nodeIconVisible = visible;
if (this.stationMarkers != null && this.stationMarkers.size() > 0) {
for (int i = 0; i < this.stationMarkers.size(); i++) {
this.stationMarkers.get(i).setVisible(visible);
}
}
} catch (Throwable e) {
e.printStackTrace();
}
}
protected void addStationMarker(MarkerOptions options) {
if(options == null) {
return;
}
Marker marker = mAMap.addMarker(options);
if(marker != null) {
stationMarkers.add(marker);
}
}
protected void addPolyLine(PolylineOptions options) {
if(options == null) {
return;
}
Polyline polyline = mAMap.addPolyline(options);
if(polyline != null) {
allPolyLines.add(polyline);
}
}
protected float getRouteWidth() {
return 18f;
}
protected int getWalkColor() {
return Color.parseColor("#6db74d");
}
/**
* 自定义路线颜色。
* return 自定义路线颜色。
* @since V2.2.1
*/
protected int getBusColor() {
return Color.parseColor("#537edc");
}
protected int getDriveColor() {
return Color.parseColor("#537edc");
}
// protected int getShowRouteZoom() {
// return 15;
// }
}
WalkRouteOverlay.java
package com.llw.mapdemo.overlay;
import java.util.List;
import com.amap.api.maps.AMap;
import com.amap.api.maps.model.BitmapDescriptor;
import com.amap.api.maps.model.LatLng;
import com.amap.api.maps.model.MarkerOptions;
import com.amap.api.maps.model.PolylineOptions;
import com.amap.api.services.core.LatLonPoint;
import com.amap.api.services.route.WalkPath;
import com.amap.api.services.route.WalkStep;
import android.content.Context;
/**
* 步行路线图层类。在高德地图API里,如果要显示步行路线规划,可以用此类来创建步行路线图层。如不满足需求,也可以自己创建自定义的步行路线图层。
* @since V2.1.0
*/
public class WalkRouteOverlay extends RouteOverlay {
private PolylineOptions mPolylineOptions;
private BitmapDescriptor walkStationDescriptor= null;
private WalkPath walkPath;
/**
* 通过此构造函数创建步行路线图层。
* @param context 当前activity。
* @param amap 地图对象。
* @param path 步行路线规划的一个方案。详见搜索服务模块的路径查询包(com.amap.api.services.route)中的类 <strong><a href="../../../../../../Search/com/amap/api/services/route/WalkStep.html" title="com.amap.api.services.route中的类">WalkStep</a></strong>。
* @param start 起点。详见搜索服务模块的核心基础包(com.amap.api.services.core)中的类<strong><a href="../../../../../../Search/com/amap/api/services/core/LatLonPoint.html" title="com.amap.api.services.core中的类">LatLonPoint</a></strong>。
* @param end 终点。详见搜索服务模块的核心基础包(com.amap.api.services.core)中的类<strong><a href="../../../../../../Search/com/amap/api/services/core/LatLonPoint.html" title="com.amap.api.services.core中的类">LatLonPoint</a></strong>。
* @since V2.1.0
*/
public WalkRouteOverlay(Context context, AMap amap, WalkPath path,
LatLonPoint start, LatLonPoint end) {
super(context);
this.mAMap = amap;
this.walkPath = path;
startPoint = AMapServicesUtil.convertToLatLng(start);
endPoint = AMapServicesUtil.convertToLatLng(end);
}
/**
* 添加步行路线到地图中。
* @since V2.1.0
*/
public void addToMap() {
initPolylineOptions();
try {
List<WalkStep> walkPaths = walkPath.getSteps();
for (int i = 0; i < walkPaths.size(); i++) {
WalkStep walkStep = walkPaths.get(i);
LatLng latLng = AMapServicesUtil.convertToLatLng(walkStep
.getPolyline().get(0));
addWalkStationMarkers(walkStep, latLng);
addWalkPolyLines(walkStep);
}
addStartAndEndMarker();
showPolyline();
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* 检查这一步的最后一点和下一步的起始点之间是否存在空隙
*/
private void checkDistanceToNextStep(WalkStep walkStep,
WalkStep walkStep1) {
LatLonPoint lastPoint = getLastWalkPoint(walkStep);
LatLonPoint nextFirstPoint = getFirstWalkPoint(walkStep1);
if (!(lastPoint.equals(nextFirstPoint))) {
addWalkPolyLine(lastPoint, nextFirstPoint);
}
}
/**
* @param walkStep
* @return
*/
private LatLonPoint getLastWalkPoint(WalkStep walkStep) {
return walkStep.getPolyline().get(walkStep.getPolyline().size() - 1);
}
/**
* @param walkStep
* @return
*/
private LatLonPoint getFirstWalkPoint(WalkStep walkStep) {
return walkStep.getPolyline().get(0);
}
private void addWalkPolyLine(LatLonPoint pointFrom, LatLonPoint pointTo) {
addWalkPolyLine(AMapServicesUtil.convertToLatLng(pointFrom), AMapServicesUtil.convertToLatLng(pointTo));
}
private void addWalkPolyLine(LatLng latLngFrom, LatLng latLngTo) {
mPolylineOptions.add(latLngFrom, latLngTo);
}
/**
* @param walkStep
*/
private void addWalkPolyLines(WalkStep walkStep) {
mPolylineOptions.addAll(AMapServicesUtil.convertArrList(walkStep.getPolyline()));
}
/**
* @param walkStep
* @param position
*/
private void addWalkStationMarkers(WalkStep walkStep, LatLng position) {
addStationMarker(new MarkerOptions()
.position(position)
.title("\u65B9\u5411:" + walkStep.getAction()
+ "\n\u9053\u8DEF:" + walkStep.getRoad())
.snippet(walkStep.getInstruction()).visible(nodeIconVisible)
.anchor(0.5f, 0.5f).icon(walkStationDescriptor));
}
/**
* 初始化线段属性
*/
private void initPolylineOptions() {
if(walkStationDescriptor == null) {
walkStationDescriptor = getWalkBitmapDescriptor();
}
mPolylineOptions = null;
mPolylineOptions = new PolylineOptions();
mPolylineOptions.color(getWalkColor()).width(getRouteWidth());
}
private void showPolyline() {
addPolyLine(mPolylineOptions);
}
}
这里面有一些图标你应该没有,我贴一下:
最好去我的源码里去拿,因为这样你就不用手动去命名了。
下面你的这三个类应该是不会报错了,OK,回到RouteActivity中,修改onWalkRouteSearched方法,代码如下:
/**
* 步行规划路径结果
*
* @param walkRouteResult 结果
* @param code 结果码
*/
@Override
public void onWalkRouteSearched(WalkRouteResult walkRouteResult, int code) {
aMap.clear();// 清理地图上的所有覆盖物
if (code == AMapException.CODE_AMAP_SUCCESS) {
if (walkRouteResult != null && walkRouteResult.getPaths() != null) {
if (walkRouteResult.getPaths().size() > 0) {
final WalkPath walkPath = walkRouteResult.getPaths().get(0);
if (walkPath == null) {
return;
}
//绘制路线
WalkRouteOverlay walkRouteOverlay = new WalkRouteOverlay(
this, aMap, walkPath,
walkRouteResult.getStartPos(),
walkRouteResult.getTargetPos());
walkRouteOverlay.removeFromMap();
walkRouteOverlay.addToMap();
walkRouteOverlay.zoomToSpan();
int dis = (int) walkPath.getDistance();
int dur = (int) walkPath.getDuration();
String des = MapUtil.getFriendlyTime(dur) + "(" + MapUtil.getFriendlyLength(dis) + ")";
Log.d(TAG, des);
} else if (walkRouteResult.getPaths() == null) {
showMsg("对不起,没有搜索到相关数据!");
}
} else {
showMsg("对不起,没有搜索到相关数据!");
}
} else {
showMsg("错误码;" + code);
}
}
返回结果时,先清空地图,然后判断是否搜索成功,否的话调用showMsg方法提示一下,方法代码如下:
private void showMsg(String msg) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
然后判断返回值是否为空,之后判断返回的路径是否大于0,大于的话则就可以开始绘制路线了,绘制完之后清空原来的,然后添加新的图层到地图上,然后进行缩放,之后就是一些其他信息的打印了。写了这么久代码了,也该运行一下了。
③ 骑行路线规划
骑行其实和步行差不多,只是路线限制时图层不同而已,其他的都类似,写起来也是比较简单的,不过我们的布局要做一下改变,假如我把骑行也就入到RouteActivity中,那么在一个地图上就有两种出行方式了,因此需要方便用户来切换不同的方式才行。因此我们打开activity_route.xml
,修改后的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android/apk/res/android"
xmlns:app="http://schemas.android/apk/res-auto"
xmlns:tools="http://schemas.android/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".RouteActivity">
<!--设置出行方式-->
<LinearLayout
android:id="@+id/top_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="6dp"
android:paddingEnd="6dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="出行方式:"
android:textColor="#000"
android:textSize="16sp" />
<Spinner
android:id="@+id/spinner"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1" />
</LinearLayout>
<!--地图-->
<com.amap.api.maps.MapView
android:id="@+id/map_view"
android:layout_below="@+id/top_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
一目了然,很简单的布局代码,首先将外部布局修改为相对布局,然后添加一个线性布局,里面添加一个下来选择控件,用于切换不同的出行方式,然后修改一下地图控件,使它置于刚才所添加的顶部布局的下方。就是android:layout_below="@+id/top_layout"
这行代码,不加的话,你的地图控件会覆盖上方的布局。
布局修改好了,进入到RouteActivity中,先创建三个成员变量,如下所示:
//出行方式数组
private static final String[] travelModeArray = {"步行出行", "骑行出行"};
//出行方式值
private static int TRAVEL_MODE = 0;
//数组适配器
private ArrayAdapter<String> arrayAdapter;
这里我们就指定了出现方式的种类,目前三种,后面些其他方式可以再加,然后就是方式值,当我们点击下拉框选择类型之后,通过位置赋值给这个TRAVEL_MODE 变量,然后我们在路线规划的方法中去根据这个值进行不同的路线搜索即可。最后一个是用来配置下拉框数据的。
下面新建一个initTravelMode方法用于初始化出行方式的数据,代码如下:
/**
* 初始化出行方式
*/
private void initTravelMode() {
Spinner spinner = findViewById(R.id.spinner);
//将可选内容与ArrayAdapter连接起来
arrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, travelModeArray);
//设置下拉列表的风格
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
//将adapter 添加到spinner中
spinner.setAdapter(arrayAdapter);
//添加事件Spinner事件监听
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
TRAVEL_MODE = position;
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
}
然后别忘了在onCreater方法中调用。
然后在startRouteSearch方法中,进行出行方式的判断,代码如下:
//出行方式判断
switch (TRAVEL_MODE) {
case 0://步行
//构建步行路线搜索对象
RouteSearch.WalkRouteQuery query = new RouteSearch.WalkRouteQuery(fromAndTo, RouteSearch.WalkDefault);
// 异步路径规划步行模式查询
routeSearch.calculateWalkRouteAsyn(query);
break;
case 1://骑行
//构建骑行路线搜索对象
RouteSearch.RideRouteQuery rideQuery = new RouteSearch.RideRouteQuery(fromAndTo, RouteSearch.WalkDefault);
//骑行规划路径计算
routeSearch.calculateRideRouteAsyn(rideQuery);
break;
default:
break;
}
然后就是回调了,之前我们写了步行的回调,下面就是骑行的回调,因此有一些东西需要加进来才行,下面先做这一步操作,首先是修改原来的MapUtil工具类,在里面新增一个方法,代码如下:
/**
* 把集合体的LatLonPoint转化为集合体的LatLng
*/
public static ArrayList<LatLng> convertArrList(List<LatLonPoint> shapes) {
ArrayList<LatLng> lineShapes = new ArrayList<LatLng>();
for (LatLonPoint point : shapes) {
LatLng latLngTemp = convertToLatLng(point);
lineShapes.add(latLngTemp);
}
return lineShapes;
}
然后在overlay包下,新增一个RideRouteOverlay类,用于在地图上绘制骑行的图层,里面的代码如下:(这个代码是源码里面有的)
package com.llw.mapdemo.overlay;
import android.content.Context;
import com.amap.api.maps.AMap;
import com.amap.api.maps.model.BitmapDescriptor;
import com.amap.api.maps.model.BitmapDescriptorFactory;
import com.amap.api.maps.model.LatLng;
import com.amap.api.maps.model.MarkerOptions;
import com.amap.api.maps.model.PolylineOptions;
import com.amap.api.services.core.LatLonPoint;
import com.amap.api.services.route.RidePath;
import com.amap.api.services.route.RideStep;
import com.llw.mapdemo.R;
import com.llw.mapdemo.util.MapUtil;
import java.util.List;
/**
* 骑行路线图层类。在高德地图API里,如果要显示步行路线规划,可以用此类来创建骑行路线图层。如不满足需求,也可以自己创建自定义的骑行路线图层。
* @since V3.5.0
*/
public class RideRouteOverlay extends RouteOverlay {
private PolylineOptions mPolylineOptions;
private BitmapDescriptor walkStationDescriptor= null;
private RidePath ridePath;
/**
* 通过此构造函数创建骑行路线图层。
* @param context 当前activity。
* @param amap 地图对象。
* @param path 骑行路线规划的一个方案。详见搜索服务模块的路径查询包(com.amap.api.services.route)中的类 <strong><a href="../../../../../../Search/com/amap/api/services/route/WalkStep.html" title="com.amap.api.services.route中的类">WalkStep</a></strong>。
* @param start 起点。详见搜索服务模块的核心基础包(com.amap.api.services.core)中的类<strong><a href="../../../../../../Search/com/amap/api/services/core/LatLonPoint.html" title="com.amap.api.services.core中的类">LatLonPoint</a></strong>。
* @param end 终点。详见搜索服务模块的核心基础包(com.amap.api.services.core)中的类<strong><a href="../../../../../../Search/com/amap/api/services/core/LatLonPoint.html" title="com.amap.api.services.core中的类">LatLonPoint</a></strong>。
* @since V3.5.0
*/
public RideRouteOverlay(Context context, AMap amap, RidePath path,
LatLonPoint start, LatLonPoint end) {
super(context);
this.mAMap = amap;
this.ridePath = path;
startPoint = MapUtil.convertToLatLng(start);
endPoint = MapUtil.convertToLatLng(end);
}
/**
* 添加骑行路线到地图中。
* @since V3.5.0
*/
public void addToMap() {
initPolylineOptions();
try {
List<RideStep> ridePaths = ridePath.getSteps();
for (int i = 0; i < ridePaths.size(); i++) {
RideStep rideStep = ridePaths.get(i);
LatLng latLng = MapUtil.convertToLatLng(rideStep
.getPolyline().get(0));
addRideStationMarkers(rideStep, latLng);
addRidePolyLines(rideStep);
}
addStartAndEndMarker();
showPolyline();
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* @param rideStep
*/
private void addRidePolyLines(RideStep rideStep) {
mPolylineOptions.addAll(MapUtil.convertArrList(rideStep.getPolyline()));
}
/**
* @param rideStep
* @param position
*/
private void addRideStationMarkers(RideStep rideStep, LatLng position) {
addStationMarker(new MarkerOptions()
.position(position)
.title("\u65B9\u5411:" + rideStep.getAction()
+ "\n\u9053\u8DEF:" + rideStep.getRoad())
.snippet(rideStep.getInstruction()).visible(nodeIconVisible)
.anchor(0.5f, 0.5f).icon(walkStationDescriptor));
}
/**
* 初始化线段属性
*/
private void initPolylineOptions() {
if(walkStationDescriptor == null) {
walkStationDescriptor = BitmapDescriptorFactory.fromResource(R.drawable.amap_ride);
}
mPolylineOptions = null;
mPolylineOptions = new PolylineOptions();
mPolylineOptions.color(getDriveColor()).width(getRouteWidth());
}
private void showPolyline() {
addPolyLine(mPolylineOptions);
}
}
下面回到RouteActivity中,找到onRideRouteSearched方法,这是骑行的搜索路线回调方法,修改代码如下:
/**
* 骑行规划路径结果
*
* @param rideRouteResult 结果
* @param code 结果码
*/
@Override
public void onRideRouteSearched(final RideRouteResult rideRouteResult, int code) {
aMap.clear();// 清理地图上的所有覆盖物
if (code == AMapException.CODE_AMAP_SUCCESS) {
if (rideRouteResult != null && rideRouteResult.getPaths() != null) {
if (rideRouteResult.getPaths().size() > 0) {
final RidePath ridePath = rideRouteResult.getPaths()
.get(0);
if(ridePath == null) {
return;
}
RideRouteOverlay rideRouteOverlay = new RideRouteOverlay(
this, aMap, ridePath,
rideRouteResult.getStartPos(),
rideRouteResult.getTargetPos());
rideRouteOverlay.removeFromMap();
rideRouteOverlay.addToMap();
rideRouteOverlay.zoomToSpan();
int dis = (int) ridePath.getDistance();
int dur = (int) ridePath.getDuration();
String des = MapUtil.getFriendlyTime(dur)+"("+MapUtil.getFriendlyLength(dis)+")";
Log.d(TAG, des);
} else if (rideRouteResult.getPaths() == null) {
showMsg("对不起,没有搜索到相关数据!");
}
} else {
showMsg("对不起,没有搜索到相关数据!");
}
} else {
showMsg("错误码;" + code);
}
}
很熟悉的代码吧,和步行的基本没有什么两样,只是里面使用的值不同而已。那么到这里你的代码就写完了,我们来运行一下吧。
这样就完成了骑行的路线规划了。
④ 驾车路线规划
现在来写这个驾车路线规划,步骤还是和前面的差不多,这回你都不用改布局了,加一个值,如下所示。
然后找到startRouteSearch方法,加一个驾车的条件分支
case 2://驾车
//构建驾车路线搜索对象 剩余三个参数分别是:途经点、避让区域、避让道路
RouteSearch.DriveRouteQuery driveQuery = new RouteSearch.DriveRouteQuery(fromAndTo, RouteSearch.WalkDefault, null, null, "");
//驾车规划路径计算
routeSearch.calculateDriveRouteAsyn(driveQuery);
break;
然后同样要有一个图层。在overlay包下新增DrivingRouteOverlay类,用于在地图上绘制驾车路线图层,代码如下:(SDK中代码)
package com.llw.mapdemo.overlay;
import android.content.Context;
import android.graphics.Color;
import com.amap.api.maps.AMap;
import com.amap.api.maps.model.BitmapDescriptor;
import com.amap.api.maps.model.BitmapDescriptorFactory;
import com.amap.api.maps.model.LatLng;
import com.amap.api.maps.model.LatLngBounds;
import com.amap.api.maps.model.Marker;
import com.amap.api.maps.model.MarkerOptions;
import com.amap.api.maps.model.PolylineOptions;
import com.amap.api.services.core.LatLonPoint;
import com.amap.api.services.route.DrivePath;
import com.amap.api.services.route.DriveStep;
import com.amap.api.services.route.TMC;
import com.llw.mapdemo.R;
import com.llw.mapdemo.util.MapUtil;
import java.util.ArrayList;
import java.util.List;
/**
* 驾车路线图层类
*/
public class DrivingRouteOverlay extends RouteOverlay{
private DrivePath drivePath;
private List<LatLonPoint> throughPointList;
private List<Marker> throughPointMarkerList = new ArrayList<Marker>();
private boolean throughPointMarkerVisible = true;
private List<TMC> tmcs;
private PolylineOptions mPolylineOptions;
private PolylineOptions mPolylineOptionscolor;
private Context mContext;
private boolean isColorfulline = true;
private float mWidth = 25;
private List<LatLng> mLatLngsOfPath;
public void setIsColorfulline(boolean iscolorfulline) {
this.isColorfulline = iscolorfulline;
}
/**
* 根据给定的参数,构造一个导航路线图层类对象。
*
* @param amap 地图对象。
* @param path 导航路线规划方案。
* @param context 当前的activity对象。
*/
public DrivingRouteOverlay(Context context, AMap amap, DrivePath path,
LatLonPoint start, LatLonPoint end, List<LatLonPoint> throughPointList) {
super(context);
mContext = context;
mAMap = amap;
this.drivePath = path;
startPoint = MapUtil.convertToLatLng(start);
endPoint = MapUtil.convertToLatLng(end);
this.throughPointList = throughPointList;
}
@Override
public float getRouteWidth() {
return mWidth;
}
/**
* 设置路线宽度
*
* @param mWidth 路线宽度,取值范围:大于0
*/
public void setRouteWidth(float mWidth) {
this.mWidth = mWidth;
}
/**
* 添加驾车路线添加到地图上显示。
*/
public void addToMap() {
initPolylineOptions();
try {
if (mAMap == null) {
return;
}
if (mWidth == 0 || drivePath == null) {
return;
}
mLatLngsOfPath = new ArrayList<LatLng>();
tmcs = new ArrayList<TMC>();
List<DriveStep> drivePaths = drivePath.getSteps();
for (DriveStep step : drivePaths) {
List<LatLonPoint> latlonPoints = step.getPolyline();
List<TMC> tmclist = step.getTMCs();
tmcs.addAll(tmclist);
addDrivingStationMarkers(step, convertToLatLng(latlonPoints.get(0)));
for (LatLonPoint latlonpoint : latlonPoints) {
mPolylineOptions.add(convertToLatLng(latlonpoint));
mLatLngsOfPath.add(convertToLatLng(latlonpoint));
}
}
if (startMarker != null) {
startMarker.remove();
startMarker = null;
}
if (endMarker != null) {
endMarker.remove();
endMarker = null;
}
addStartAndEndMarker();
addThroughPointMarker();
if (isColorfulline && tmcs.size()>0 ) {
colorWayUpdate(tmcs);
showcolorPolyline();
}else {
showPolyline();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* 初始化线段属性
*/
private void initPolylineOptions() {
mPolylineOptions = null;
mPolylineOptions = new PolylineOptions();
mPolylineOptions.color(getDriveColor()).width(getRouteWidth());
}
private void showPolyline() {
addPolyLine(mPolylineOptions);
}
private void showcolorPolyline() {
addPolyLine(mPolylineOptionscolor);
}
/**
* 根据不同的路段拥堵情况展示不同的颜色
*
* @param tmcSection
*/
private void colorWayUpdate(List<TMC> tmcSection) {
if (mAMap == null) {
return;
}
if (tmcSection == null || tmcSection.size() <= 0) {
return;
}
TMC segmentTrafficStatus;
mPolylineOptionscolor = null;
mPolylineOptionscolor = new PolylineOptions();
mPolylineOptionscolor.width(getRouteWidth());
List<Integer> colorList = new ArrayList<Integer>();
mPolylineOptionscolor.add(MapUtil.convertToLatLng(tmcSection.get(0).getPolyline().get(0)));
colorList.add(getDriveColor());
for (int i = 0; i < tmcSection.size(); i++) {
segmentTrafficStatus = tmcSection.get(i);
int color = getcolor(segmentTrafficStatus.getStatus());
List<LatLonPoint> mployline = segmentTrafficStatus.getPolyline();
for (int j = 1; j < mployline.size(); j++) {
mPolylineOptionscolor.add(MapUtil.convertToLatLng(mployline.get(j)));
colorList.add(color);
}
}
colorList.add(getDriveColor());
mPolylineOptionscolor.colorValues(colorList);
}
private int getcolor(String status) {
if (status.equals("畅通")) {
return Color.GREEN;
} else if (status.equals("缓行")) {
return Color.YELLOW;
} else if (status.equals("拥堵")) {
return Color.RED;
} else if (status.equals("严重拥堵")) {
return Color.parseColor("#990033");
} else {
return Color.parseColor("#537edc");
}
}
public LatLng convertToLatLng(LatLonPoint point) {
return new LatLng(point.getLatitude(),point.getLongitude());
}
/**
* @param driveStep
* @param latLng
*/
private void addDrivingStationMarkers(DriveStep driveStep, LatLng latLng) {
addStationMarker(new MarkerOptions()
.position(latLng)
.title("\u65B9\u5411:" + driveStep.getAction()
+ "\n\u9053\u8DEF:" + driveStep.getRoad())
.snippet(driveStep.getInstruction()).visible(nodeIconVisible)
.anchor(0.5f, 0.5f).icon(getDriveBitmapDescriptor()));
}
@Override
protected LatLngBounds getLatLngBounds() {
LatLngBounds.Builder b = LatLngBounds.builder();
b.include(new LatLng(startPoint.latitude, startPoint.longitude));
b.include(new LatLng(endPoint.latitude, endPoint.longitude));
if (this.throughPointList != null && this.throughPointList.size() > 0) {
for (int i = 0; i < this.throughPointList.size(); i++) {
b.include(new LatLng(
this.throughPointList.get(i).getLatitude(),
this.throughPointList.get(i).getLongitude()));
}
}
return b.build();
}
public void setThroughPointIconVisibility(boolean visible) {
try {
throughPointMarkerVisible = visible;
if (this.throughPointMarkerList != null
&& this.throughPointMarkerList.size() > 0) {
for (int i = 0; i < this.throughPointMarkerList.size(); i++) {
this.throughPointMarkerList.get(i).setVisible(visible);
}
}
} catch (Throwable e) {
e.printStackTrace();
}
}
private void addThroughPointMarker() {
if (this.throughPointList != null && this.throughPointList.size() > 0) {
LatLonPoint latLonPoint = null;
for (int i = 0; i < this.throughPointList.size(); i++) {
latLonPoint = this.throughPointList.get(i);
if (latLonPoint != null) {
throughPointMarkerList.add(mAMap
.addMarker((new MarkerOptions())
.position(
new LatLng(latLonPoint
.getLatitude(), latLonPoint
.getLongitude()))
.visible(throughPointMarkerVisible)
.icon(getThroughPointBitDes())
.title("\u9014\u7ECF\u70B9")));
}
}
}
}
private BitmapDescriptor getThroughPointBitDes() {
return BitmapDescriptorFactory.fromResource(R.drawable.amap_through);
}
/**
* 获取两点间距离
*
* @param start
* @param end
* @return
*/
public static int calculateDistance(LatLng start, LatLng end) {
double x1 = start.longitude;
double y1 = start.latitude;
double x2 = end.longitude;
double y2 = end.latitude;
return calculateDistance(x1, y1, x2, y2);
}
public static int calculateDistance(double x1, double y1, double x2, double y2) {
final double NF_pi = 0.01745329251994329; // 弧度 PI/180
x1 *= NF_pi;
y1 *= NF_pi;
x2 *= NF_pi;
y2 *= NF_pi;
double sinx1 = Math.sin(x1);
double siny1 = Math.sin(y1);
double cosx1 = Math.cos(x1);
double cosy1 = Math.cos(y1);
double sinx2 = Math.sin(x2);
double siny2 = Math.sin(y2);
double cosx2 = Math.cos(x2);
double cosy2 = Math.cos(y2);
double[] v1 = new double[3];
v1[0] = cosy1 * cosx1 - cosy2 * cosx2;
v1[1] = cosy1 * sinx1 - cosy2 * sinx2;
v1[2] = siny1 - siny2;
double dist = Math.sqrt(v1[0] * v1[0] + v1[1] * v1[1] + v1[2] * v1[2]);
return (int) (Math.asin(dist / 2) * 12742001.5798544);
}
//获取指定两点之间固定距离点
public static LatLng getPointForDis(LatLng sPt, LatLng ePt, double dis) {
double lSegLength = calculateDistance(sPt, ePt);
double preResult = dis / lSegLength;
return new LatLng((ePt.latitude - sPt.latitude) * preResult + sPt.latitude, (ePt.longitude - sPt.longitude) * preResult + sPt.longitude);
}
/**
* 去掉DriveLineOverlay上的线段和标记。
*/
@Override
public void removeFromMap() {
try {
super.removeFromMap();
if (this.throughPointMarkerList != null
&& this.throughPointMarkerList.size() > 0) {
for (int i = 0; i < this.throughPointMarkerList.size(); i++) {
this.throughPointMarkerList.get(i).remove();
}
this.throughPointMarkerList.clear();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
然后回到RouteActivity中,找到onDriveRouteSearched方法,里面的代码如下:
/**
* 驾车规划路径结果
*
* @param driveRouteResult 结果
* @param code 结果码
*/
@Override
public void onDriveRouteSearched(DriveRouteResult driveRouteResult, int code) {
aMap.clear();// 清理地图上的所有覆盖物
if (code == AMapException.CODE_AMAP_SUCCESS) {
if (driveRouteResult != null && driveRouteResult.getPaths() != null) {
if (driveRouteResult.getPaths().size() > 0) {
final DrivePath drivePath = driveRouteResult.getPaths()
.get(0);
if(drivePath == null) {
return;
}
DrivingRouteOverlay drivingRouteOverlay = new DrivingRouteOverlay(
this, aMap, drivePath,
driveRouteResult.getStartPos(),
driveRouteResult.getTargetPos(), null);
drivingRouteOverlay.removeFromMap();
drivingRouteOverlay.addToMap();
drivingRouteOverlay.zoomToSpan();
int dis = (int) drivePath.getDistance();
int dur = (int) drivePath.getDuration();
String des = MapUtil.getFriendlyTime(dur)+"("+MapUtil.getFriendlyLength(dis)+")";
Log.d(TAG, des);
} else if (driveRouteResult.getPaths() == null) {
showMsg("对不起,没有搜索到相关数据!");
}
} else {
showMsg("对不起,没有搜索到相关数据!");
}
} else {
showMsg("错误码;" + code);
}
}
下面运行一下:
⑤ 公交路线规划
还有一个公交路线规划,公交的规划其实还包括了步行,不过步行是一点点。下面首先是改变里面的值,增加一个出行方式。
然后找到startRouteSearch方法,增加如下方式:
case 3://公交
//构建驾车路线搜索对象 第三个参数表示公交查询城市区号,第四个参数表示是否计算夜班车,0表示不计算,1表示计算
RouteSearch.BusRouteQuery busQuery = new RouteSearch.BusRouteQuery(fromAndTo, RouteSearch.BusLeaseWalk, "0755",0);
//公交规划路径计算
routeSearch.calculateBusRouteAsyn(busQuery);
break;
这里注意一点,那就是城市的区号,这里我填的是0755,表示深圳。你可以通过城市区号查询去查看你所在地的城市区号,当然如果你不想去查询也可以,你可以直接用中文来解决,比如把0755改成深圳,也是可以的。如果你觉得这样也比较麻烦的话,那么你可以定义一个成员变量。
//城市
private String city;
然后在onLocationChanged中赋值。
city = aMapLocation.getCity();
最后在替换,这样的话就会以你当前所在城市为准。
RouteSearch.BusRouteQuery busQuery = new RouteSearch.BusRouteQuery(fromAndTo, RouteSearch.BusLeaseWalk, city, 0);
那么下面同样要绘制公交的图层,在overlay包下新建一个BusRouteOverlay类,代码如下:(来源于SDK)
package com.llw.mapdemo.overlay;
import android.content.Context;
import com.amap.api.maps.AMap;
import com.amap.api.maps.model.LatLng;
import com.amap.api.maps.model.MarkerOptions;
import com.amap.api.maps.model.PolylineOptions;
import com.amap.api.services.busline.BusStationItem;
import com.amap.api.services.core.LatLonPoint;
import com.amap.api.services.route.BusPath;
import com.amap.api.services.route.BusStep;
import com.amap.api.services.route.Doorway;
import com.amap.api.services.route.RailwayStationItem;
import com.amap.api.services.route.RouteBusLineItem;
import com.amap.api.services.route.RouteBusWalkItem;
import com.amap.api.services.route.RouteRailwayItem;
import com.amap.api.services.route.TaxiItem;
import com.amap.api.services.route.WalkStep;
import com.llw.mapdemo.util.MapUtil;
import java.util.ArrayList;
import java.util.List;
/**
* 公交路线图层类。在高德地图API里,如果需要显示公交路线,可以用此类来创建公交路线图层。如不满足需求,也可以自己创建自定义的公交路线图层。
* @since V2.1.0
*/
public class BusRouteOverlay extends RouteOverlay {
private BusPath busPath;
private LatLng latLng;
/**
* 通过此构造函数创建公交路线图层。
* @param context 当前activity。
* @param amap 地图对象。
* @param path 公交路径规划的一个路段。详见搜索服务模块的路径查询包(com.amap.api.services.route)中的类<strong> <a href="../../../../../../Search/com/amap/api/services/route/BusPath.html" title="com.amap.api.services.route中的类">BusPath</a></strong>。
* @param start 起点坐标。详见搜索服务模块的核心基础包(com.amap.api.services.core)中的类 <strong><a href="../../../../../../Search/com/amap/api/services/core/LatLonPoint.html" title="com.amap.api.services.core中的类">LatLonPoint</a></strong>。
* @param end 终点坐标。详见搜索服务模块的核心基础包(com.amap.api.services.core)中的类 <strong><a href="../../../../../../Search/com/amap/api/services/core/LatLonPoint.html" title="com.amap.api.services.core中的类">LatLonPoint</a></strong>。
* @since V2.1.0
*/
public BusRouteOverlay(Context context, AMap amap, BusPath path,
LatLonPoint start, LatLonPoint end) {
super(context);
this.busPath = path;
startPoint = MapUtil.convertToLatLng(start);
endPoint = MapUtil.convertToLatLng(end);
mAMap = amap;
}
/**
* 添加公交路线到地图上。
* @since V2.1.0
*/
public void addToMap() {
/**
* 绘制节点和线<br>
* 细节情况较多<br>
* 两个step之间,用step和step1区分<br>
* 1.一个step内可能有步行和公交,然后有可能他们之间连接有断开<br>
* 2.step的公交和step1的步行,有可能连接有断开<br>
* 3.step和step1之间是公交换乘,且没有步行,需要把step的终点和step1的起点连起来<br>
* 4.公交最后一站和终点间有步行,加入步行线路,还会有一些步行marker<br>
* 5.公交最后一站和终点间无步行,之间连起来<br>
*/
try {
List<BusStep> busSteps = busPath.getSteps();
for (int i = 0; i < busSteps.size(); i++) {
BusStep busStep = busSteps.get(i);
if (i < busSteps.size() - 1) {
BusStep busStep1 = busSteps.get(i + 1);// 取得当前下一个BusStep对象
// 假如步行和公交之间连接有断开,就把步行最后一个经纬度点和公交第一个经纬度点连接起来,避免断线问题
if (busStep.getWalk() != null
&& busStep.getBusLine() != null) {
checkWalkToBusline(busStep);
}
// 假如公交和步行之间连接有断开,就把上一公交经纬度点和下一步行第一个经纬度点连接起来,避免断线问题
if (busStep.getBusLine() != null
&& busStep1.getWalk() != null
&& busStep1.getWalk().getSteps().size() > 0) {
checkBusLineToNextWalk(busStep, busStep1);
}
// 假如两个公交换乘中间没有步行,就把上一公交经纬度点和下一步公交第一个经纬度点连接起来,避免断线问题
if (busStep.getBusLine() != null
&& busStep1.getWalk() == null
&& busStep1.getBusLine() != null) {
checkBusEndToNextBusStart(busStep, busStep1);
}
// 和上面的很类似
if (busStep.getBusLine() != null
&& busStep1.getWalk() == null
&& busStep1.getBusLine() != null) {
checkBusToNextBusNoWalk(busStep, busStep1);
}
if (busStep.getBusLine() != null
&& busStep1.getRailway() != null ) {
checkBusLineToNextRailway(busStep, busStep1);
}
if (busStep1.getWalk() != null &&
busStep1.getWalk().getSteps().size() > 0 &&
busStep.getRailway() != null) {
checkRailwayToNextWalk(busStep, busStep1);
}
if ( busStep1.getRailway() != null &&
busStep.getRailway() != null) {
checkRailwayToNextRailway(busStep, busStep1);
}
if (busStep.getRailway() != null &&
busStep1.getTaxi() != null ){
checkRailwayToNextTaxi(busStep, busStep1);
}
}
if (busStep.getWalk() != null
&& busStep.getWalk().getSteps().size() > 0) {
addWalkSteps(busStep);
} else {
if (busStep.getBusLine() == null && busStep.getRailway() == null && busStep.getTaxi() == null) {
addWalkPolyline(latLng, endPoint);
}
}
if (busStep.getBusLine() != null) {
RouteBusLineItem routeBusLineItem = busStep.getBusLine();
addBusLineSteps(routeBusLineItem);
addBusStationMarkers(routeBusLineItem);
if (i == busSteps.size() - 1) {
addWalkPolyline(MapUtil.convertToLatLng(getLastBuslinePoint(busStep)), endPoint);
}
}
if (busStep.getRailway() != null) {
addRailwayStep(busStep.getRailway());
addRailwayMarkers(busStep.getRailway());
if (i == busSteps.size() - 1) {
addWalkPolyline(MapUtil.convertToLatLng(busStep.getRailway().getArrivalstop().getLocation()), endPoint);
}
}
if (busStep.getTaxi() != null) {
addTaxiStep(busStep.getTaxi());
addTaxiMarkers(busStep.getTaxi());
}
}
addStartAndEndMarker();
} catch (Throwable e) {
e.printStackTrace();
}
}
private void checkRailwayToNextTaxi(BusStep busStep, BusStep busStep1) {
LatLonPoint railwayLastPoint = busStep.getRailway().getArrivalstop().getLocation();
LatLonPoint taxiFirstPoint = busStep1.getTaxi().getOrigin();
if (!railwayLastPoint.equals(taxiFirstPoint)) {
addWalkPolyLineByLatLonPoints(railwayLastPoint, taxiFirstPoint);
}
}
private void checkRailwayToNextRailway(BusStep busStep, BusStep busStep1) {
LatLonPoint railwayLastPoint = busStep.getRailway().getArrivalstop().getLocation();
LatLonPoint railwayFirstPoint = busStep1.getRailway().getDeparturestop().getLocation();
if (!railwayLastPoint.equals(railwayFirstPoint)) {
addWalkPolyLineByLatLonPoints(railwayLastPoint, railwayFirstPoint);
}
}
private void checkBusLineToNextRailway(BusStep busStep, BusStep busStep1) {
LatLonPoint busLastPoint = getLastBuslinePoint(busStep);
LatLonPoint railwayFirstPoint = busStep1.getRailway().getDeparturestop().getLocation();
if (!busLastPoint.equals(railwayFirstPoint)) {
addWalkPolyLineByLatLonPoints(busLastPoint, railwayFirstPoint);
}
}
private void checkRailwayToNextWalk(BusStep busStep, BusStep busStep1) {
LatLonPoint railwayLastPoint = busStep.getRailway().getArrivalstop().getLocation();
LatLonPoint walkFirstPoint = getFirstWalkPoint(busStep1);
if (!railwayLastPoint.equals(walkFirstPoint)) {
addWalkPolyLineByLatLonPoints(railwayLastPoint, walkFirstPoint);
}
}
private void addRailwayStep(RouteRailwayItem railway) {
List<LatLng> railwaylistpoint = new ArrayList<LatLng>();
List<RailwayStationItem> railwayStationItems = new ArrayList<RailwayStationItem>();
railwayStationItems.add(railway.getDeparturestop());
railwayStationItems.addAll(railway.getViastops());
railwayStationItems.add(railway.getArrivalstop());
for (int i = 0; i < railwayStationItems.size(); i++) {
railwaylistpoint.add(MapUtil.convertToLatLng(railwayStationItems.get(i).getLocation()));
}
addRailwayPolyline(railwaylistpoint);
}
private void addTaxiStep(TaxiItem taxi){
addPolyLine(new PolylineOptions().width(getRouteWidth())
.color(getBusColor())
.add(MapUtil.convertToLatLng(taxi.getOrigin()))
.add(MapUtil.convertToLatLng(taxi.getDestination())));
}
/**
* @param busStep
*/
private void addWalkSteps(BusStep busStep) {
RouteBusWalkItem routeBusWalkItem = busStep.getWalk();
List<WalkStep> walkSteps = routeBusWalkItem.getSteps();
for (int j = 0; j < walkSteps.size(); j++) {
WalkStep walkStep = walkSteps.get(j);
if (j == 0) {
LatLng latLng = MapUtil.convertToLatLng(walkStep
.getPolyline().get(0));
String road = walkStep.getRoad();// 道路名字
String instruction = getWalkSnippet(walkSteps);// 步行导航信息
addWalkStationMarkers(latLng, road, instruction);
}
List<LatLng> listWalkPolyline = MapUtil
.convertArrList(walkStep.getPolyline());
this.latLng = listWalkPolyline.get(listWalkPolyline.size() - 1);
addWalkPolyline(listWalkPolyline);
// 假如步行前一段的终点和下的起点有断开,断画直线连接起来,避免断线问题
if (j < walkSteps.size() - 1) {
LatLng lastLatLng = listWalkPolyline.get(listWalkPolyline
.size() - 1);
LatLng firstlatLatLng = MapUtil
.convertToLatLng(walkSteps.get(j + 1).getPolyline()
.get(0));
if (!(lastLatLng.equals(firstlatLatLng))) {
addWalkPolyline(lastLatLng, firstlatLatLng);
}
}
}
}
/**
* 添加一系列的bus PolyLine
*
* @param routeBusLineItem
*/
private void addBusLineSteps(RouteBusLineItem routeBusLineItem) {
addBusLineSteps(routeBusLineItem.getPolyline());
}
private void addBusLineSteps(List<LatLonPoint> listPoints) {
if (listPoints.size() < 1) {
return;
}
addPolyLine(new PolylineOptions().width(getRouteWidth())
.color(getBusColor())
.addAll(MapUtil.convertArrList(listPoints)));
}
/**
* @param latLng
* marker
* @param title
* @param snippet
*/
private void addWalkStationMarkers(LatLng latLng, String title,
String snippet) {
addStationMarker(new MarkerOptions().position(latLng).title(title)
.snippet(snippet).anchor(0.5f, 0.5f).visible(nodeIconVisible)
.icon(getWalkBitmapDescriptor()));
}
/**
* @param routeBusLineItem
*/
private void addBusStationMarkers(RouteBusLineItem routeBusLineItem) {
BusStationItem startBusStation = routeBusLineItem
.getDepartureBusStation();
LatLng position = MapUtil.convertToLatLng(startBusStation
.getLatLonPoint());
String title = routeBusLineItem.getBusLineName();
String snippet = getBusSnippet(routeBusLineItem);
addStationMarker(new MarkerOptions().position(position).title(title)
.snippet(snippet).anchor(0.5f, 0.5f).visible(nodeIconVisible)
.icon(getBusBitmapDescriptor()));
}
private void addTaxiMarkers(TaxiItem taxiItem) {
LatLng position = MapUtil.convertToLatLng(taxiItem
.getOrigin());
String title = taxiItem.getmSname()+"打车";
String snippet = "到终点";
addStationMarker(new MarkerOptions().position(position).title(title)
.snippet(snippet).anchor(0.5f, 0.5f).visible(nodeIconVisible)
.icon(getDriveBitmapDescriptor()));
}
private void addRailwayMarkers(RouteRailwayItem railway) {
LatLng Departureposition = MapUtil.convertToLatLng(railway
.getDeparturestop().getLocation());
String Departuretitle = railway.getDeparturestop().getName()+"上车";
String Departuresnippet = railway.getName();
addStationMarker(new MarkerOptions().position(Departureposition).title(Departuretitle)
.snippet(Departuresnippet).anchor(0.5f, 0.5f).visible(nodeIconVisible)
.icon(getBusBitmapDescriptor()));
LatLng Arrivalposition = MapUtil.convertToLatLng(railway
.getArrivalstop().getLocation());
String Arrivaltitle = railway.getArrivalstop().getName()+"下车";
String Arrivalsnippet = railway.getName();
addStationMarker(new MarkerOptions().position(Arrivalposition).title(Arrivaltitle)
.snippet(Arrivalsnippet).anchor(0.5f, 0.5f).visible(nodeIconVisible)
.icon(getBusBitmapDescriptor()));
}
/**
* 如果换乘没有步行 检查bus最后一点和下一个step的bus起点是否一致
*
* @param busStep
* @param busStep1
*/
private void checkBusToNextBusNoWalk(BusStep busStep, BusStep busStep1) {
LatLng endbusLatLng = MapUtil
.convertToLatLng(getLastBuslinePoint(busStep));
LatLng startbusLatLng = MapUtil
.convertToLatLng(getFirstBuslinePoint(busStep1));
if (startbusLatLng.latitude - endbusLatLng.latitude > 0.0001
|| startbusLatLng.longitude - endbusLatLng.longitude > 0.0001) {
drawLineArrow(endbusLatLng, startbusLatLng);// 断线用带箭头的直线连?
}
}
/**
*
* checkBusToNextBusNoWalk 和这个类似
*
* @param busStep
* @param busStep1
*/
private void checkBusEndToNextBusStart(BusStep busStep, BusStep busStep1) {
LatLonPoint busLastPoint = getLastBuslinePoint(busStep);
LatLng endbusLatLng = MapUtil.convertToLatLng(busLastPoint);
LatLonPoint busFirstPoint = getFirstBuslinePoint(busStep1);
LatLng startbusLatLng = MapUtil.convertToLatLng(busFirstPoint);
if (!endbusLatLng.equals(startbusLatLng)) {
drawLineArrow(endbusLatLng, startbusLatLng);//
}
}
/**
* 检查bus最后一步和下一各step的步行起点是否一致
*
* @param busStep
* @param busStep1
*/
private void checkBusLineToNextWalk(BusStep busStep, BusStep busStep1) {
LatLonPoint busLastPoint = getLastBuslinePoint(busStep);
LatLonPoint walkFirstPoint = getFirstWalkPoint(busStep1);
if (!busLastPoint.equals(walkFirstPoint)) {
addWalkPolyLineByLatLonPoints(busLastPoint, walkFirstPoint);
}
}
/**
* 检查 步行最后一点 和 bus的起点 是否一致
*
* @param busStep
*/
private void checkWalkToBusline(BusStep busStep) {
LatLonPoint walkLastPoint = getLastWalkPoint(busStep);
LatLonPoint buslineFirstPoint = getFirstBuslinePoint(busStep);
if (!walkLastPoint.equals(buslineFirstPoint)) {
addWalkPolyLineByLatLonPoints(walkLastPoint, buslineFirstPoint);
}
}
/**
* @param busStep1
* @return
*/
private LatLonPoint getFirstWalkPoint(BusStep busStep1) {
return busStep1.getWalk().getSteps().get(0).getPolyline().get(0);
}
/**
*
*/
private void addWalkPolyLineByLatLonPoints(LatLonPoint pointFrom,
LatLonPoint pointTo) {
LatLng latLngFrom = MapUtil.convertToLatLng(pointFrom);
LatLng latLngTo = MapUtil.convertToLatLng(pointTo);
addWalkPolyline(latLngFrom, latLngTo);
}
/**
* @param latLngFrom
* @param latLngTo
* @return
*/
private void addWalkPolyline(LatLng latLngFrom, LatLng latLngTo) {
addPolyLine(new PolylineOptions().add(latLngFrom, latLngTo)
.width(getRouteWidth()).color(getWalkColor()).setDottedLine(true));
}
/**
* @param listWalkPolyline
*/
private void addWalkPolyline(List<LatLng> listWalkPolyline) {
addPolyLine(new PolylineOptions().addAll(listWalkPolyline)
.color(getWalkColor()).width(getRouteWidth()).setDottedLine(true));
}
private void addRailwayPolyline(List<LatLng> listPolyline) {
addPolyLine(new PolylineOptions().addAll(listPolyline)
.color(getDriveColor()).width(getRouteWidth()));
}
private String getWalkSnippet(List<WalkStep> walkSteps) {
float disNum = 0;
for (WalkStep step : walkSteps) {
disNum += step.getDistance();
}
return "\u6B65\u884C" + disNum + "\u7C73";
}
public void drawLineArrow(LatLng latLngFrom, LatLng latLngTo) {
addPolyLine(new PolylineOptions().add(latLngFrom, latLngTo).width(3)
.color(getBusColor()).width(getRouteWidth()));// 绘制直线
}
private String getBusSnippet(RouteBusLineItem routeBusLineItem) {
return "("
+ routeBusLineItem.getDepartureBusStation().getBusStationName()
+ "-->"
+ routeBusLineItem.getArrivalBusStation().getBusStationName()
+ ") \u7ECF\u8FC7" + (routeBusLineItem.getPassStationNum() + 1)
+ "\u7AD9";
}
/**
* @param busStep
* @return
*/
private LatLonPoint getLastWalkPoint(BusStep busStep) {
List<WalkStep> walkSteps = busStep.getWalk().getSteps();
WalkStep walkStep = walkSteps.get(walkSteps.size() - 1);
List<LatLonPoint> lonPoints = walkStep.getPolyline();
return lonPoints.get(lonPoints.size() - 1);
}
private LatLonPoint getExitPoint(BusStep busStep) {
Doorway doorway = busStep.getExit();
if (doorway == null) {
return null;
}
return doorway.getLatLonPoint();
}
private LatLonPoint getLastBuslinePoint(BusStep busStep) {
List<LatLonPoint> lonPoints = busStep.getBusLine().getPolyline();
return lonPoints.get(lonPoints.size() - 1);
}
private LatLonPoint getEntrancePoint(BusStep busStep) {
Doorway doorway = busStep.getEntrance();
if (doorway == null) {
return null;
}
return doorway.getLatLonPoint();
}
private LatLonPoint getFirstBuslinePoint(BusStep busStep) {
return busStep.getBusLine().getPolyline().get(0);
}
}
最后回到RouteActivity。
/**
* 公交规划路径结果
*
* @param busRouteResult 结果
* @param code 结果码
*/
@Override
public void onBusRouteSearched(BusRouteResult busRouteResult, int code) {
aMap.clear();// 清理地图上的所有覆盖物
if (code == AMapException.CODE_AMAP_SUCCESS) {
if (busRouteResult != null && busRouteResult.getPaths() != null) {
if (busRouteResult.getPaths().size() > 0) {
final BusPath busPath = busRouteResult.getPaths().get(0);
if (busPath == null) {
return;
}
BusRouteOverlay busRouteOverlay = new BusRouteOverlay(
this, aMap, busPath,
busRouteResult.getStartPos(),
busRouteResult.getTargetPos());
busRouteOverlay.removeFromMap();
busRouteOverlay.addToMap();
busRouteOverlay.zoomToSpan();
int dis = (int) busPath.getDistance();
int dur = (int) busPath.getDuration();
String des = MapUtil.getFriendlyTime(dur) + "(" + MapUtil.getFriendlyLength(dis) + ")";
Log.d(TAG, des);
} else if (busRouteResult.getPaths() == null) {
showMsg("对不起,没有搜索到相关数据!");
}
} else {
showMsg("对不起,没有搜索到相关数据!");
}
} else {
showMsg("错误码;" + code);
}
}
然后来看看运行的效果。
公交路线规划就写完了。
⑥ 步行路线详情
上面我们写好了四种出行方式的路线规划,并且一一做了测试,但是还有一个问题,就是不知道具体的路线信息,称之为路线规划详情。因此下面来介绍这个详情信息是怎么样显示的。因为这个路线详情涉及的内容比较多,所以我这里一个一个来写,首先是步行路线详情。
打开activity_route.xml,在里面增加如下布局代码:
<!--底部规划时间详情-->
<RelativeLayout
android:id="@+id/bottom_layout"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_alignParentBottom="true"
android:background="#FFF"
android:orientation="horizontal"
android:padding="5dp"
android:visibility="gone">
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:paddingEnd="12dp"
android:singleLine="true"
android:textColor="#333333"
android:textSize="16sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginEnd="10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="详情"
android:textColor="#4c90f9"
android:textSize="14sp" />
<Button
android:layout_width="7dp"
android:layout_height="13dp"
android:layout_marginStart="4dp"
android:background="@mipmap/arrow_right_blue"
android:gravity="center_vertical" />
</LinearLayout>
</RelativeLayout>
添加位置如下:
没有的图标去我的源码里面找,或者去高德的SDKdemo里面去找。下面进入到RouteActivity,
//路线规划详情
private RelativeLayout bottomLayout;
//花费时间
private TextView tvTime;
然后绑定控件iD,在initTravelMode方法中。
下面要显示步行花费的时间,找到onWalkRouteSearched方法,在里面增加如下代码:
//显示步行花费时间
tvTime.setText(des);
bottomLayout.setVisibility(View.VISIBLE);
//跳转到路线详情页面
bottomLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(RouteActivity.this,
RouteDetailActivity.class);
intent.putExtra("type",0);
intent.putExtra("path", walkPath);
startActivity(intent);
}
});
添加位置如下图:
这里我把之前的日志打印去掉了,换成文本显示,你会看到有一个RouteDetailActivity.class,这是路线详情页面,然后通过Intent给它传递数据,type用于区分当前的出行类型,path用于显示详情信息,目前还没有这个Activity,所以你需要创建一个。
创建好之后,在刚才报错的地方导一下包就可以了。那么我们先不去写这个详情页面,先运行一下看看。
嗯,还是很不错的,那么下面来写这个详情页面,我还是比较注重这个外观的,希望能有一个好的用户体验,因此我打算美化一下这个详情页面,首先打开styles.xml,增加如下代码:
<!--详情页面主题-->
<style name="DetailTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">#FFF</item>
<item name="colorPrimaryDark">#FFF</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
如下图所示:
你可以看到这里面还有一个主题AppTheme,下面进入到AndroidManifest.xml中,配置这个刚才创建的DetailTheme样式。
从这个图你可以看到很多有用的信息,首先是程序默认使用的是AppTheme主题,当你创建的Activity没有指定主题样式时,则都是AppTheme,大部分人后面都会修改主题样式,因为原生的太丑了,而我给RouteDetailActivity指定了DetailTheme主题,那么就会使用指定的主题。
下面进入activity_route_detail.xml页面,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
xmlns:app="http://schemas.android/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#efefef"
android:fitsSystemWindows="true"
android:orientation="vertical">
<!--标题-->
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#FFF"
app:navigationIcon="@drawable/ic_black_24dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#000"
android:textSize="16sp"
android:textStyle="bold" />
</androidx.appcompat.widget.Toolbar>
<!--分隔线-->
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#000" />
<!--路线详情-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_time"
android:layout_width="match_parent"
android:layout_height="70dp"
android:background="#FFF"
android:gravity="center_vertical"
android:orientation="vertical"
android:padding="5dp"
android:paddingStart="12dp"
android:textColor="#333333"
android:textSize="16sp" />
<ImageView
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#e0e0e0" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:cacheColorHint="#00000000"
android:divider="#00000000"
android:fadingEdge="none"
android:fadingEdgeLength="0dp"
android:footerDividersEnabled="false"
android:headerDividersEnabled="false"
android:listSelector="#00000000"/>
</LinearLayout>
</LinearLayout>
下面进入到RouteDetailActivity,创建成员变量
private Toolbar toolbar;
private TextView tvTitle, tvTime;
private RecyclerView rv;//列表
然后新增一个initView方法,代码如下:
/**
* 初始化
*/
private void initView() {
toolbar = findViewById(R.id.toolbar);
tvTitle = findViewById(R.id.tv_title);
tvTime = findViewById(R.id.tv_time);
rv = findViewById(R.id.rv);
//高亮状态栏
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
Intent intent = getIntent();
if (intent == null) {
return;
}
switch (intent.getIntExtra("type", 0)) {
case 0://步行
break;
case 1://骑行
break;
case 2://驾车
break;
case 3://公交
break;
default:
break;
}
}
这里就是根绝传递进来的type做判断处理,下面就是显示详情数据了,这里我用的是RecyclerView,因此可以使用一个帮助框架。打开项目工程的build.gradle,添加如下库:
maven { url "https://jitpack.io" }
新版Android Studio在settings.gradle中添加,如下图所示:
然后打开app下的build.gradle,在dependencies闭包里面添加如下依赖:
//RecyclerView的好搭档
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'
然后点击右上角的Sync Now进行同步,下面创建列表的item布局,在layout下新建一个item_segment.xml文件,里面的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFF" >
<ImageView
android:id="@+id/bus_seg_split_line"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginLeft="50dp"
android:layout_marginStart="50dp"
android:background="#e0e0e0"/>
<RelativeLayout
android:id="@+id/bus_route_direction"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="11dp"
android:layout_marginStart="11dp" >
<ImageView
android:id="@+id/bus_dir_icon"
android:layout_width="22dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:src="@drawable/dir_start" />
<ImageView
android:id="@+id/bus_dir_icon_up"
android:layout_width="2dp"
android:layout_height="match_parent"
android:layout_above="@id/bus_dir_icon"
android:layout_centerHorizontal="true"
android:background="#b6b6b6"
android:visibility="gone"/>
<ImageView
android:id="@+id/bus_dir_icon_down"
android:layout_width="2dp"
android:layout_height="match_parent"
android:layout_below="@id/bus_dir_icon"
android:layout_centerHorizontal="true"
android:background="#b6b6b6"
android:visibility="gone"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/bus_item"
android:layout_width="match_parent"
android:layout_height="44dp" >
<RelativeLayout
android:id="@+id/stationinfo"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true">
<ImageView
android:id="@+id/bus_expand_image"
android:layout_width="25dp"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:clickable="true"
android:scaleType="centerInside"
android:src="@drawable/down"
android:visibility="gone" />
<TextView
android:id="@+id/bus_station_num"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_toLeftOf="@id/bus_expand_image"
android:layout_toStartOf="@id/bus_expand_image"
android:gravity="center_vertical"
android:textColor="#4c90f9"
android:textSize="12sp"
android:visibility="gone" >
</TextView>
</RelativeLayout>
<TextView
android:id="@+id/bus_line_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="50dp"
android:layout_marginStart="50dp"
android:layout_marginRight="70dp"
android:layout_marginEnd="70dp"
android:textColor="#333333"
android:text="出发"
android:textSize="14sp" />
</RelativeLayout>
<LinearLayout
android:id="@+id/expand_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/bus_item"
android:orientation="vertical">
</LinearLayout>
</RelativeLayout>
下面就来写列表适配器了,这里面会用到我刚才添加的那个依赖框架,它可以让你的RecycleView使用起来很简洁,在com.llw.mapdemo下新建一个adapter包,用于放置所有的适配器,在这个包下新建一个WalkSegmentListAdapter类,里面的代码如下:
package com.llw.mapdemo.adapter;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.amap.api.services.route.WalkStep;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.viewholder.BaseViewHolder;
import com.llw.mapdemo.R;
import com.llw.mapdemo.util.MapUtil;
import java.util.List;
/**
* 步行段列表适配器
*
* @author llw
* @date 2021/2/23 9:31
*/
public class WalkSegmentListAdapter extends BaseQuickAdapter<WalkStep, BaseViewHolder> {
private List<WalkStep> mItemList;
public WalkSegmentListAdapter(int layoutResId, @Nullable List<WalkStep> data) {
super(layoutResId, data);
mItemList = data;
}
@Override
protected void convert(BaseViewHolder helper, WalkStep item) {
TextView lineName = helper.getView(R.id.bus_line_name);
ImageView dirIcon = helper.getView(R.id.bus_dir_icon);
ImageView dirUp = helper.getView(R.id.bus_dir_icon_up);
ImageView dirDown = helper.getView(R.id.bus_dir_icon_down);
ImageView splitLine = helper.getView(R.id.bus_seg_split_line);
int position = getItemPosition(item);
if (position == 0) {
dirIcon.setImageResource(R.drawable.dir_start);
lineName.setText("出发");
dirUp.setVisibility(View.INVISIBLE);
dirDown.setVisibility(View.VISIBLE);
splitLine.setVisibility(View.INVISIBLE);
} else if (position == mItemList.size() - 1) {
dirIcon.setImageResource(R.drawable.dir_end);
lineName.setText("到达终点");
dirUp.setVisibility(View.VISIBLE);
dirDown.setVisibility(View.INVISIBLE);
} else {
splitLine.setVisibility(View.VISIBLE);
dirUp.setVisibility(View.VISIBLE);
dirDown.setVisibility(View.VISIBLE);
String actionName = item.getAction();
int resID = MapUtil.getWalkActionID(actionName);
dirIcon.setImageResource(resID);
lineName.setText(item.getInstruction());
}
}
}
当你改完包名,放置了图标之后,你会发现有一个地方报错。
这是因为还没有这个方法的,所以我们需要在MapUtil中添加这个方法。
public static int getWalkActionID(String actionName) {
if (actionName == null || actionName.equals("")) {
return R.drawable.dir13;
}
if ("左转".equals(actionName)) {
return R.drawable.dir2;
}
if ("右转".equals(actionName)) {
return R.drawable.dir1;
}
if ("向左前方".equals(actionName) || "靠左".equals(actionName) || actionName.contains("向左前方")) {
return R.drawable.dir6;
}
if ("向右前方".equals(actionName) || "靠右".equals(actionName) || actionName.contains("向右前方")) {
return R.drawable.dir5;
}
if ("向左后方".equals(actionName)|| actionName.contains("向左后方")) {
return R.drawable.dir7;
}
if ("向右后方".equals(actionName)|| actionName.contains("向右后方")) {
return R.drawable.dir8;
}
if ("直行".equals(actionName)) {
return R.drawable.dir3;
}
if ("通过人行横道".equals(actionName)) {
return R.drawable.dir9;
}
if ("通过过街天桥".equals(actionName)) {
return R.drawable.dir11;
}
if ("通过地下通道".equals(actionName)) {
return R.drawable.dir10;
}
return R.drawable.dir13;
}
就是通过你的行动ID来设置图标,很简单的方法。那么现在你的这个适配器就没有问题了,回到RouteDetailActivity。新写一个walkDetail方法。
/**
* 步行详情
* @param intent
*/
private void walkDetail(Intent intent) {
tvTitle.setText("步行路线规划");
WalkPath walkPath = intent.getParcelableExtra("path");
String dur = MapUtil.getFriendlyTime((int) walkPath.getDuration());
String dis = MapUtil.getFriendlyLength((int) walkPath.getDistance());
tvTime.setText(dur + "(" + dis + ")");
rv.setLayoutManager(new LinearLayoutManager(this));
rv.setAdapter(new WalkSegmentListAdapter(R.layout.item_segment, walkPath.getSteps()));
}
那么现在要做的就是方法的调用了,在步行中调用walkDetail方法。
当然还要在onCreate中调用initView()。
下面运行一下:
⑦ 骑行路线详情
在RouteActivity中找到onRideRouteSearched方法,方法里面添加如下代码:
tvTime.setText(des);
bottomLayout.setVisibility(View.VISIBLE);
bottomLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(RouteActivity.this,
RouteDetailActivity.class);
intent.putExtra("type",1);
intent.putExtra("path", ridePath);
startActivity(intent);
}
});
然后在adapter包下新增一个RideSegmentListAdapter类,里面的代码如下;
package com.llw.mapdemo.adapter;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.amap.api.services.route.RideStep;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.viewholder.BaseViewHolder;
import com.llw.mapdemo.R;
import com.llw.mapdemo.util.MapUtil;
import java.util.List;
/**
* 骑行段列表适配器
*
* @author llw
* @date 2021/2/23 10:25
*/
public class RideSegmentListAdapter extends BaseQuickAdapter<RideStep, BaseViewHolder> {
private List<RideStep> mItemList;
public RideSegmentListAdapter(int layoutResId, @Nullable List<RideStep> data) {
super(layoutResId, data);
mItemList = data;
}
@Override
protected void convert(BaseViewHolder helper, RideStep item) {
TextView lineName = helper.getView(R.id.bus_line_name);
ImageView dirIcon = helper.getView(R.id.bus_dir_icon);
ImageView dirUp = helper.getView(R.id.bus_dir_icon_up);
ImageView dirDown = helper.getView(R.id.bus_dir_icon_down);
ImageView splitLine = helper.getView(R.id.bus_seg_split_line);
int position = getItemPosition(item);
if (position == 0) {
dirIcon.setImageResource(R.drawable.dir_start);
lineName.setText("出发");
dirUp.setVisibility(View.INVISIBLE);
dirDown.setVisibility(View.VISIBLE);
splitLine.setVisibility(View.INVISIBLE);
} else if (position == mItemList.size() - 1) {
dirIcon.setImageResource(R.drawable.dir_end);
lineName.setText("到达终点");
dirUp.setVisibility(View.VISIBLE);
dirDown.setVisibility(View.INVISIBLE);
} else {
splitLine.setVisibility(View.VISIBLE);
dirUp.setVisibility(View.VISIBLE);
dirDown.setVisibility(View.VISIBLE);
String actionName = item.getAction();
int resID = MapUtil.getWalkActionID(actionName);
dirIcon.setImageResource(resID);
lineName.setText(item.getInstruction());
}
}
}
下面再进入到RouteDetailActivity,新增如下方法:
/**
* 骑行详情
* @param intent
*/
private void rideDetail(Intent intent) {
tvTitle.setText("骑行路线规划");
RidePath ridePath = intent.getParcelableExtra("path");
String dur = MapUtil.getFriendlyTime((int) ridePath.getDuration());
String dis = MapUtil.getFriendlyLength((int) ridePath.getDistance());
tvTime.setText(dur + "(" + dis + ")");
rv.setLayoutManager(new LinearLayoutManager(this));
rv.setAdapter(new RideSegmentListAdapter(R.layout.item_segment, ridePath.getSteps()));
}
然后在initView中调用。
然后你就可以运行了。
⑧ 驾车路线详情
在RouteActivity中找到onRideRouteSearched方法,方法里面添加如下代码:
tvTime.setText(des);
bottomLayout.setVisibility(View.VISIBLE);
bottomLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(RouteActivity.this,
RouteDetailActivity.class);
intent.putExtra("type",2);
intent.putExtra("path", drivePath);
startActivity(intent);
}
});
然后在adapter包下新增一个DriveSegmentListAdapter类,里面的代码如下;
package com.llw.mapdemo.adapter;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.amap.api.services.route.DriveStep;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.viewholder.BaseViewHolder;
import com.llw.mapdemo.R;
import com.llw.mapdemo.util.MapUtil;
import java.util.List;
/**
* 驾车段列表适配器
*
* @author llw
* @date 2021/2/23 11:18
*/
public class DriveSegmentListAdapter extends BaseQuickAdapter<DriveStep, BaseViewHolder> {
private List<DriveStep> mItemList;
public DriveSegmentListAdapter(int layoutResId, @Nullable List<DriveStep> data) {
super(layoutResId, data);
mItemList = data;
}
@Override
protected void convert(BaseViewHolder helper, DriveStep item) {
TextView lineName = helper.getView(R.id.bus_line_name);
ImageView dirIcon = helper.getView(R.id.bus_dir_icon);
ImageView dirUp = helper.getView(R.id.bus_dir_icon_up);
ImageView dirDown = helper.getView(R.id.bus_dir_icon_down);
ImageView splitLine = helper.getView(R.id.bus_seg_split_line);
int position = getItemPosition(item);
if (position == 0) {
dirIcon.setImageResource(R.drawable.dir_start);
lineName.setText("出发");
dirUp.setVisibility(View.INVISIBLE);
dirDown.setVisibility(View.VISIBLE);
splitLine.setVisibility(View.INVISIBLE);
} else if (position == mItemList.size() - 1) {
dirIcon.setImageResource(R.drawable.dir_end);
lineName.setText("到达终点");
dirUp.setVisibility(View.VISIBLE);
dirDown.setVisibility(View.INVISIBLE);
} else {
splitLine.setVisibility(View.VISIBLE);
dirUp.setVisibility(View.VISIBLE);
dirDown.setVisibility(View.VISIBLE);
String actionName = item.getAction();
int resID = MapUtil.getDriveActionID(actionName);
dirIcon.setImageResource(resID);
lineName.setText(item.getInstruction());
}
}
}
这里面会有一个方法报错,所以需要在MapUtil中新增如下方法:
public static int getDriveActionID(String actionName) {
if (actionName == null || actionName.equals("")) {
return R.drawable.dir3;
}
if ("左转".equals(actionName)) {
return R.drawable.dir2;
}
if ("右转".equals(actionName)) {
return R.drawable.dir1;
}
if ("向左前方行驶".equals(actionName) || "靠左".equals(actionName)) {
return R.drawable.dir6;
}
if ("向右前方行驶".equals(actionName) || "靠右".equals(actionName)) {
return R.drawable.dir5;
}
if ("向左后方行驶".equals(actionName) || "左转调头".equals(actionName)) {
return R.drawable.dir7;
}
if ("向右后方行驶".equals(actionName)) {
return R.drawable.dir8;
}
if ("直行".equals(actionName)) {
return R.drawable.dir3;
}
if ("减速行驶".equals(actionName)) {
return R.drawable.dir4;
}
return R.drawable.dir3;
}
适配器没有问题了,下面进入到RouteDetailActivity,新增如下方法:
/**
* 驾车详情
* @param intent
*/
private void driveDetail(Intent intent) {
tvTitle.setText("驾车路线规划");
DrivePath drivePath = intent.getParcelableExtra("path");
String dur = MapUtil.getFriendlyTime((int) drivePath.getDuration());
String dis = MapUtil.getFriendlyLength((int) drivePath.getDistance());
tvTime.setText(dur + "(" + dis + ")");
rv.setLayoutManager(new LinearLayoutManager(this));
rv.setAdapter(new DriveSegmentListAdapter(R.layout.item_segment, drivePath.getSteps()));
}
然后在initView中调用。
然后运行一下:
⑨ 公交路线详情
最后这个公交路线详情是最麻烦的,前面三个其实都还蛮简单的,同样在RouteActivity中找到onBusRouteSearched方法,然后在里面添加如下代码:
tvTime.setText(des);
bottomLayout.setVisibility(View.VISIBLE);
bottomLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(RouteActivity.this,
RouteDetailActivity.class);
intent.putExtra("type",3);
intent.putExtra("path", busPath);
startActivity(intent);
}
});
下面这个适配器的代码就比较多了,为什么呢?因为公交的话它还有这个公交车的信息,比如你是要做几路车,然后经过几个站,之后再转几路车,最后到达终点。因此先增加一个站点的布局,在layout下创建一个item_segment_ex.xml文件,里面的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="27dp"
android:layout_marginLeft="15.5dp"
android:layout_marginStart="15.5dp" >
<ImageView
android:id="@+id/bus_dir_icon"
android:layout_width="13dp"
android:layout_height="13dp"
android:layout_centerVertical="true"
android:src="@drawable/dir_station" />
<ImageView
android:id="@+id/bus_dir_icon_up"
android:layout_width="2dp"
android:layout_height="match_parent"
android:layout_above="@id/bus_dir_icon"
android:layout_centerHorizontal="true"
android:background="#b6b6b6" />
<ImageView
android:id="@+id/bus_dir_icon_down"
android:layout_width="2dp"
android:layout_height="match_parent"
android:layout_below="@id/bus_dir_icon"
android:layout_centerHorizontal="true"
android:background="#b6b6b6" />
</RelativeLayout>
<TextView
android:id="@+id/bus_line_station_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:layout_marginLeft="20dp"
android:layout_marginStart="20dp"
android:textColor="#999999"
android:textSize="13sp" />
</LinearLayout>
这里还需要对原来的BusStep进行一次封装,而不是直接使用这个BusStep,在util包下新增一个SchemeBusStep类,代码如下:
package com.llw.mapdemo.util;
import com.amap.api.services.route.BusStep;
public class SchemeBusStep extends BusStep {
private boolean isWalk = false;
private boolean isBus = false;
private boolean israilway = false;
private boolean istaxi = false;
private boolean isStart = false;
private boolean isEnd = false;
private boolean arrowExpend = false;
public SchemeBusStep(BusStep step) {
if (step != null) {
this.setBusLine(step.getBusLine());
this.setWalk(step.getWalk());
this.setRailway(step.getRailway());
this.setTaxi(step.getTaxi());
}
}
public boolean isArrowExpend() {
return arrowExpend;
}
public void setArrowExpend(boolean arrowExpend) {
this.arrowExpend = arrowExpend;
}
public boolean isWalk() {
return isWalk;
}
public void setWalk(boolean isWalk) {
this.isWalk = isWalk;
}
public boolean isBus() {
return isBus;
}
public void setBus(boolean isBus) {
this.isBus = isBus;
}
public boolean isStart() {
return isStart;
}
public void setStart(boolean isStart) {
this.isStart = isStart;
}
public boolean isEnd() {
return isEnd;
}
public void setEnd(boolean isEnd) {
this.isEnd = isEnd;
}
public boolean isRailway() {
return israilway;
}
public boolean isTaxi() {
return istaxi;
}
public void setRailway(boolean israilway) {
this.israilway = israilway;
}
public void setTaxi(boolean istaxi) {
this.istaxi = istaxi;
}
}
这个里面主要就是对数据的控制,使你在适配器中可以更好处理显示的效果。下面来写这个适配器,在adapter包下新建一个BusSegmentListAdapter类,代码如下:
package com.llw.mapdemo.adapter;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.amap.api.services.busline.BusStationItem;
import com.amap.api.services.route.RailwayStationItem;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.viewholder.BaseViewHolder;
import com.llw.mapdemo.R;
import com.llw.mapdemo.util.SchemeBusStep;
import java.util.List;
/**
* 公交段列表适配器
*
* @author llw
* @date 2021/2/23 14:37
*/
public class BusSegmentListAdapter extends BaseQuickAdapter<SchemeBusStep, BaseViewHolder> {
private List<SchemeBusStep> mBusStepList;
public BusSegmentListAdapter(int layoutResId, @Nullable List<SchemeBusStep> data) {
super(layoutResId, data);
mBusStepList = data;
}
@Override
protected void convert(BaseViewHolder helper, final SchemeBusStep item) {
RelativeLayout busItem = helper.getView(R.id.bus_item);
TextView busLineName = helper.getView(R.id.bus_line_name);
ImageView busDirIcon = helper.getView(R.id.bus_dir_icon);
TextView busStationNum = helper.getView(R.id.bus_station_num);
final ImageView busExpandImage = helper.getView(R.id.bus_expand_image);
ImageView busDirUp = helper.getView(R.id.bus_dir_icon_up);
ImageView busDirDown = helper.getView(R.id.bus_dir_icon_down);
ImageView splitLine = helper.getView(R.id.bus_seg_split_line);
final LinearLayout expandContent = helper.getView(R.id.expand_content);
int position = getItemPosition(item);
if (position == 0) {
busDirIcon.setImageResource(R.drawable.dir_start);
busLineName.setText("出发");
busDirUp.setVisibility(View.INVISIBLE);
busDirDown.setVisibility(View.VISIBLE);
splitLine.setVisibility(View.GONE);
busStationNum.setVisibility(View.GONE);
busExpandImage.setVisibility(View.GONE);
} else if (position == mBusStepList.size() - 1) {
busDirIcon.setImageResource(R.drawable.dir_end);
busLineName.setText("到达终点");
busDirUp.setVisibility(View.VISIBLE);
busDirDown.setVisibility(View.INVISIBLE);
busStationNum.setVisibility(View.INVISIBLE);
busExpandImage.setVisibility(View.INVISIBLE);
} else {
if (item.isWalk() && item.getWalk() != null && item.getWalk().getDistance() > 0) {
busDirIcon.setImageResource(R.drawable.dir13);
busDirUp.setVisibility(View.VISIBLE);
busDirDown.setVisibility(View.VISIBLE);
busLineName.setText("步行"
+ (int) item.getWalk().getDistance() + "米");
busStationNum.setVisibility(View.GONE);
busExpandImage.setVisibility(View.GONE);
} else if (item.isBus() && item.getBusLines().size() > 0) {
busDirIcon.setImageResource(R.drawable.dir14);
busDirUp.setVisibility(View.VISIBLE);
busDirDown.setVisibility(View.VISIBLE);
busLineName.setText(item.getBusLines().get(0).getBusLineName());
busStationNum.setVisibility(View.VISIBLE);
busStationNum
.setText((item.getBusLines().get(0).getPassStationNum() + 1) + "站");
busExpandImage.setVisibility(View.VISIBLE);
} else if (item.isRailway() && item.getRailway() != null) {
busDirIcon.setImageResource(R.drawable.dir16);
busDirUp.setVisibility(View.VISIBLE);
busDirDown.setVisibility(View.VISIBLE);
busLineName.setText(item.getRailway().getName());
busStationNum.setVisibility(View.VISIBLE);
busStationNum
.setText((item.getRailway().getViastops().size() + 1) + "站");
busExpandImage.setVisibility(View.VISIBLE);
} else if (item.isTaxi() && item.getTaxi() != null) {
busDirIcon.setImageResource(R.drawable.dir14);
busDirUp.setVisibility(View.VISIBLE);
busDirDown.setVisibility(View.VISIBLE);
busLineName.setText("打车到终点");
busStationNum.setVisibility(View.GONE);
busExpandImage.setVisibility(View.GONE);
}
}
busItem.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (item.isBus()) {//公交
if (item.isArrowExpend() == false) {
item.setArrowExpend(true);
busExpandImage.setImageResource(R.drawable.up);
addBusStation(item.getBusLine().getDepartureBusStation(), expandContent);
for (BusStationItem station : item.getBusLine()
.getPassStations()) {
addBusStation(station, expandContent);
}
addBusStation(item.getBusLine().getArrivalBusStation(), expandContent);
} else {
item.setArrowExpend(false);
busExpandImage.setImageResource(R.drawable.down);
expandContent.removeAllViews();
}
} else if (item.isRailway()) {//火车
if (item.isArrowExpend() == false) {
item.setArrowExpend(true);
busExpandImage.setImageResource(R.drawable.up);
addRailwayStation(item.getRailway().getDeparturestop(), expandContent);
for (RailwayStationItem station : item.getRailway().getViastops()) {
addRailwayStation(station, expandContent);
}
addRailwayStation(item.getRailway().getArrivalstop(), expandContent);
} else {
item.setArrowExpend(false);
busExpandImage.setImageResource(R.drawable.down);
expandContent.removeAllViews();
}
}
}
});
}
/**
* 添加公交车站
* @param station
* @param expandContent
*/
private void addBusStation(BusStationItem station, LinearLayout expandContent) {
LinearLayout ll = (LinearLayout) View.inflate(getContext(),
R.layout.item_segment_ex, null);
TextView tv = ll.findViewById(R.id.bus_line_station_name);
tv.setText(station.getBusStationName());
expandContent.addView(ll);
}
/**
* 添加火车站
* @param station
* @param expandContent
*/
private void addRailwayStation(RailwayStationItem station, LinearLayout expandContent) {
LinearLayout ll = (LinearLayout) View.inflate(getContext(),
R.layout.item_segment_ex, null);
TextView tv = ll
.findViewById(R.id.bus_line_station_name);
tv.setText(station.getName() + " " + getRailwayTime(station.getTime()));
expandContent.addView(ll);
}
/**
* 获取铁路时间
* @param time
* @return
*/
public static String getRailwayTime(String time) {
return time.substring(0, 2) + ":" + time.substring(2, time.length());
}
}
里面也是比较简单的代码,不清楚的话和我说一声,我再给你解释一下,那么下面进入RouteDetailActivity,先增加一个getBusSteps方法,对BusStep数据进行组装。
/**
* 公交方案数据组装
* @param list
* @return
*/
private List<SchemeBusStep> getBusSteps(List<BusStep> list) {
List<SchemeBusStep> busStepList = new ArrayList<>();
SchemeBusStep start = new SchemeBusStep(null);
start.setStart(true);
busStepList.add(start);
for (BusStep busStep : list) {
if (busStep.getWalk() != null && busStep.getWalk().getDistance() > 0) {
SchemeBusStep walk = new SchemeBusStep(busStep);
walk.setWalk(true);
busStepList.add(walk);
}
if (busStep.getBusLine() != null) {
SchemeBusStep bus = new SchemeBusStep(busStep);
bus.setBus(true);
busStepList.add(bus);
}
if (busStep.getRailway() != null) {
SchemeBusStep railway = new SchemeBusStep(busStep);
railway.setRailway(true);
busStepList.add(railway);
}
if (busStep.getTaxi() != null) {
SchemeBusStep taxi = new SchemeBusStep(busStep);
taxi.setTaxi(true);
busStepList.add(taxi);
}
}
SchemeBusStep end = new SchemeBusStep(null);
end.setEnd(true);
busStepList.add(end);
return busStepList;
}
然后添加一个busDetail方法,里面的代码如下:
/**
* 公交详情
* @param intent
*/
private void busDetail(Intent intent) {
tvTitle.setText("公交路线规划");
BusPath busPath = intent.getParcelableExtra("path");
String dur = MapUtil.getFriendlyTime((int) busPath.getDuration());
String dis = MapUtil.getFriendlyLength((int) busPath.getDistance());
tvTime.setText(dur + "(" + dis + ")");
rv.setLayoutManager(new LinearLayoutManager(this));
rv.setAdapter(new BusSegmentListAdapter(R.layout.item_segment, getBusSteps(busPath.getSteps())));
}
最后在initView中调用。
然后运行一下:
⑩ 手动输入目的地和出发地
在实际使用中,通常都是定位到当前所在地,然后用户再输入一个目的地,以此来计算这个两点之间的路线规划,手动点地图这种方式并不常用,因为你很难点到足够精确的位置,其次就是你要在地图上寻找这个地方所在,这样用户花费的时间就会更多,因此日常使用都是手动输入,输入方式有多种,常规的键盘输入、声音输入、扫码输入等。下面我们使用键盘输入。
这里就需要修改activity_route.xml布局了,将之前的出行方式布局改为如下代码:
<!--设置出行方式-->
<LinearLayout
android:id="@+id/top_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="6dp"
android:paddingEnd="6dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="出行方式:"
android:textColor="#000"
android:textSize="16sp" />
<Spinner
android:id="@+id/spinner"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="6dp"
android:paddingEnd="6dp">
<TextView
android:id="@+id/tv_start"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:gravity="center_vertical"
android:text="当前所在地"
android:textColor="#000"
android:textSize="@dimen/sp_14" />
<EditText
android:id="@+id/et_start_address"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginStart="12dp"
android:layout_toRightOf="@+id/tv_start"
android:background="@null"
android:textColor="#000"
android:textSize="@dimen/sp_12" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_below="@+id/et_start_address"
android:background="#000" />
<TextView
android:id="@+id/tv_end"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_below="@+id/et_start_address"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:text="前往目的地"
android:textColor="#000"
android:textSize="@dimen/sp_14" />
<EditText
android:id="@+id/et_end_address"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_below="@+id/et_start_address"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:layout_toRightOf="@+id/tv_end"
android:background="@null"
android:hint="请输入目的地"
android:imeOptions="actionSearch"
android:singleLine="true"
android:textColor="#000"
android:textSize="14sp" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_below="@+id/et_end_address"
android:background="#000" />
</RelativeLayout>
</LinearLayout>
预览效果如下:
然后进入到RouteActivity,创建变量。
//起点、终点
private EditText etStartAddress, etEndAddress;
然后在initTravelMode方法中绑定ID,同时给目的地输入框添加一个软键盘按键监听。
etStartAddress = findViewById(R.id.et_start_address);
etEndAddress = findViewById(R.id.et_end_address);
//键盘按键监听
etEndAddress.setOnKeyListener(this);
这里的当前地我还是使用定位所在地,在onLocationChanged方法中增加如下代码。
//设置当前所在地
etStartAddress.setText(address);
etStartAddress.setEnabled(false);//禁用输入
我在布局中设置了软键盘变成搜索功能按钮,此时etEndAddress.setOnKeyListener(this);
的这个this应该下方有一条红线,因此你当前的Activity需要实现按键监听。
然后重写onKey方法。
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
//获取输入框的值
String endAddress = etEndAddress.getText().toString().trim();
if (endAddress.isEmpty()) {
showMsg("请输入要前往的目的地");
} else {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
//隐藏软键盘
imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
//通过输入的目的地转为经纬度,然后进行地图上添加标点,最后计算出行路线规划
}
return true;
}
return false;
}
我相信这个代码你应该见过一次了,只不过现在还差一个地址转换的处理,通过地理编码就可以把地址转化成坐标。
//地理编码搜索
private GeocodeSearch geocodeSearch;
//解析成功标识码
private static final int PARSE_SUCCESS_CODE = 1000;
然后在initMap中
//构造 GeocodeSearch 对象
geocodeSearch = new GeocodeSearch(this);
//设置监听
geocodeSearch.setOnGeocodeSearchListener(this);
同样Activity依然需要实现GeocodeSearch.OnGeocodeSearchListener。
然后重写如下方法:
@Override
public void onRegeocodeSearched(RegeocodeResult regeocodeResult, int i) {
}
/**
* 地址转坐标
*
* @param geocodeResult
* @param rCode
*/
@Override
public void onGeocodeSearched(GeocodeResult geocodeResult, int rCode) {
if (rCode == PARSE_SUCCESS_CODE) {
List<GeocodeAddress> geocodeAddressList = geocodeResult.getGeocodeAddressList();
if (geocodeAddressList != null && geocodeAddressList.size() > 0) {
//终点
mEndPoint = geocodeAddressList.get(0).getLatLonPoint();
//开始路线搜索
startRouteSearch();
}
} else {
showMsg("获取坐标失败");
}
}
在onGeocodeSearched方法中通过坐标确定终点,然后进行路线规划,最后一步就是在onKey方法的else中触发这个地理编码搜索。
// name表示地址,第二个参数表示查询城市,中文或者中文全拼,citycode、adcode
GeocodeQuery query = new GeocodeQuery(endAddress, city);
geocodeSearch.getFromLocationNameAsyn(query);
这样就OK了,运行一下。
现在这个当前所在地是通过定位获取的,那么怎么样改成可以手动输入的呢?其实很简单,一开始我写这个页面时,因为是通过定位获取到这个所在地的地址信息然后设置到控件上,所以控件的字体我做了一些调整,如果说要想设置为可输入的话,那么两个输入框的字体应该一致才行,下面先更改这一个,打开activity_route.xml。
之前在页面中我是对这个输入框拿到定位地址后就做了禁用,因此你无法再去手动输入了,现在去掉它。
这行代码我注释掉了,之前设置这个出发地也就是所在地的时候通过定位的方式可以拿到这个经纬度和地址信息,因此很容易就可以构建出mStartPoint(起点),而现在你要做到不光是可以定位,也可以通过手动输入的方式。那么业务逻辑上肯定要有改变了。
首先将定位的地址设置为成员变量。
现在你看到的这个address,是一个局部变量,只会在定位获得结果的时候赋值,因此需要改变一下。
定义变量,我把原来的变量名从address改成了locationAddress,表明这是一个通过定位获得的地址。
//定位地址
private String locationAddress;
对于路线的规划是在输入完目的地之后,点击软键盘上的搜索按钮之后执行的。还记得之前的目的地输入后,有一个地址转坐标的操作,在这里进行了目的地的构建,然后才是路线的规划。
那么你现在的出发地也是手动输入的话,那就同样要转换成坐标,然后去构建这个出发地,也就是起点左标,而解析只有一个返回,那么你就需要区分这个解析是起点还是终点了。然后来看这一块的代码,这里就是对这个目的地输入框进行了检查,这是之前的代码,现在这一块的代码就要做更改了。
首先定义变量。
//起点地址转坐标标识 1
private int tag = -1;
然后设置起点这个输入框的软键盘搜索按键监听。
然后修改onKey方法。如下所示:
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
//获取输入框的值 出发地(起点)
String startAddress = etStartAddress.getText().toString().trim();
//获取输入框的值 目的地(终点)
String endAddress = etEndAddress.getText().toString().trim();
//判断出发地是否有值 不管这个值是定位还是手动输入
if (startAddress.isEmpty()) {
showMsg("请输入当前的出发地");
return false;
}
//判断目的地是否有值
if (endAddress.isEmpty()) {
showMsg("请输入要前往的目的地");
return false;
}
//当出发地输入框有值的时候,判断这个值是否是定位的地址,是则说明你没有更改过,则不需要进行地址转坐标,不是则需要转换。
if (!locationAddress.equals(startAddress)) {
tag = 1;
GeocodeQuery startQuery = new GeocodeQuery(startAddress, city);
geocodeSearch.getFromLocationNameAsyn(startQuery);
} else {
tag = -1;
}
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
//隐藏软键盘
imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
//通过输入的目的地转为经纬度,然后进行地图上添加标点,最后计算出行路线规划
// name表示地址,第二个参数表示查询城市,中文或者中文全拼,citycode、adcode
GeocodeQuery endQuery = new GeocodeQuery(endAddress, city);
geocodeSearch.getFromLocationNameAsyn(endQuery);
return true;
}
return false;
}
然后再去修改onGeocodeSearched方法的代码:
@Override
public void onGeocodeSearched(GeocodeResult geocodeResult, int rCode) {
if (rCode == PARSE_SUCCESS_CODE) {
List<GeocodeAddress> geocodeAddressList = geocodeResult.getGeocodeAddressList();
if (geocodeAddressList != null && geocodeAddressList.size() > 0) {
//判断是不是起点的搜索
if (tag == 1) {
//起点
mStartPoint = geocodeAddressList.get(0).getLatLonPoint();
} else {
//终点
mEndPoint = geocodeAddressList.get(0).getLatLonPoint();
}
if (mStartPoint != null && mEndPoint != null) {
//开始路线搜索
startRouteSearch();
}
}
} else {
showMsg("获取坐标失败");
}
}
下面就可以运行了。
十、源码
如果对你有所帮助的话,不妨欢迎Star和Fork。
源码地址:GaodeMapDemo
尾声
说实话写这篇文章真的很不容易,花费了我很多精力,当然首先是对得起看这篇文章的人吧,山高水长,后会有期~
版权声明:本文标题:Android 高德地图API(详细步骤+源码) 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1729141383h1322978.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论