Android 基于高德 SDK开发的导航项目(入门级)
项目实现了一个功能完整的导航系统,包含:
- 地图显示和控制
- 路线规划和导航
- 语音播报功能
- 实时路况监控
- 安全提醒系统
- 天气信息和预警
- 智能出行建议
一、高德地图KEY的申请
①、进入高德的控制台:https://tengyun-console.amap.com
②、完成注册后,点击右边的 应用管理→我的应用→创建新应用→获取KEY (不会的就跟着我下面的步骤来,包会的)
如何获取自己的测试版安全码SHA1:
上述的命令:keytool -list -v -keystore debug.keystore
二、配置安卓相关权限和接入高德KEY
通过上述的操作我们已经获取了高德地图的KEY,接下来就是对KEY的接入和对项目的权限配置
1、应用高德KEY
在AndroidManifest.xml中标签里面但不要在activity 标签下粘贴:
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="换成自己的高德KEY" />
2、配置权限
这些是可能会用到的权限,请在AndroidManifest.xml中粘贴(不要在标签里面粘贴,会报错):
<!-- 允许程序访问互联网 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 允许程序设置内置sd卡的写权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 允许程序获取网络状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 允许程序访问WiFi网络信息 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 允许程序读写手机状态和身份 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 允许程序访问CellID或WiFi热点来获取粗略的位置 -->
<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.MODIFY_AUDIO_SETTINGS" />
<!-- 允许程序录制音频,用于语音识别功能 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 允许程序控制手机振动器 -->
<uses-permission android:name="android.permission.VIBRATE" />
三、Android 地图SDK 相关下载与引入
①登录高德地图站点:https://lbs.amap.com/api/android-sdk/download
②将下载的压缩包进行压缩(最好存放在一起我们等会要用)
③引入 JAR包到Android
创建一个libs:
选中 JAR包并复制粘贴到libs下,右击这个包点击菜单栏里的“Add AS Library…”
这一步其实就是修改App应用下的“build.gradle”文件,添加了一个依赖 JAR包
在build.gradle(app)里应该会有这一行代码:implementation(files(“libs\AMap3DMap_10.1.200_AMapNavi_10.1.200_AMapSearch_9.7.4_AMapLocation_6.4.9_20241226.jar”))
④在Android应用的app/src/main目录下新建一个jniLibs子目录
最后就是这样的,到这里就算完成了配置,我们要正式开始写程序了
四、开发地图导航功能
1、地图的初始化创建(导入地图)
MainActivity里的代码
import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.amap.api.maps.AMap;
import com.amap.api.maps.MapView;
import com.amap.api.maps.MapsInitializer;
import com.amap.api.maps.model.MyLocationStyle;
import com.amap.api.maps.CameraUpdateFactory;
/**
* 主活动类
* 用于展示高德地图的主界面
*/
public class MainActivity extends AppCompatActivity {
// 地图视图组件
private MapView mapView;
// 地图控制器对象
private AMap aMap;
// 定位权限请求码
private static final int LOCATION_PERMISSION_REQUEST_CODE = 1;
// 需要进行检测的权限数组
private static final String[] NEEDED_PERMISSIONS = {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置高德地图隐私政策
// 更新同意隐私状态,需要在初始化地图之前完成
//如果没有这两行代码,你的地图可能会不显示
MapsInitializer.updatePrivacyShow(this, true, true); // 展示隐私合规对话框
MapsInitializer.updatePrivacyAgree(this, true); // 同意隐私合规政策
setContentView(R.layout.activity_main);
// 初始化地图组件
mapView = findViewById(R.id.map);
// 必须回调MapView的onCreate方法,否则地图无法显示
mapView.onCreate(savedInstanceState);
// 初始化地图控制器
// 只有在aMap为空时才重新获取,避免重复初始化
if (aMap == null) {
aMap = mapView.getMap();
// 设置地图UI控件
setupMapUI();
}
// 检查并请求定位权限
checkAndRequestPermissions();
}
/**
* 设置地图UI和定位样式
*/
private void setupMapUI() {
// 设置定位蓝点的样式
MyLocationStyle myLocationStyle = new MyLocationStyle();
// 连续定位、且将视角移动到地图中心点,定位点依照设备方向旋转,并且会跟随设备移动
myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE);
// 设置定位蓝点的Style
myLocationStyle.strokeColor(Color.argb(180, 3, 145, 255));// 设置圆形的边框颜色
myLocationStyle.radiusFillColor(Color.argb(10, 0, 0, 180));// 设置圆形的填充颜色
myLocationStyle.strokeWidth(5.0f);// 设置圆形的边框粗细
// 设置定位蓝点的 Style
aMap.setMyLocationStyle(myLocationStyle);
// 设置定位监听
aMap.setOnMyLocationChangeListener(location -> {
// 当位置发生变化时,将地图中心点移动到当前位置
aMap.moveCamera(CameraUpdateFactory.newLatLngZoom(
new com.amap.api.maps.model.LatLng(location.getLatitude(), location.getLongitude()),
17)); // 设置缩放级别为17
});
}
/**
* 检查并请求定位权限
*/
private void checkAndRequestPermissions() {
if (lacksPermissions(NEEDED_PERMISSIONS)) {
ActivityCompat.requestPermissions(this, NEEDED_PERMISSIONS, LOCATION_PERMISSION_REQUEST_CODE);
} else {
// 已经有权限,启用定位图层
enableMyLocation();
}
}
/**
* 检查是否缺少权限
*/
private boolean lacksPermissions(String... permissions) {
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(this, permission) !=
PackageManager.PERMISSION_GRANTED) {
return true;
}
}
return false;
}
/**
* 处理权限请求结果
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0) {
boolean allGranted = true;
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
// 获得权限,启用定位图层
enableMyLocation();
} else {
Toast.makeText(this, "需要定位权限才能显示当前位置", Toast.LENGTH_SHORT).show();
}
}
}
}
/**
* 启用定位图层
*/
private void enableMyLocation() {
// 设置为true表示启动显示定位蓝点
aMap.setMyLocationEnabled(true);
}
/**
* Activity生命周期方法 - 恢复
* 必须回调MapView的onResume方法,否则地图会处于暂停状态
*/
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
}
/**
* Activity生命周期方法 - 暂停
* 必须回调MapView的onPause方法,暂停地图的绘制
*/
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
}
/**
* Activity生命周期方法 - 保存状态
* 保存地图当前的状态,在重建Activity时恢复状态
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mapView.onSaveInstanceState(outState);
}
/**
* Activity生命周期方法 - 销毁
* 必须回调MapView的onDestroy方法,销毁地图,释放资源
*/
@Override
protected void onDestroy() {
super.onDestroy();
mapView.onDestroy();
}
}
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.amap.api.maps.MapView
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
通过上面的代码,我们能实现地图的导入,也成功的实现了这个项目的第一步(用真机进行测试,虚拟机会报错)
2、实现路线规划和导航
①类定义和接口实现
public class MainActivity extends AppCompatActivity implements
RouteSearch.OnRouteSearchListener, // 路线搜索监听
AMapNaviListener // 导航监听
写完以后会有这样的报错:那是因为接口里的方法没有写全
解决步骤:
将鼠标放在这行代码上,点击Alt+Enter,Android会自动告诉你解决方法
这样Android就会帮我补全缺失的方法了
②创建一个可以拉动的半透明的面板
在res/layout下创建一个layout Resource File文件 名字叫 bottom_sheet_layout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#F5FFFFFF"
android:orientation="vertical"
android:padding="16dp"
app:behavior_hideable="false"
app:behavior_peekHeight="180dp"
app:behavior_draggable="true"
app:behavior_fitToContents="true"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<!-- 拖动条指示器 -->
<View
android:layout_width="40dp"
android:layout_height="4dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:background="#CCCCCC" />
</LinearLayout>
③创建一个搜索框
在res/drawable下创建一个search_background
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F5F5F5" />
<corners android:radius="24dp" />
<stroke
android:width="1dp"
android:color="#E0E0E0" />
</shape>
在Activity_main.xml文件
<!-- 搜索框区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp"
android:background="@drawable/search_background"
android:elevation="2dp">
<!-- 搜索图标 -->
<ImageView
android:id="@+id/search_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:src="@android:drawable/ic_search_category_default"
app:tint="#666666" />
<!-- 搜索输入框 -->
<EditText
android:id="@+id/search_input"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:background="@null"
android:hint="搜索目的地"
android:textColorHint="#999999"
android:textColor="#333333"
android:textSize="16sp"
android:singleLine="true"
android:imeOptions="actionSearch"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
</LinearLayout>
④规划和导航功能完整的代码
-
导航初始化 :
在MainActivity.java
中,通过initNaviAndRoute
方法初始化了导航功能,使用了高德地图的AMapNavi SDK。 -
路线规划 :
通过planRoute
方法进行路线规划,支持驾车导航。 -
导航控制 :
在底部面板中有一个"开始导航"按钮,点击后会调用startNavi
方法启动真实导航。 -
导航事件监听 :
实现了AMapNaviListener
接口,处理各种导航事件,如: -
导航初始化成功/失败
-
开始导航
-
到达目的地
-
路线计算成功/失败
-
位置变化等
-
路线绘制 :
通过drawRouteLine
方法在地图上绘制导航路线。 -
导航信息展示 :
在底部面板中显示导航相关信息,包括目的地、距离和预计时间,通过updateBottomSheetInfo
方法更新。
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.amap.api.maps.AMap;
import com.amap.api.maps.MapView;
import com.amap.api.maps.MapsInitializer;
import com.amap.api.maps.model.MyLocationStyle;
import com.amap.api.maps.CameraUpdateFactory;
import com.amap.api.maps.model.LatLng;
import com.amap.api.maps.model.Marker;
import com.amap.api.maps.model.MarkerOptions;
import com.amap.api.navi.AMapNavi;
import com.amap.api.navi.AMapNaviListener;
import com.amap.api.navi.enums.PathPlanningStrategy;
import com.amap.api.navi.model.AMapCalcRouteResult;
import com.amap.api.navi.model.AMapLaneInfo;
import com.amap.api.navi.model.AMapModelCross;
import com.amap.api.navi.model.AMapNaviCameraInfo;
import com.amap.api.navi.model.AMapNaviCross;
import com.amap.api.navi.model.AMapNaviLocation;
import com.amap.api.navi.model.AMapNaviPath;
import com.amap.api.navi.model.AMapNaviRouteNotifyData;
import com.amap.api.navi.model.AMapNaviTrafficFacilityInfo;
import com.amap.api.navi.model.AMapServiceAreaInfo;
import com.amap.api.navi.model.AimLessModeCongestionInfo;
import com.amap.api.navi.model.AimLessModeStat;
import com.amap.api.navi.model.NaviInfo;
import com.amap.api.navi.model.NaviLatLng;
import com.amap.api.services.core.AMapException;
import com.amap.api.services.core.LatLonPoint;
import com.amap.api.services.core.PoiItem;
import com.amap.api.services.route.BusRouteResult;
import com.amap.api.services.route.DriveRouteResult;
import com.amap.api.services.route.RideRouteResult;
import com.amap.api.services.route.RouteSearch;
import com.amap.api.services.route.WalkRouteResult;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.amap.api.services.poisearch.PoiSearch;
import com.amap.api.services.poisearch.PoiResult;
import com.amap.api.maps.model.PolylineOptions;
import com.amap.api.maps.model.LatLngBounds;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity implements RouteSearch.OnRouteSearchListener, AMapNaviListener, PoiSearch.OnPoiSearchListener {
// 地图视图组件
private MapView mapView;
// 地图控制器对象
private AMap aMap;
// 导航控制器对象
private AMapNavi mAMapNavi;
// 路线搜索对象
private RouteSearch routeSearch;
// 目的地标记
private Marker destinationMarker;
// 定位权限请求码
private static final int LOCATION_PERMISSION_REQUEST_CODE = 1;
// 需要进行检测的权限数组
private static final String[] NEEDED_PERMISSIONS = {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
};
private BottomSheetBehavior<View> bottomSheetBehavior;
private TextView tvDestination, tvDistance, tvTime;
private Button btnStartNavi;
private LatLonPoint destinationPoint; // 保存目的地坐标
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
MapsInitializer.updatePrivacyShow(this, true, true);
MapsInitializer.updatePrivacyAgree(this, true);
setContentView(R.layout.activity_main);
// 初始化底部面板
initBottomSheet();
mapView = findViewById(R.id.map);
mapView.onCreate(savedInstanceState);
if (aMap == null) {
aMap = mapView.getMap();
setupMapUI();
}
initNaviAndRoute();
checkAndRequestPermissions();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "初始化失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**
* 设置地图UI和定位样式
*/
private void setupMapUI() {
try {
// 设置定位蓝点的样式
MyLocationStyle myLocationStyle = new MyLocationStyle();
// 连续定位、且将视角移动到地图中心点,定位点依照设备方向旋转,并且会跟随设备移动
myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE);
// 设置定位蓝点的Style
myLocationStyle.strokeColor(Color.argb(180, 3, 145, 255));// 设置圆形的边框颜色
myLocationStyle.radiusFillColor(Color.argb(10, 0, 0, 180));// 设置圆形的填充颜色
myLocationStyle.strokeWidth(5.0f);// 设置圆形的边框粗细
// 设置定位蓝点的 Style
aMap.setMyLocationStyle(myLocationStyle);
// 设置定位监听
aMap.setOnMyLocationChangeListener(location -> {
// 当位置发生变化时,将地图中心点移动到当前位置
aMap.moveCamera(CameraUpdateFactory.newLatLngZoom(
new com.amap.api.maps.model.LatLng(location.getLatitude(), location.getLongitude()),
17)); // 设置缩放级别为17
});
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "设置地图UI失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**
* 初始化导航和路线搜索
*/
private void initNaviAndRoute() {
try {
// 初始化路线规划
routeSearch = new RouteSearch(this);
routeSearch.setRouteSearchListener(this);
// 初始化导航
mAMapNavi = AMapNavi.getInstance(getApplicationContext());
mAMapNavi.addAMapNaviListener(this);
// 设置地图点击监听,用于选择目的地
aMap.setOnMapClickListener(latLng -> {
try {
// 清除之前的标记
if (destinationMarker != null) {
destinationMarker.remove();
}
// 添加新的目的地标记
MarkerOptions markerOptions = new MarkerOptions()
.position(latLng)
.title("目的地");
destinationMarker = aMap.addMarker(markerOptions);
// 开始路线规划
planRoute(latLng);
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "设置目的地失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "初始化导航失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**
* 检查并请求定位权限
*/
private void checkAndRequestPermissions() {
if (lacksPermissions(NEEDED_PERMISSIONS)) {
ActivityCompat.requestPermissions(this, NEEDED_PERMISSIONS, LOCATION_PERMISSION_REQUEST_CODE);
} else {
// 已经有权限,启用定位图层
enableMyLocation();
}
}
/**
* 检查是否缺少权限
*/
private boolean lacksPermissions(String... permissions) {
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(this, permission) !=
PackageManager.PERMISSION_GRANTED) {
return true;
}
}
return false;
}
/**
* 处理权限请求结果
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0) {
boolean allGranted = true;
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
// 获得权限,启用定位图层
enableMyLocation();
} else {
Toast.makeText(this, "需要定位权限才能显示当前位置", Toast.LENGTH_SHORT).show();
}
}
}
}
/*启用定位图层*/
private void enableMyLocation() {
// 设置为true表示启动显示定位蓝点
aMap.setMyLocationEnabled(true);
}
@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();
try {
if (mapView != null) {
mapView.onDestroy();
}
if (mAMapNavi != null) {
mAMapNavi.destroy();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onInitNaviFailure() {
// 导航初始化失败
Toast.makeText(this, "导航初始化失败", Toast.LENGTH_SHORT).show();
}
@Override
public void onInitNaviSuccess() {
// 导航初始化成功
Toast.makeText(this, "导航初始化成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onStartNavi(int type) {
// 开始导航回调
Toast.makeText(this, "开始导航", Toast.LENGTH_SHORT).show();
}
@Override public void onTrafficStatusUpdate() {}
@Override
public void onLocationChange(AMapNaviLocation location) {
// 导航过程中位置变化
}
@Override public void onGetNavigationText(int i, String s) {}
@Override public void onGetNavigationText(String s) {}
@Override public void onEndEmulatorNavi() {}
@Override
public void onArriveDestination() {
// 到达目的地
Toast.makeText(this, "到达目的地", Toast.LENGTH_SHORT).show();
}
@Override public void onCalculateRouteFailure(int i) {}
@Override public void onReCalculateRouteForYaw() {}
@Override public void onReCalculateRouteForTrafficJam() {}
@Override
public void onArrivedWayPoint(int i) {}
@Override public void onGpsOpenStatus(boolean b) {}
@Override public void onNaviInfoUpdate(NaviInfo naviInfo) {}
@Override
public void updateCameraInfo(AMapNaviCameraInfo[] aMapNaviCameraInfos) {}
@Override
public void updateIntervalCameraInfo(AMapNaviCameraInfo aMapNaviCameraInfo, AMapNaviCameraInfo aMapNaviCameraInfo1, int i) {}
@Override public void onServiceAreaUpdate(AMapServiceAreaInfo[] aMapServiceAreaInfos) {}
@Override public void showCross(AMapNaviCross aMapNaviCross) {}
@Override public void hideCross() {}
@Override public void showModeCross(AMapModelCross aMapModelCross) {}
@Override public void hideModeCross() {}
@Override public void showLaneInfo(AMapLaneInfo[] aMapLaneInfos, byte[] bytes, byte[] bytes1) {}
@Override public void showLaneInfo(AMapLaneInfo aMapLaneInfo) {}
@Override public void hideLaneInfo() {}
@Override public void onCalculateRouteSuccess(int[] ints) {}
@Override public void notifyParallelRoad(int i) {}
@Override public void OnUpdateTrafficFacility(AMapNaviTrafficFacilityInfo[] aMapNaviTrafficFacilityInfos) {}
@Override public void OnUpdateTrafficFacility(AMapNaviTrafficFacilityInfo aMapNaviTrafficFacilityInfo) {}
@Override public void updateAimlessModeStatistics(AimLessModeStat aimLessModeStat) {}
@Override public void updateAimlessModeCongestionInfo(AimLessModeCongestionInfo aimLessModeCongestionInfo) {}
@Override public void onPlayRing(int i) {}
@Override
public void onCalculateRouteSuccess(AMapCalcRouteResult result) {
Toast.makeText(this, "路线规划成功", Toast.LENGTH_SHORT).show();
AMapNaviPath path = mAMapNavi.getNaviPath();
if (path != null) {
// 更新底部面板信息
updateBottomSheetInfo(path);
// 绘制路线
drawRouteLine(path);
}
}
@Override
public void onCalculateRouteFailure(AMapCalcRouteResult result) {
// 路线规划失败
Toast.makeText(this, "路线规划失败:" + result.getErrorDetail(), Toast.LENGTH_SHORT).show();
}
@Override public void onNaviRouteNotify(AMapNaviRouteNotifyData aMapNaviRouteNotifyData) {}
@Override public void onGpsSignalWeak(boolean b) {}
@Override public void onBusRouteSearched(BusRouteResult result, int errorCode) {}
@Override public void onDriveRouteSearched(DriveRouteResult result, int errorCode) {}
@Override public void onWalkRouteSearched(WalkRouteResult result, int errorCode) {}
@Override public void onRideRouteSearched(RideRouteResult result, int errorCode) {}
private void planRoute(LatLng destination) {
try {
if (aMap.getMyLocation() == null) {
Toast.makeText(this, "等待定位信息...", Toast.LENGTH_SHORT).show();
return;
}
// 获取起点(当前位置)
LatLonPoint startPoint = new LatLonPoint(
aMap.getMyLocation().getLatitude(),
aMap.getMyLocation().getLongitude()
);
// 终点
LatLonPoint endPoint = new LatLonPoint(destination.latitude, destination.longitude);
// 构建导航数据
List<NaviLatLng> startList = new ArrayList<>();
List<NaviLatLng> endList = new ArrayList<>();
startList.add(new NaviLatLng(startPoint.getLatitude(), startPoint.getLongitude()));
endList.add(new NaviLatLng(endPoint.getLatitude(), endPoint.getLongitude()));
// 开始算路
mAMapNavi.calculateDriveRoute(startList, endList, null, 2);
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "路线规划失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
private void initBottomSheet() {
try {
View bottomSheet = findViewById(R.id.bottom_sheet);
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
// 设置初始状态为折叠
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
// 初始化视图
tvDestination = findViewById(R.id.tv_destination);
tvDistance = findViewById(R.id.tv_distance);
tvTime = findViewById(R.id.tv_time);
btnStartNavi = findViewById(R.id.btn_start_navi);
// 初始化搜索框
EditText searchInput = findViewById(R.id.search_input);
searchInput.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
String keyword = searchInput.getText().toString().trim();
if (!TextUtils.isEmpty(keyword)) {
// 隐藏键盘
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(searchInput.getWindowToken(), 0);
// 开始搜索
searchLocation(keyword);
return true;
}
}
return false;
});
// 设置底部面板的状态改变监听
bottomSheetBehavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
switch (newState) {
case BottomSheetBehavior.STATE_COLLAPSED:
// 面板折叠状态
break;
case BottomSheetBehavior.STATE_EXPANDED:
// 面板展开状态
break;
}
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
// 滑动时的处理
}
});
// 设置开始导航按钮点击事件
btnStartNavi.setOnClickListener(v -> {
if (mAMapNavi != null) {
mAMapNavi.startNavi(1); // 1表示真实导航
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
});
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "初始化底部面板失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**搜索位置*/
private void searchLocation(String keyword) {
try {
Toast.makeText(this, "正在搜索: " + keyword, Toast.LENGTH_SHORT).show();
// 创建POI搜索对象
PoiSearch.Query query = new PoiSearch.Query(keyword, "", "全国"); // 设置搜索范围为全国
query.setPageSize(10); // 设置每页最多返回多少条数据
query.setPageNum(1); // 设置查询第几页,从1开始
PoiSearch poiSearch = new PoiSearch(this, query);
poiSearch.setOnPoiSearchListener(this);
// 如果当前位置可用,设置搜索中心点
if (aMap != null && aMap.getMyLocation() != null) {
poiSearch.setBound(new PoiSearch.SearchBound(
new LatLonPoint(aMap.getMyLocation().getLatitude(),
aMap.getMyLocation().getLongitude()),
50000)); // 设置搜索半径为50公里
}
poiSearch.searchPOIAsyn(); // 开始异步搜索
} catch (AMapException e) {
e.printStackTrace();
Toast.makeText(this, "搜索初始化失败:" + e.getErrorMessage(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "搜索失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onPoiSearched(PoiResult result, int rCode) {
if (rCode == AMapException.CODE_AMAP_SUCCESS) {
if (result != null && result.getPois() != null && !result.getPois().isEmpty()) {
// 获取第一个POI点
PoiItem poi = result.getPois().get(0);
// 更新目的地信息
updateDestinationInfo(poi);
// 移动地图到搜索位置
moveMapToLocation(poi.getLatLonPoint());
// 规划路线
planRouteToDestination(poi.getLatLonPoint());
} else {
Toast.makeText(this, "未找到相关地点,请尝试其他关键词", Toast.LENGTH_SHORT).show();
}
} else {
String errorMessage;
switch (rCode) {
case AMapException.CODE_AMAP_ENGINE_RESPONSE_ERROR:
errorMessage = "服务响应错误";
break;
case AMapException.CODE_AMAP_ENGINE_CONNECT_TIMEOUT:
errorMessage = "连接超时";
break;
case AMapException.CODE_AMAP_ENGINE_RETURN_TIMEOUT:
errorMessage = "请求超时";
break;
default:
errorMessage = "未知错误(错误码:" + rCode + ")";
}
Toast.makeText(this, "搜索失败:" + errorMessage, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onPoiItemSearched(PoiItem poiItem, int rCode) {
// 处理单个POI搜索结果
}
private void updateDestinationInfo(PoiItem poi) {
tvDestination.setText("目的地:" + poi.getTitle());
// 保存目的地信息,用于后续导航
destinationPoint = poi.getLatLonPoint();
// 展开面板
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
private void moveMapToLocation(LatLonPoint point) {
if (aMap != null) {
// 清除之前的标记
aMap.clear();
// 添加新的标记
LatLng latLng = new LatLng(point.getLatitude(), point.getLongitude());
aMap.addMarker(new MarkerOptions()
.position(latLng)
.title("目的地"));
// 移动地图
aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15));
}
}
private void planRouteToDestination(LatLonPoint destination) {
if (aMap.getMyLocation() == null) {
Toast.makeText(this, "无法获取当前位置", Toast.LENGTH_SHORT).show();
return;
}
// 构建导航起终点
List<NaviLatLng> startList = new ArrayList<>();
List<NaviLatLng> endList = new ArrayList<>();
// 起点为当前位置
startList.add(new NaviLatLng(aMap.getMyLocation().getLatitude(),
aMap.getMyLocation().getLongitude()));
// 终点为搜索的位置
endList.add(new NaviLatLng(destination.getLatitude(),
destination.getLongitude()));
// 开始算路
mAMapNavi.calculateDriveRoute(startList, endList, null,
PathPlanningStrategy.DRIVING_DEFAULT);
}
private void drawRouteLine(AMapNaviPath path) {
if (aMap != null) {
// 清除之前的路线
aMap.clear();
// 绘制新路线
AMapNaviPath naviPath = mAMapNavi.getNaviPath();
if (naviPath != null) {
// 获取路线坐标点
List<NaviLatLng> coordinates = naviPath.getCoordList();
if (coordinates != null && coordinates.size() > 0) {
List<LatLng> latLngs = new ArrayList<>();
for (NaviLatLng coordinate : coordinates) {
latLngs.add(new LatLng(coordinate.getLatitude(), coordinate.getLongitude()));
}
// 绘制路线
aMap.addPolyline(new PolylineOptions()
.addAll(latLngs)
.width(20)
.color(Color.BLUE));
// 添加起点和终点标记
LatLng startPoint = latLngs.get(0);
LatLng endPoint = latLngs.get(latLngs.size() - 1);
// 添加起点标记
aMap.addMarker(new MarkerOptions()
.position(startPoint)
.title("起点"));
// 添加终点标记
aMap.addMarker(new MarkerOptions()
.position(endPoint)
.title("终点"));
// 调整地图视野以显示整条路线
LatLngBounds bounds = new LatLngBounds.Builder()
.include(startPoint)
.include(endPoint)
.build();
aMap.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100));
}
}
}
}
private void updateBottomSheetInfo(AMapNaviPath path) {
if (path != null) {
// 更新距离信息(转换为公里)
String distance = String.format("距离:%.1f公里", path.getAllLength() / 1000.0);
tvDistance.setText(distance);
// 更新时间信息(转换为分钟)
String time = String.format("预计用时:%d分钟", path.getAllTime() / 60);
tvTime.setText(time);
// 如果有目的地标记,更新目的地信息
if (destinationMarker != null) {
String destination = "目的地:" + destinationMarker.getTitle();
tvDestination.setText(destination);
}
}
}
}
XML文件
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<!-- 地图容器 -->
<com.amap.api.maps.MapView
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- 底部导航面板 -->
<LinearLayout
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#F5FFFFFF"
android:orientation="vertical"
android:elevation="8dp"
app:behavior_peekHeight="180dp"
app:behavior_hideable="false"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<!-- 拖动条指示器 -->
<View
android:layout_width="40dp"
android:layout_height="4dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:background="#CCCCCC" />
<!-- 搜索框区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp"
android:background="@drawable/search_background"
android:elevation="2dp">
<!-- 搜索图标 -->
<ImageView
android:id="@+id/search_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:src="@android:drawable/ic_search_category_default"
app:tint="#666666" />
<!-- 搜索输入框 -->
<EditText
android:id="@+id/search_input"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:background="@null"
android:hint="搜索目的地"
android:textColorHint="#999999"
android:textColor="#333333"
android:textSize="16sp"
android:singleLine="true"
android:imeOptions="actionSearch"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
</LinearLayout>
<!-- 导航信息区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- 目的地信息 -->
<TextView
android:id="@+id/tv_destination"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="目的地:"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold" />
<!-- 距离信息 -->
<TextView
android:id="@+id/tv_distance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="距离:"
android:textColor="#666666"
android:textSize="16sp" />
<!-- 预计时间 -->
<TextView
android:id="@+id/tv_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="预计时间:"
android:textColor="#666666"
android:textSize="16sp" />
<!-- 导航按钮 -->
<Button
android:id="@+id/btn_start_navi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@android:color/holo_blue_light"
android:elevation="4dp"
android:padding="12dp"
android:text="开始导航"
android:textColor="#FFFFFF"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
最后实现的效果:
3、处理偏航
①onReCalculateRouteForYaw 方法(核心偏航处理方法)
@Override
public void onReCalculateRouteForYaw() {
// 偏航重新计算路线回调
requireActivity().runOnUiThread(() -> {
String message = "您已偏离规划路线,正在重新规划";
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show();
if (!isMuted && textToSpeech != null) {
textToSpeech.speak(message, TextToSpeech.QUEUE_FLUSH, null, null);
}
});
}
②、calculateRoute 方法(路线重新规划)
private void calculateRoute() {
if (mStartPoint == null) {
// 如果没有起点,使用当前位置
Location location = aMap.getMyLocation();
if (location != null) {
mStartPoint = new LatLng(location.getLatitude(), location.getLongitude());
} else {
Toast.makeText(requireContext(), "无法获取当前位置", Toast.LENGTH_SHORT).show();
return;
}
}
try {
// 清除之前的路线
aMap.clear();
// 准备导航起终点
List<NaviLatLng> startPoints = new ArrayList<>();
startPoints.add(new NaviLatLng(mStartPoint.latitude, mStartPoint.longitude));
List<NaviLatLng> endPoints = new ArrayList<>();
endPoints.add(new NaviLatLng(mEndPoint.latitude, mEndPoint.longitude));
// 根据出行方式计算路线
switch (currentNaviMode) {
case 0: // 驾车
mAMapNavi.calculateDriveRoute(startPoints, endPoints, null,
PathPlanningStrategy.DRIVING_DEFAULT);
break;
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(requireContext(), "路线规划失败:" + e.getMessage(),
Toast.LENGTH_SHORT).show();
}
}
③、onCalculateRouteSuccess 方法(路线重新规划成功回调)
@Override
public void onCalculateRouteSuccess(AMapCalcRouteResult result) {
if (result != null) {
try {
// 清除之前的标记和路线
aMap.clear();
mapPolylines.clear();
// 添加起点和终点标记
if (mStartPoint != null) {
MarkerOptions startMarker = new MarkerOptions()
.position(mStartPoint)
.title("起点")
.snippet("当前位置")
.icon(BitmapDescriptorFactory.defaultMarker(
BitmapDescriptorFactory.HUE_GREEN));
aMap.addMarker(startMarker);
}
if (mEndPoint != null) {
MarkerOptions endMarker = new MarkerOptions()
.position(mEndPoint)
.title("终点")
.snippet("目的地")
.icon(BitmapDescriptorFactory.defaultMarker(
BitmapDescriptorFactory.HUE_RED));
aMap.addMarker(endMarker);
}
// 获取所有路线ID并绘制路线
int[] routeIds = result.getRouteid();
if (routeIds != null && routeIds.length > 0) {
currentRouteIds = routeIds;
updateRouteOptions();
drawSelectedRoute();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(requireContext(), "路线显示失败:" + e.getMessage(),
Toast.LENGTH_SHORT).show();
}
}
}
④、drawSelectedRoute 方法(绘制新路线)
private void drawSelectedRoute() {
try {
// 选择路线
mAMapNavi.selectRouteId(currentRouteIds[selectedRouteIndex]);
// 清除之前的路线
for (Polyline line : mapPolylines) {
line.remove();
}
mapPolylines.clear();
// 获取路线信息
List<NaviLatLng> pathPoints = mAMapNavi.getNaviPath().getCoordList();
if (pathPoints != null && !pathPoints.isEmpty()) {
// 设置路线颜色
int pathColor;
switch (currentNaviMode) {
case 1: // 步行
pathColor = Color.GREEN;
break;
}
// 转换坐标点并绘制路线
List<LatLng> mapPoints = new ArrayList<>();
for (NaviLatLng point : pathPoints) {
mapPoints.add(new LatLng(point.getLatitude(), point.getLongitude()));
}
// 添加新路线
Polyline polyline = aMap.addPolyline(new PolylineOptions()
.addAll(mapPoints)
.width(20)
.color(pathColor)
.zIndex(1)
.setDottedLine(false));
mapPolylines.add(polyline);
// 调整地图视野以显示整条路线
LatLngBounds.Builder builder = new LatLngBounds.Builder();
for (LatLng point : mapPoints) {
builder.include(point);
}
aMap.animateCamera(CameraUpdateFactory.newLatLngBounds(
builder.build(), 100));
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(requireContext(), "路线绘制失败:" + e.getMessage(),
Toast.LENGTH_SHORT).show();
}
}
五、其他功能
1、语音播报功能
① TextToSpeech 基本介绍与用法
TextToSpeech 是 Android 提供的文字转语音引擎,可以将文本转换为语音输出。
1)初始化方式
private TextToSpeech textToSpeech;
// 方式一:基础初始化
textToSpeech = new TextToSpeech(context, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
// 设置语言
int result = textToSpeech.setLanguage(Locale.CHINESE);
// 检查语言是否支持
if (result == TextToSpeech.LANG_MISSING_DATA ||
result == TextToSpeech.LANG_NOT_SUPPORTED) {
Toast.makeText(context, "语言不支持", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(context, "初始化失败", Toast.LENGTH_SHORT).show();
}
}
});
// 方式二:Lambda表达式(简化写法)
textToSpeech = new TextToSpeech(requireContext(), status -> {
if (status == TextToSpeech.SUCCESS) {
textToSpeech.setLanguage(Locale.CHINESE);
}
});
2)主要配置方法
// 设置语言
textToSpeech.setLanguage(Locale.CHINESE);
// 设置语速(1.0为正常速度)
textToSpeech.setSpeechRate(1.0f);
// 设置音调(1.0为正常音调)
textToSpeech.setPitch(1.0f);
// 设置音量(0.0-1.0)
textToSpeech.setVolume(1.0f);
// 设置音频流类型
textToSpeech.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build());
3)语音播报方法
// 1. 基本播报方法
textToSpeech.speak(
"要播报的文本", // 文本内容
TextToSpeech.QUEUE_FLUSH, // 播报模式
null, // Bundle参数
"utteranceId" // 话语ID
);
// 2. 立即播报(清除队列)
textToSpeech.speak(
"紧急提示信息",
TextToSpeech.QUEUE_FLUSH,
null,
null
);
// 3. 加入播报队列
textToSpeech.speak(
"普通提示信息",
TextToSpeech.QUEUE_ADD,
null,
null
);
// 4. 带参数的播报
Bundle params = new Bundle();
params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, 0.8f);
params.putFloat(TextToSpeech.Engine.KEY_PARAM_PITCH, 1.2f);
textToSpeech.speak(
"自定义参数播报",
TextToSpeech.QUEUE_FLUSH,
params,
"customId"
);
4)播报状态监听
textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
// 开始播报
}
@Override
public void onDone(String utteranceId) {
// 播报完成
}
@Override
public void onError(String utteranceId) {
// 播报错误
}
});
5)权限要求
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
②使用示例(步骤解析)
1). 初始化语音系统
// 在类成员变量中声明
private TextToSpeech textToSpeech;
// 在 onCreateView 中初始化
textToSpeech = new TextToSpeech(requireContext(), status -> {
if (status == TextToSpeech.SUCCESS) {
// 设置语音为中文
textToSpeech.setLanguage(Locale.CHINESE);
}
});
2).导航语音播报实现
//开始导航
private void startNavi() {
if (mStartPoint == null || mEndPoint == null) {
Toast.makeText(requireContext(), "请先规划路线", Toast.LENGTH_SHORT).show();
return;
}
try {
mAMapNavi.selectRouteId(currentRouteIds[selectedRouteIndex]);
mAMapNavi.startNavi(NaviType.GPS);
isNavigating = true;
updateNavigationButtons(true);
showNavigationInfoPanel();
// 这里可以添加开始导航的语音提示
if (!isMuted && textToSpeech != null) {
textToSpeech.speak("开始导航", TextToSpeech.QUEUE_FLUSH, null, null);
}
// ... 其他代码
} catch (Exception e) {
e.printStackTrace();
}
}
//导航过程中的语音提示:
@Override
public void onGetNavigationText(String text) {
try {
// 这是导航过程中的语音播报核心方法
if (!isMuted && textToSpeech != null && text != null) {
textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onGetNavigationText(int type, String text) {
onGetNavigationText(text);
}
// 到达目的地提醒
@Override
public void onArriveDestination() {
String message = "已到达目的地,本次导航结束";
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show();
// 播放语音提醒
if (!isMuted && textToSpeech != null) {
textToSpeech.speak(message, TextToSpeech.QUEUE_FLUSH, null, null);
}
// 震动提醒
if (ContextCompat.checkSelfPermission(requireContext(),
android.Manifest.permission.VIBRATE) == PackageManager.PERMISSION_GRANTED) {
android.os.Vibrator vibrator = (android.os.Vibrator)
requireContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null && vibrator.hasVibrator()) {
vibrator.vibrate(500); // 震动500毫秒
}
}
}
3). 特殊情况的语音提醒(按需选择)
// 偏航重新规划提醒
@Override
public void onReCalculateRouteForYaw() {
requireActivity().runOnUiThread(() -> {
String message = "您已偏离规划路线,正在重新规划";
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show();
if (!isMuted && textToSpeech != null) {
textToSpeech.speak(message, TextToSpeech.QUEUE_FLUSH, null, null);
}
});
}
// 拥堵重新规划提醒
@Override
public void onReCalculateRouteForTrafficJam() {
requireActivity().runOnUiThread(() -> {
String message = "前方道路拥堵,正在重新规划路线";
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show();
if (!isMuted && textToSpeech != null) {
textToSpeech.speak(message, TextToSpeech.QUEUE_FLUSH, null, null);
}
});
}
4). 交通设施语音提醒
@Override
public void OnUpdateTrafficFacility(AMapNaviTrafficFacilityInfo facility) {
if (facility == null || !isTrafficEnabled) return;
String message = "";
switch (facility.getBroadcastType()) {
case 0: // 测速摄像头
message = String.format("前方%d米有测速摄像头,限速%d公里/小时",
facility.getDistance(), facility.getLimitSpeed());
break;
case 1: // 监控摄像头
message = String.format("前方%d米有监控摄像头",
facility.getDistance());
break;
case 2: // 事故
message = String.format("前方%d米发生交通事故,请谨慎驾驶",
facility.getDistance());
break;
case 3: // 施工
message = String.format("前方%d米正在施工,请减速慢行",
facility.getDistance());
break;
case 4: // 交通拥堵
message = String.format("前方%d米出现拥堵,建议绕行",
facility.getDistance());
break;
}
if (!TextUtils.isEmpty(message)) {
final String finalMessage = message;
requireActivity().runOnUiThread(() -> {
// 显示提示信息
Toast.makeText(requireContext(), finalMessage, Toast.LENGTH_LONG).show();
// 播放语音提醒
if (!isMuted && textToSpeech != null) {
textToSpeech.speak(finalMessage, TextToSpeech.QUEUE_ADD, null, null);
}
});
}
}
5). 速度超限提醒
private void showSpeedWarning(int currentSpeed) {
String speedWarning = String.format("当前速度%d公里/小时,请注意行车安全",
currentSpeed);
Toast.makeText(requireContext(), speedWarning, Toast.LENGTH_SHORT).show();
if (!isMuted && textToSpeech != null) {
textToSpeech.speak("您的车速较快,请注意行车安全",
TextToSpeech.QUEUE_ADD, null, null);
}
}
6). 资源释放
@Override
public void onDestroy() {
super.onDestroy();
if (textToSpeech != null) {
textToSpeech.stop(); // 停止播报
textToSpeech.shutdown(); // 关闭引擎
}
}
2、实时路况监控
①路况开关控制
//MainActivity
private ImageButton btnTraffic;
private boolean isTrafficEnabled = true; // 默认开启实时路况
// 初始化实时路况按钮
btnTraffic = rootView.findViewById(R.id.btn_traffic);
// 实时路况按钮点击事件
btnTraffic.setOnClickListener(v -> {
isTrafficEnabled = !isTrafficEnabled;
aMap.setTrafficEnabled(isTrafficEnabled); // 设置地图是否显示实时路况
updateTrafficButton();
// 显示提示
String message = isTrafficEnabled ? "已开启实时路况" : "已关闭实时路况";
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show();
});
// 更新路况按钮状态
private void updateTrafficButton() {
btnTraffic.setSelected(isTrafficEnabled);
btnTraffic.setAlpha(isTrafficEnabled ? 1.0f : 0.5f);
}
XML文件
<!-- 实时路况按钮 -->
<ImageButton
android:id="@+id/btn_traffic"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="8dp"
android:background="@drawable/bg_map_button"
android:contentDescription="@string/traffic_button"
android:src="@drawable/ic_traffic" />
按钮样式: res/drawable/bg_map_button.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="oval">
<solid android:color="#E0E0E0" />
</shape>
</item>
<item android:state_selected="true">
<shape android:shape="oval">
<solid android:color="#E3F2FD" />
</shape>
</item>
<item>
<shape android:shape="oval">
<solid android:color="#FFFFFF" />
</shape>
</item>
</selector>
路况图标样式:res/drawable/ic_traffic.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"
android:drawable="@drawable/ic_traffic_on" />
<item
android:drawable="@drawable/ic_traffic_off" />
</selector>
字符串资源:在values/strings
<string name="traffic_button">实时路况</string>
②路况状态更新
@Override
public void onTrafficStatusUpdate() {
// 路况更新时的处理
if (isTrafficEnabled) {
requireActivity().runOnUiThread(() -> {
// 可以在这里更新UI
});
}
}
@Override
public void onNaviInfoUpdate(NaviInfo naviInfo) {
if (isNavigating && navigationInfoPanel.getVisibility() == View.VISIBLE && naviInfo != null) {
requireActivity().runOnUiThread(() -> {
// 更新路况状态(根据当前速度判断)
int currentSpeed = naviInfo.getCurrentSpeed();
String trafficText;
int trafficColor;
// 根据当前速度判断路况状态
if (currentSpeed < 15) { // 低于15km/h认为是拥堵
trafficText = "拥堵";
trafficColor = Color.parseColor("#F44336"); // 红色
} else if (currentSpeed < 30) { // 低于30km/h认为是缓行
trafficText = "缓行";
trafficColor = Color.parseColor("#FF9800"); // 橙色
} else { // 高于30km/h认为是畅通
trafficText = "畅通";
trafficColor = Color.parseColor("#4CAF50"); // 绿色
}
tvTrafficStatus.setText("当前路况:" + trafficText);
tvTrafficStatus.setTextColor(trafficColor);
});
}
}
③拥堵重新规划
@Override
public void onReCalculateRouteForTrafficJam() {
requireActivity().runOnUiThread(() -> {
String message = "前方道路拥堵,正在重新规划路线";
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show();
if (!isMuted && textToSpeech != null) {
textToSpeech.speak(message, TextToSpeech.QUEUE_FLUSH, null, null);
}
});
}
3、安全提醒系统
①OnUpdateTrafficFacility 方法(交通设施安全提醒)
@Override
public void OnUpdateTrafficFacility(AMapNaviTrafficFacilityInfo[] facilities) {
// 批量交通设施提醒
if (facilities == null || facilities.length == 0 || !isTrafficEnabled) return;
for (AMapNaviTrafficFacilityInfo facility : facilities) {
String message = "";
switch (facility.getBroadcastType()) {
case 0: // 测速摄像头
message = String.format("前方%d米有测速摄像头,限速%d公里/小时",
facility.getDistance(), facility.getLimitSpeed());
break;
case 1: // 监控摄像头
message = String.format("前方%d米有监控摄像头", facility.getDistance());
break;
case 2: // 事故
message = String.format("前方%d米发生交通事故,请谨慎驾驶", facility.getDistance());
break;
case 3: // 施工
message = String.format("前方%d米正在施工,请减速慢行", facility.getDistance());
break;
case 4: // 交通拥堵
message = String.format("前方%d米出现拥堵,建议绕行", facility.getDistance());
break;
}
// 发送提醒
if (!TextUtils.isEmpty(message)) {
final String finalMessage = message;
requireActivity().runOnUiThread(() -> {
// 显示提示信息
Toast.makeText(requireContext(), finalMessage, Toast.LENGTH_LONG).show();
// 语音播报
if (!isMuted && textToSpeech != null) {
textToSpeech.speak(finalMessage, TextToSpeech.QUEUE_ADD, null, null);
}
});
}
}
}
②onNaviInfoUpdate 方法中的速度监控
@Override
public void onNaviInfoUpdate(NaviInfo naviInfo) {
if (isNavigating && navigationInfoPanel.getVisibility() == View.VISIBLE && naviInfo != null) {
requireActivity().runOnUiThread(() -> {
// ... 其他代码 ...
// 速度监控
int currentSpeed = naviInfo.getCurrentSpeed();
if (currentSpeed > 80) {
String speedWarning = String.format("当前速度%d公里/小时,请注意行车安全",
currentSpeed);
Toast.makeText(requireContext(), speedWarning, Toast.LENGTH_SHORT).show();
if (!isMuted && textToSpeech != null) {
textToSpeech.speak("您的车速较快,请注意行车安全",
TextToSpeech.QUEUE_ADD, null, null);
}
}
});
}
}
③getWarningMessage 方法(天气安全提醒)
private String getWarningMessage(String weatherType) {
if (weatherType.contains("雨")) {
return "注意:当前降雨可能影响出行安全,请携带雨具,谨慎驾驶。";
} else if (weatherType.contains("雪")) {
return "注意:当前降雪可能导致路面结冰,建议减少出行。";
} else if (weatherType.contains("雾")) {
return "注意:空气质量较差,建议戴口罩出行。";
} else if (weatherType.contains("霾")) {
return "注意:空气质量较差,建议戴口罩出行。";
}
return "";
}
4、天气信息和预警
①获取高德天气服务KEY
大体的流程与上面获取高德地图KEY相同,只有一些细微的差别:
将获取的KEY复制到AndroidManifest里
②调用天气服务的方法
1)updateWeatherInfo 方法(核心方法)
private void updateWeatherInfo(LatLng destination) {
try {
// 清除旧的天气信息
clearWeatherInfo();
// 计算并显示距离
float distance = calculateDistance(null, destination);
if (distance > 0) {
final float finalDistance = distance;
requireActivity().runOnUiThread(() -> {
tvDistance.setText(String.format("距离: %.1fkm", finalDistance));
});
}
// 使用逆地理编码获取城市名称
GeocodeSearch geocodeSearch = new GeocodeSearch(requireContext());
geocodeSearch.setOnGeocodeSearchListener(new GeocodeSearch.OnGeocodeSearchListener() {
@Override
public void onRegeocodeSearched(RegeocodeResult result, int rCode) {
if (rCode == AMapException.CODE_AMAP_SUCCESS) {
// 获取城市名称
String city = result.getRegeocodeAddress().getCity();
if (TextUtils.isEmpty(city)) {
city = result.getRegeocodeAddress().getDistrict();
}
// 使用城市名称查询天气
WeatherSearchQuery query = new WeatherSearchQuery(
city,
WeatherSearchQuery.WEATHER_TYPE_LIVE
);
WeatherSearch weatherSearch = new WeatherSearch(requireContext());
weatherSearch.setOnWeatherSearchListener(NavigationFragment.this);
weatherSearch.setQuery(query);
weatherSearch.searchWeatherAsyn();
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
2)onWeatherLiveSearched 方法(天气查询回调)
@Override
public void onWeatherLiveSearched(LocalWeatherLiveResult weatherLiveResult, int rCode) {
requireActivity().runOnUiThread(() -> {
if (rCode == AMapException.CODE_AMAP_SUCCESS) {
LocalWeatherLive weatherLive = weatherLiveResult.getLiveResult();
// 更新天气信息UI
tvWeather.setText(String.format("%s %s°C",
weatherLive.getWeather(),
weatherLive.getTemperature()));
tvHumidity.setText(weatherLive.getHumidity() + "%");
tvWind.setText(String.format("%s风 %s级",
weatherLive.getWindDirection(),
weatherLive.getWindPower()));
// 更新空气质量
String airQuality = getAirQualityByWeather(weatherLive.getWeather());
tvAqi.setText("空气质量:" + airQuality);
tvAqi.setTextColor(getAirQualityColor(airQuality));
// 更新天气图标
updateWeatherIcon(weatherLive.getWeather());
// 生成出行建议
String recommendation = generateRecommendation(
weatherLive.getWeather(),
calculateDistance(mStartPoint, mEndPoint)
);
tvRecommendation.setText(recommendation);
// 显示天气警告
if (shouldShowWarning(weatherLive.getWeather())) {
tvWeatherWarning.setVisibility(View.VISIBLE);
tvWeatherWarning.setText(getWarningMessage(weatherLive.getWeather()));
}
}
});
}
3)getAirQualityByWeather 方法(获取空气质量)
private String getAirQualityByWeather(String weather) {
if (weather.contains("霾") || weather.contains("沙尘")) {
return "重度污染";
} else if (weather.contains("雾")) {
return "中度污染";
} else if (weather.contains("阴") || weather.contains("多云")) {
return "良";
} else if (weather.contains("雨") || weather.contains("雪")) {
return "良";
} else if (weather.contains("晴")) {
return "优";
} else {
return "良";
}
}
//获取空气质量对应颜色
private int getAirQualityColor(String quality) {
switch (quality) {
case "优":
return Color.parseColor("#00C853");
case "良":
return Color.parseColor("#FFB300");
case "轻度污染":
return Color.parseColor("#FB8C00");
case "中度污染":
return Color.parseColor("#F4511E");
case "重度污染":
return Color.parseColor("#C62828");
case "严重污染":
return Color.parseColor("#6A1B9A");
default:
return Color.GRAY;
}
}
4)clearWeatherInfo 方法(清除天气信息)
private void clearWeatherInfo() {
requireActivity().runOnUiThread(() -> {
if (tvWeather != null) tvWeather.setText("正在获取天气...");
if (tvHumidity != null) tvHumidity.setText("");
if (tvWind != null) tvWind.setText("");
if (tvDistance != null) tvDistance.setText("");
if (tvRecommendation != null) tvRecommendation.setText("");
if (tvWeatherWarning != null) {
tvWeatherWarning.setText("");
tvWeatherWarning.setVisibility(View.GONE);
}
});
}
上述只是调用天气服务的方法,你还要创建一个显示天气信息的布局weather_info,例如:
<!-- 天气信息卡片 -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:id="@+id/weather_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_weather_sunny"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_destination"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#333333"
android:textStyle="bold"
android:text="目的地"/>
<TextView
android:id="@+id/tv_weather"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:textColor="#666666"
android:text="晴天 26°C"/>
</LinearLayout>
<TextView
android:id="@+id/tv_distance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="#2196F3"
android:text="距离: 5.2km"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:background="#E0E0E0"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="@+id/tv_humidity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="#333333"
android:text="湿度"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="12sp"
android:textColor="#666666"
android:text="65%"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="@+id/tv_wind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="#333333"
android:text="风速"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="12sp"
android:textColor="#666666"
android:text="3级"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="@+id/tv_aqi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="#333333"
android:text="空气质量:优"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
对控件进行初始化:
// 在 onCreateView 方法中初始化天气信息相关的控件
View weatherInfo = rootView.findViewById(R.id.weather_info);
weatherIcon = weatherInfo.findViewById(R.id.weather_icon);
tvDestination = weatherInfo.findViewById(R.id.tv_destination);
tvWeather = weatherInfo.findViewById(R.id.tv_weather);
tvDistance = weatherInfo.findViewById(R.id.tv_distance);
tvHumidity = weatherInfo.findViewById(R.id.tv_humidity);
tvWind = weatherInfo.findViewById(R.id.tv_wind);
tvAqi = weatherInfo.findViewById(R.id.tv_aqi);
天气信息面板的显示控制
View weatherInfo = rootView.findViewById(R.id.weather_info);
weatherInfo.setVisibility(View.VISIBLE); // 显示天气
// weatherInfo.setVisibility(View.GONE); // 隐藏天气
5、智能出行建议
①、generateRecommendation 方法(核心方法)
private String generateRecommendation(String weatherType, float distance) {
// 根据天气类型给出建议
if (weatherType.contains("暴雨") || weatherType.contains("大雨") || weatherType.contains("雷")) {
return "当前天气恶劣,建议选择驾车出行或推迟行程,注意道路积水和打滑。";
} else if (weatherType.contains("中雨") || weatherType.contains("小雨")) {
return "当前有降雨,建议携带雨具,选择驾车出行较为安全。";
} else if (weatherType.contains("雪")) {
return "当前降雪天气,路面可能结冰,建议驾车出行并保持低速行驶。";
} else if (weatherType.contains("雾") || weatherType.contains("霾")) {
return "当前能见度较低,建议戴好防护口罩,驾车出行时开启雾灯并保持安全车距。";
}
// 根据天气和距离组合给出建议
else if (weatherType.contains("阴")) {
if (distance > 5) {
return "天气阴沉,距离较远,建议驾车出行。";
} else if (distance > 2) {
return "虽然天气阴沉,但温度适宜,可以考虑骑行或步行。";
} else {
return "距离较近,阴天适合运动,建议步行前往。";
}
} else if (weatherType.contains("晴")) {
if (distance > 5) {
return "天气晴朗,但距离较远,建议驾车出行。";
} else if (distance > 2) {
return "阳光明媚,距离适中,非常适合骑行,记得防晒。";
} else {
return "天气晴好,距离不远,建议步行,享受阳光。";
}
} else if (weatherType.contains("多云")) {
if (distance > 5) {
return "天气较好,但距离较远,建议驾车出行。";
} else if (distance > 2) {
return "云朵遮阳,温度舒适,是骑行的好天气。";
} else {
return "天气舒适,距离不远,非常适合步行。";
}
}
// 默认建议
if (distance > 5) {
return "距离较远,建议选择驾车出行。";
} else if (distance > 2) {
return "距离适中,可以考虑骑行方式。";
} else {
return "距离较近,建议步行前往。";
}
}
②、calculateDistance 方法(计算距离)
private float calculateDistance(LatLng start, LatLng end) {
try {
if (end == null) {
return 0;
}
// 获取当前位置作为起点
Location currentLocation = aMap.getMyLocation();
if (currentLocation == null) {
if (start == null) {
return 0;
}
} else {
// 使用当前实际位置作为起点
start = new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude());
}
// 计算距离
if (start != null && end != null) {
float distance = AMapUtils.calculateLineDistance(start, end);
return distance / 1000.0f; // 转换为公里
}
return 0;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
③、onWeatherLiveSearched 方法中的智能建议部分
@Override
public void onWeatherLiveSearched(LocalWeatherLiveResult weatherLiveResult, int rCode) {
requireActivity().runOnUiThread(() -> {
if (rCode == AMapException.CODE_AMAP_SUCCESS) {
LocalWeatherLive weatherLive = weatherLiveResult.getLiveResult();
// 生成出行建议
String recommendation = generateRecommendation(
weatherLive.getWeather(),
calculateDistance(mStartPoint, mEndPoint)
);
tvRecommendation.setText(recommendation);
// 更新推荐的出行方式
updateRecommendedMode(weatherLive.getWeather(),
calculateDistance(mStartPoint, mEndPoint));
}
});
}
总结:
上述的方法与代码仅供参考,当然这个项目并不完善,能修改的地方还有很多,例如:我们还可以添加一个出行方式的选择,驾车、骑车、步行、公交等等,由于篇幅有限这里就不赘述了。
} else if (weatherType.contains(“晴”)) {
if (distance > 5) {
return “天气晴朗,但距离较远,建议驾车出行。”;
} else if (distance > 2) {
return “阳光明媚,距离适中,非常适合骑行,记得防晒。”;
} else {
return “天气晴好,距离不远,建议步行,享受阳光。”;
}
} else if (weatherType.contains(“多云”)) {
if (distance > 5) {
return “天气较好,但距离较远,建议驾车出行。”;
} else if (distance > 2) {
return “云朵遮阳,温度舒适,是骑行的好天气。”;
} else {
return “天气舒适,距离不远,非常适合步行。”;
}
}
// 默认建议
if (distance > 5) {
return "距离较远,建议选择驾车出行。";
} else if (distance > 2) {
return "距离适中,可以考虑骑行方式。";
} else {
return "距离较近,建议步行前往。";
}
}
##### ②、calculateDistance 方法(计算距离)
```Java
private float calculateDistance(LatLng start, LatLng end) {
try {
if (end == null) {
return 0;
}
// 获取当前位置作为起点
Location currentLocation = aMap.getMyLocation();
if (currentLocation == null) {
if (start == null) {
return 0;
}
} else {
// 使用当前实际位置作为起点
start = new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude());
}
// 计算距离
if (start != null && end != null) {
float distance = AMapUtils.calculateLineDistance(start, end);
return distance / 1000.0f; // 转换为公里
}
return 0;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
③、onWeatherLiveSearched 方法中的智能建议部分
@Override
public void onWeatherLiveSearched(LocalWeatherLiveResult weatherLiveResult, int rCode) {
requireActivity().runOnUiThread(() -> {
if (rCode == AMapException.CODE_AMAP_SUCCESS) {
LocalWeatherLive weatherLive = weatherLiveResult.getLiveResult();
// 生成出行建议
String recommendation = generateRecommendation(
weatherLive.getWeather(),
calculateDistance(mStartPoint, mEndPoint)
);
tvRecommendation.setText(recommendation);
// 更新推荐的出行方式
updateRecommendedMode(weatherLive.getWeather(),
calculateDistance(mStartPoint, mEndPoint));
}
});
}
总结:
上述的方法与代码仅供参考,当然这个项目并不完善,能修改的地方还有很多,例如:我们还可以添加一个出行方式的选择,驾车、骑车、步行、公交等等,由于篇幅有限这里就不赘述了。
通过这个项目,对于高德地图SDK的使用,天气服务和数据处理,Android生命周期管理,异步编程和线程处理,错误处理和异常管理都有不小的收获,对于这些不熟悉的可以跟着做一下。