Android景点导游开发实践:高德地图SDK集成与定位路线规划
2026/9/10 6:02:42 网站建设 项目流程

简介:基于安卓的景点导游毕业设计源码包,面向高校计算机相关专业学生与安卓开发入门者,帮助解决毕业设计选题与落地问题。客户端完整实现用户注册登录、景点上报、景点浏览、评论购票及在线沟通、景点推荐、我的订单、个人信息与密码修改等功能;后台采用Java实现管理员端,支持用户信息、景点信息、订单信息的查看与管理。压缩包为RAR格式,体积约87.85MB,文件总数未在详情中单独列出,内容以安卓客户端和Java后台管理端的源码、配置文件及说明文档为主。已有311人学习,适合作为毕设项目参考或安卓开发练手素材。通过这份资源,读者可掌握安卓网络请求、数据存储、列表展示与后台交互等关键实现,并基于完整的业务模块进行二次开发与功能扩展。

1. Android 景点导游毕业设计,别等到答辩才发现跑不起来

屏幕前摆着“毕业设计源码-基于Android的景点导游”这个名字时,大多数人下载解压、导入 Android Studio、等 Gradle 转完第一圈,然后模拟器上白屏或崩溃——这才是拿到这份源码的真实起点。所谓景点导游,不是给你一个装好的 App,而是一个空的 Android 工程骨架:地图要接入 SDK,定位要申请权限,景点数据要么写在本地要么现拉现用,路线规划还得盯着高德地图的调用配额。这篇文章就把这套东西怎么落地讲清楚:从地图 SDK 与数据源选型,到工程搭建、POI 加载和路线绘制,最后收在 adb 连真机排错与打包验证这些最容易翻车的细节上。它面向两类人:准备拿它交毕业设计的学生,以及想快速把 LBS 能力塞进自己项目的在职工程师。

2. Android 景点导游怎么选型:地图 SDK、数据来源与工程分层

先定地图引擎。这个项目最重的第三方依赖就是地图,国内可选空间其实很小,主流就是高德地图 SDK 和百度地图 SDK,我做技术选型时一般从下表的几个维度去卡:

对比项高德地图 SDK百度地图 SDK
Key 申请控制台按包名+签名指纹下发,调试与发布各自独立同样按包名+AK 绑定,区分调试环境
定位能力高德定位完全内置,基站+GPS+Wi-Fi 融合定位定位需额外集成百度定位组件,多一层依赖
路线规划步行、骑行、驾车接口齐全,文档有现成 Demo接口类似,但步行路线返回质量略粗糙
包体积影响全量功能约 10MB,可用 ABI 过滤裁剪全量更大,需要同时裁 so 和资源
社区活跃度毕业设计相关代码片段最多,抄作业容易社区也有,但搜索到的解法往往更旧

我一般会选高德,核心理由是“少一个依赖”。百度地图的定位能力不在主 SDK 里,要多引一个定位包,对只需要在学校周边几个景点做演示的毕业场景来说,每多一个依赖,答辩预演就多一个出问题的环节。顺着这个思路往下,下一个要拍板的不是 UI 框架,而是数据从哪来。

2.1 景点数据不依赖服务器,用本地 JSON 也能把架构讲圆

毕业设计通常没有专职后端,强行上 Spring Boot 反而增加部署风险。最常见的做法是把景点数据放在app/src/main/assets/spots.json中,结构类似这样:

{ "campus": { "name": "东湖校区", "center": [114.4155, 30.5255] }, "spots": [ { "id": "s001", "name": "樱花大道", "latitude": 30.5261, "longitude": 114.4168, "description": "每年三月底盛花期,全长约八百米", "audioUrl": "assets://audio/s001.mp3", "tags": ["花景", "拍照"] } ] }

数据格式定好之后,代码里用 Gson 解析,注意流要指定 UTF-8,不然在 Windows 上编辑过的中文注释会直接乱码:

AssetManager am = context.getAssets(); InputStream is = am.open("spots.json"); List<Spot> spots = new Gson().fromJson( new InputStreamReader(is, StandardCharsets.UTF_8), new TypeToken<List<Spot>>() {}.getType());

为什么不用后端?一是答辩现场经常没有网络,数据放 assets 里断网也能演示;二是评审问“数据从哪来”,你可以说当前版本走本地缓存,同时补一句“网络层已预留 Retrofit 入口”,这一句话就能把架构完整度拉起来。只把 Retrofit 依赖加好、Service 接口定义好,业务代码不动,等真有后端时切换数据源即可。

把选型落进工程的方式,就是直接敲进app/build.gradle

implementation 'com.amap.api:3dmap:latest.integration' implementation 'com.amap.api:location:latest.integration' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

注意:高德的 3dmap 和 location 要分两行声明,写在同一行虽然不报错,但后续单独升级定位 SDK 时容易互相覆盖。compileSdk 建议和 Android Studio 当前推荐的 targetSdk 对齐,否则部分新接口会自动走兼容路径,定位回调的字段会有细微差异。

2.2 包结构分层,决定你答辩时能不能说清楚代码

我见过不少源码包把 Activity 全部堆在一个包下,几百行一个文件,评审问“代码结构”时只能支支吾吾。推荐按职责分五层:

com.example.guide ├── data // 本地 JSON 解析、Repository │ ├── model // Spot、Route、UserLocation │ └── source // SpotsRepository、JsonSource ├── map // 地图 Activity、定位回调、Marker 工具类 ├── ui // 景点列表 Fragment、详情 BottomSheet、Banner 适配器 ├── nav // 路线规划入口,步行/骑行参数封装 └── utils // 经纬度距离计算、格式化工具

分层意义不是代码洁癖,而是把“导游”业务和“地图 SDK”解耦。MapActivity 只负责地图生命周期和 Marker 点击;景点列表由 Fragment 自己加载数据;两者通过一个SpotDetailBottomSheet通信。这样即使地图 SDK 升级后某个类被标记为 deprecated,你只改 map 包,数据层完全不受影响。配套的 Repository 也不复杂:

class SpotsRepository(private val context: Context) { fun loadFromAssets(): List<Spot> { return JsonSource(context).load("spots.json") } }

2.3 用 MVVM 还是 MVC,对导游项目来说哪个更稳

这种量级的项目,我推荐用 MVVM,而且直接用官方推荐的 ViewModel + LiveData 组合。原因很具体:模拟器旋转屏幕时,Activity 会重建,如果用 MVC 在 Activity 里直接解析 JSON,转一次屏就读一次 assets;MVVM 让SpotsViewModel持有数据,旋转后直接复用,列表不闪烁。代码骨架:

class SpotsViewModel(application: Application) : AndroidViewModel(application) { private val repo = SpotsRepository(application) val spots: LiveData<List<Spot>> = liveData { emit(repo.loadFromAssets()) } val selectedSpot = MutableLiveData<Spot>() fun onMarkerClick(spot: Spot) { selectedSpot.value = spot } }

提示:ViewModel 里不要持有 MapView 或 Fragment 引用,否则退出页面后内存泄漏。所有地图操作都放到 Activity 层,用observe接收 LiveData 回调后再执行。写这块时顺手把 Kotlin 作为主力语言,Android Studio 新建项目默认就是 Kotlin,遇到问题你搜索到的攻略也基本是 Kotlin 片段,抄作业成本最低。

2.4 MapView 生命周期,是整个地图模块的地基

接入任何地图业务之前,先把MapView的五个生命周期方法写成模板:

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mapView.onCreate(savedInstanceState) } override fun onResume() { super.onResume() mapView.onResume() } override fun onPause() { super.onPause() mapView.onPause() } override fun onDestroy() { super.onDestroy() mapView.onDestroy() }

漏掉任何一个,轻则地图黑屏,重则内存泄漏,而且日志只报GLSurfaceView相关异常,很容易误导你去查 OpenGL 而不是生命周期。这五个方法的调用顺序不能调整,onCreate里拿到aMap对象后先做初始配置,再谈定位和 Marker。

3. Android 景点导游工程搭建:Gradle 校准、权限与主界面骨架

这一章解决“工程能编译”到“骨架能跑”之间的问题。很多源码下载下来编译失败,多半不是代码问题,而是环境漂移:Gradle 版本、JDK 版本、compileSdk 三者对不上。

3.1 Android Studio、SDK 与 JDK 的前置校准

Android Studio 本身安装不难,难的是装完后 SDK 组件下载不动。国内网络下经常卡在 0%,处理方式是:在 Settings → SDK Location 里指定本地 SDK 路径,并确保Android SDK Platform-Tools已勾选。这个目录里包含 adb 和 fastboot,后面连小米真机调试时全靠它。建议的工程参数基准如下:

配置项推荐值理由
compileSdk34追平新版本 androidx 编译要求
minSdk24覆盖 Android 7.0+,高德 SDK 完全支持
targetSdk34满足应用市场要求,答辩演示不受影响
JDK17(随 Studio 自带)Kotlin 与高德 SDK 均兼容
Gradlewrapper 内置版本不要手动升级大版本,容易触发兼容问题

align 好之后,app/build.gradle里的 defaultConfig 典型长这样:

android { compileSdk 34 defaultConfig { applicationId "com.example.guide" minSdk 24 targetSdk 34 versionCode 1 versionName "1.0" ndk { abiFilters "armeabi-v7a", "arm64-v8a" } } }
3.1.1 用 gradle 获取 SHA1,比命令行省事

申请高德 Key 必须用到 SHA1,用 Gradle 自带任务查看:

cd android && ./gradlew signingReport

signingReport会把 debug 与 release 变体的 SHA1、MD5、SHA256 全部打印出来。注意applicationId必须和高德控制台填写的包名一致,很多同学改包名后 Key 失效,不是 Key 错了,而是包名变更导致签名指纹不匹配。高德把 Key 与“包名+SHA1”绑定,任何一项变化都要重新申请。

3.2 AndroidManifest 权限与 Key 配置,缺一项定位就废

景点导游离不开定位权限,最低权限集合是:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />

然后在<application>标签内加高德 Key:

<meta-data android:name="com.amap.api.v2.apikey" android:value="你的高德Key" />

注意:WRITE_EXTERNAL_STORAGE从 Android 10 开始被系统忽略,但高德在写入定位缓存时仍会尝试读取,保留声明但限制maxSdkVersion="29",可以避免 targetSdk 升级时的 lint 警告。

Android 6.0 以上光声明权限不够,还得动态请求。用 Activity Result API 比老式onRequestPermissionsResult简洁得多:

private val requestLocation = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { if (it[Manifest.permission.ACCESS_FINE_LOCATION] == true) { startLocation() } else { Toast.makeText(this, "需要定位权限才能开展导游功能", Toast.LENGTH_LONG).show() } }

动态权限不是形式主义:用户第一次打开 App 时拒绝定位,后续所有“基于当前位置的景点距离排序”都会失效,所以要在用户点击“开始导游”时二次触发请求。

3.3 底部导航 + 协调布局,搭出景点与地图的切换骨架

导游 App 的常规骨架是三个 tab:景点、地图、我的。底部导航用BottomNavigationView,中间用FragmentContainerView承载,避免复杂的 Fragment 替换:

<androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_container" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_nav" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" app:menu="@menu/bottom_nav_menu" />

菜单资源如下:

<menu> <item android:id="@+id/nav_list" android:icon="@drawable/ic_list" android:title="景点" /> <item android:id="@+id/nav_map" android:icon="@drawable/ic_map" android:title="地图" /> <item android:id="@+id/nav_profile" android:icon="@drawable/ic_profile" android:title="我的" /> </menu>

景点列表页推荐用CoordinatorLayout嵌套CollapsingToolbarLayoutRecyclerView,列表下拉时顶部景点 Banner 收起、标题吸顶,交互接近旅游 App 的详情页:

<androidx.coordinatorlayout.widget.CoordinatorLayout> <com.google.android.material.appbar.AppBarLayout> <com.google.android.material.appbar.CollapsingToolbarLayout> <BannerView android:id="@+id/banner" /> <androidx.appcompat.widget.Toolbar ... /> </CollapsingToolbarLayout> </AppBarLayout> <androidx.recyclerview.widget.RecyclerView app:layout_behavior="@string/appbar_scrolling_view_behavior" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>

这里的appbar_scrolling_view_behavior是系统内置值,不要手改成自定义字符串,否则 RecyclerView 会被 AppBar 盖住。Banner 轮播用 ViewPager2 加一个 Handler 定时切页,二十行代码量级,不引第三方库也能跑通。骨架搭完,下一步就是把核心业务——地图、定位与景点展示——接进来。

4. 核心功能实战:定位、POI 加载、路线规划与列表联动

这一章把地图真正的业务逻辑串起来。先看定位:导游场景下定位是数据的锚点,没有坐标,景点距离和路线规划全部失去意义。

4.1 高德定位回调与坐标参数,直接决定演示效果

val locationClient = AMapLocationClient(this).apply { setLocationListener { loc -> if (loc.errorCode == 0) { currentLat = loc.latitude currentLng = loc.longitude aMap.moveCamera( CameraUpdateFactory.newLatLngZoom( LatLng(loc.latitude, loc.longitude), 16f ) ) } else { Log.e("Loc", "location error: ${loc.errorCode}, ${loc.errorInfo}") } } } val option = AMapLocationClientOption().apply { interval = 5000 isNeedAddress = true isOnceLocation = false } locationClient.setLocationOption(option) locationClient.startLocation()

interval是主动上报间隔,不是 GPS 点收集间隔,5 秒适合步行导游场景;如果只打算在进入景区时定一次位,把isOnceLocation设为 true 会省不少电。isNeedAddress开启后能在loc.getAddress()拿到文字地址,列表页可以直接展示“武汉市东湖新技术开发区”这样的信息,不用自己调逆地理编码接口。

4.2 景点 POI 的 Marker 展示与点击,别用 title 查对象

解析完 JSON 后,第一步不是直接aMap.addMarker,而是维护一个 Marker 列表,便于统一清理和刷新:

private val markers = mutableListOf<Marker>() fun showSpots(spots: List<Spot>) { markers.forEach { it.remove() } markers.clear() spots.forEach { spot -> val marker = aMap.addMarker( MarkerOptions() .position(LatLng(spot.latitude, spot.longitude)) .title(spot.name) .snippet(spot.description) .icon(BitmapDescriptorFactory.fromView(buildIcon(spot))) ) marker.obj = spot markers.add(marker) } }

marker.obj = spot是高频写法,比点击时遍历 Marker 集合按 title 查找效率高得多。点击回调里做两件事:把 Marker 设为选中态,并弹出详情卡片:

aMap.setOnMarkerClickListener { marker -> val spot = marker.obj as? Spot ?: return@setOnMarkerClickListener false marker.showInfoWindow() binding.spotDetailBottomSheet.showSpot(spot) true }

提示:列表页和地图联动时,点击列表项要走同一套逻辑。我的做法是让 Fragment 把 Spot 发给共享 ViewModel,MapActivity 观察后调moveCamera并高亮对应 Marker,避免两套重复代码维护两个状态源。

4.3 路线规划:从“看地图”升级成“带路”

景点导游最有“干货感”的功能,是点击详情页的“去这里”后绘制一条步行路线:

val routeSearch = RouteSearch(this) val query = RouteSearch.WalkRouteQuery( RouteSearch.FromAndTo( LatLng(currentLat, currentLng), LatLng(spot.latitude, spot.longitude) ), RouteSearch.WALK_DEFAULT ) routeSearch.calculateWalkRouteAsyn(query) routeSearch.setRouteSearchListener(object : RouteSearch.OnRouteSearchListener { override fun onWalkRouteSearched(result: RouteSearch.WalkRouteResult?, code: Int) { if (code != 1000 || result?.paths.isNullOrEmpty()) return val walkPath = result.paths[0] val list = walkPath.steps.flatMap { it.polyline.map { p -> LatLng(p.latitude, p.longitude) } } aMap.addPolyline( PolylineOptions() .addAll(list) .width(14f) .color(Color.parseColor("#1E88E5")) ) } })

这里的坑在于:步行路径是分 step 返回的,如果直接拿result.paths[0]的坐标集合去画线,会出现断线。上面用flatMap把每个 step 的 polyline 压平拼接成完整坐标链,这是路线绘制最容易出错的地方。code != 1000时,九成原因是高德控制台没有给这个 Key 开通“路线规划”服务,需要单独申请。

4.4 景点列表加载进度条,放对位置比样式重要

加载 assets 里的 JSON 本身很快,但景点语音包和图片在真机上首次读取会有卡顿,列表无反馈会显得很“死”。加一个 Material 的CircularProgressIndicator

binding.progressIndicator.visibility = View.VISIBLE viewModel.spots.observe(viewLifecycleOwner) { result -> binding.progressIndicator.visibility = View.GONE binding.recyclerView.adapter = SpotAdapter(result) }

进度条放在ConstraintLayout中间位置,数据到了就隐藏。除了加载态,还要处理一个错误态:JSON 解析失败时显示“加载失败+重试按钮”,而不是白屏。这个兜底在答辩演示时尤其重要,机器临时抽风也是评审考察的一部分。

4.5 列表与地图联动,用最小粒度刷新避免掉帧

联动时不要每次点击都重建整个 Adapter,而是定位下标后局部刷新:

fun setSelectedSpot(spotId: String) { val index = spots.indexOfFirst { it.id == spotId } lastSelected?.let { notifyItemChanged(it) } notifyItemChanged(index) lastSelected = index }

细节:Marker 点击回调在主线程,moveCamera动画期间如果同步滚动列表会掉帧。我一般不做scrollToPosition,只刷新选中状态,用户视觉焦点已经在地图上,列表停留在原位反而更自然。地图动画结束后再刷新列表选中态,是体感最好的顺序。

5. Android 景点导游打包验证:混淆、签名与 adb 连真机排错

最后一章收在验证环节。这里必须提前做一件事:把 release 签名配好,不要拿 debug 包去答辩。高德 Key 区分 debug 与 release,两者签名指纹不一样,你在控制台只申请了 debug Key,发布包跑起来定位就会静默失败。用 release 之前,先跑一遍./gradlew signingReport拿到发布变体的 SHA1,到高德控制台补一个 Key。

5.1 用 adb 连小米真机,先查这三个位置

很多同学开完“USB 调试”后发现adb devices列表还是空的,第一反应是换线。排查顺序应该是:先adb kill-server && adb start-server,再看手机上有没有授权弹窗,最后查 USB 模式是不是“仅充电”。小米设备要额外打开“USB 安装”和“USB 调试(安全设置)”,连接后常用命令:

adb devices adb shell dumpsys package com.example.guide

第二条命令能确认当前安装包的签名信息、权限列表和目标 SDK,比在设置里翻半天可靠。网上有些帖子会教你执行adb shell sh /storage/.../up.sh之类的脚本,那是给特定模块用的,不是通用操作,遇到问题先看adb devices的输出状态是device还是unauthorized

5.2 定位失败现场,先看 errorCode 再动代码

高德定位失败时,回调里的errorCode直接指向问题方向:

errorCode含义处理方式
4Key 不正确或不存在核对包名与 SHA1,重新生成 Key
7签名验证失败确认 release 包使用独立 keystore,重新签名
12缺少定位权限检查动态权限是否真正勾选,而不是只在 Manifest 声明

遇到 7 的典型路径:改过包名、换过电脑、或重新生成过 keystore 但忘记更新高德控制台。处理完 Key 后不要只点 Run,要卸载旧包重装,否则系统缓存里还是旧签名数据。

5.3 混淆配置,保住数据模型和高德 SDK

打 release 包时要保活高德 SDK 和 Gson 要反射的数据模型:

-keep class com.amap.api.**{*;} -keep class com.autonavi.**{*;} -keep class com.example.guide.data.model.**{*;} -keepclassmembers class ** { @android.webkit.JavascriptInterface <methods>; }

data.model必须 keep,原因很直接:Gson 通过反射按字段名赋值,混淆后Spotlatitude变成a,JSON 解析出来的对象全为空。验证方式:打 release 包后完整走一遍“景点列表 → 地图定位 → 路线规划”主链路,再用adb shell dumpsys package核对安装包名与密钥配置。模拟器上定位不准是正常的,但 API 调用链通不通、Marker 有没有加载、路线有没有画出,一看便知。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询