Android自助旅游系统开发:行程编排、地图定位与Room持久化
简介这套基于Android手机平台的自助旅游系统毕业设计资源专为需要完成Android课程设计、毕业设计或旅游类APP项目的在校生与初级开发者准备围绕“景点查询GPS定位”两条主线实现了用户注册登录、地图多模式切换、未来三天天气预报、步行与乘车路径规划、周边住宿餐饮娱乐检索、区域进出音乐提醒以及轨迹保存与动画回放等完整功能可直接作为毕设选题的落地参考。压缩包共含2000个文件以java源码、xml布局与配置、png图片资源为主另有json数据、so库、jar依赖、sql数据库脚本等类型整体136.23MB目录按工程结构组织便于按模块对照学习。已有305人学习下载。借助这份资源既能快速理清Android网络请求、地图SDK接入、定位服务、数据持久化等关键技术的实现思路也能获得一套结构完整、可运行可二次开发的工程模板对顺利完成毕业设计或提升Android实战能力都很有价值。1. 出门不想跟团手机才是最好的领队一个人出门旅行最怕的不是迷路而是“下一站去哪”这个问题在手机电量还剩 20% 的时候蹦出来。跟团有人安排路线但行程死、起得早、吃得不自由自己玩自由却要把地图、攻略、车次、天气、预算捏在一起手动算每次换城市都像重新做一遍数学题。这套基于 Android 手机平台的自助旅游系统要解决的就是这个问题把“行程编排”这件原本靠人脑完成的事交给手机端的一条龙模块在地图上完成路线规划、停留时间分配、费用预估和天气提醒。对 Android 工程师来说它不只是几个页面拼一块而是一套包含定位、地图渲染、本地存储和后台线程调度的完整工程适合有 Android 基础、想练手或真要拿去用的开发者。建议把阅读重点放在系统分层和数据流上这两块决定了你后面加功能会不会越加越乱。2. 自助旅游系统的分层架构与核心数据模型2.1 为什么自助旅游系统不能只写 Activity市面上很多旅行 App 的问题是“页面太多、逻辑太散”每一个景点详情页都自己拉数据、自己存缓存结果用户划过三个城市就开始卡。Android 端自助旅游系统要撑住多目的地、多日期、多交通方式的组合就得先定边界。我在搭这套系统时采用的模块划分是三个纵向层加一个横向服务分层责任边界关键类/组件展示层地图选点、行程时间轴、费用卡片Activity、Fragment、RecyclerView业务层行程编排、顺路计算、费用汇总TripPlanner、BudgetCalculator数据层城市库、POI 缓存、行程快照Room Database、DataStore横向是定位与地图服务它在后台独立运行用 Android 系统的 LocationManager 和高德地图 SDK 给上面三层提供“人在哪、周围有什么”的能力。为什么把数据层单独拎出来因为自助旅游场景里离线比在线更重要。景区和山区网络不稳定用户刚把一个城市的点选完突然进隧道如果所有数据都要实时拉行程就断了。数据层用 Room 把城市、景点、行程三张表落本地业务层在联网时同步断网时直接读本地副本。2.2 行程数据模型的五张核心表旅游系统的数据不像电商订单那么规整它的特点是“嵌套”一天行程里包含多个景点每个景点又有交通、预算、停留时间三个维度。用一张大表去存会让字段膨胀到没法维护所以我把模型拆成五张表city城市基础信息cityId 主键cityNamelat/lng时区poi景点或餐饮点poiIdcityId 外键poiNamecoordinaterecommendedDurationMincostLeveltrip一次旅行计划tripIdtripNamestartDateendDatetotalBudgetday_plan某一天的具体安排day_plan_idtripId 外键datecityIdpoiOrderListexpense_item单项费用itemIddayPlanId 外键categoryamount其中 poi 表里的 recommendedDurationMin 是行程编排算法的核心输入用户可以不填系统会用分类默认值兜底餐饮 45 分钟、博物馆 120 分钟、自然风光 150 分钟。2.2.1 用 Room 建表时要注意外键约束数据层用的是 Android 官方推荐的 Room建表 SQL 里必须把外键的 onDelete 策略定为 CASCADE否则用户删掉一个城市后关联的 day_plan 记录会变成孤儿数据。Entity(tableName day_plan, foreignKeys ForeignKey( entity Trip.class, parentColumns tripId, childColumns tripId, onDelete CASCADE)) public class DayPlan { PrimaryKey(autoGenerate true) public long dayPlanId; public long tripId; public String date; public String poiOrderList; // JSON 字符串例如 [poi_1001,poi_1002] }这里把 poiOrderList 存成 JSON 字符串而非关系表是因为一天的景点顺序是用户反复拖拽调整的每次调整都写一行关系记录太重。读取时用 Gson 解析成 List 再按顺序查 poi 表整体性能损耗可以忽略。2.3 行程计算的线程模型编排行程是 IO 密集加计算密集混合的操作要按经纬度查 POI、要算两点间距离、要按时间窗口排序。如果这些都在主线程跑用户手指拖动地图时会出现明显掉帧。我的做法是把 TripPlanner 做成一个单例内部持有 ExecutorService核心线程数设置为 4。所有计算任务用 CompletableFuture 包装耗时超过 300 毫秒的任务会自动降级到线程池执行结果通过 LiveData 回调回 UI。Android 12 以上系统对后台线程的调度更激进建议把线程优先级设为 THREAD_PRIORITY_BACKGROUND避免抢占 UI 绘制资源。ExecutorService plannerExecutor new ThreadPoolExecutor( 2, 4, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue(128), r - { Thread t new Thread(r, trip-planner); t.setPriority(Process.THREAD_PRIORITY_BACKGROUND); return t; });线程池大小不是越大越好。自助旅游系统在单机端的并发任务量是有限的同时进行的计算通常只有“当前城市的行程编排”和“后台同步天气”两件事4 个线程足够。如果开 16 个线程反而会在中低端 Android 设备上引起 CPU 争抢。3. 自助旅游系统的关键 Android 组件选型与参数设置3.1 地图选型和定位参数的真实调整过程自助旅游系统绕不开地图。地图 SDK 选高德而不是 Google 的原因不是功能对比而是国内 POI 数据丰富度和定位纠偏能力。高德在室内场景商场、车站的定位精度明显更好而且离线地图包对本地缓存友好这点对旅游场景非常关键。在使用高德地图 SDK 时有几个参数是必须手工调的setLocationMode(LocationMode.Hight_Accuracy)同时使用 GPS、Wi-Fi 和基站定位setInterval(2000)定位间隔 2 秒。旅游场景下用户是移动的5 秒太久1 秒耗电太快setOnceLocation(true)放在最后确认景点坐标时定位权限的申请在 Android 6.0 以后是运行时权限。旅游系统涉及连续定位需要在 AndroidManifest 里声明ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION同时在代码里用 ActivityResultLauncher 请求。还有一个容易被忽略的点Android 10 以上如果要在后台持续定位必须额外申请ACCESS_BACKGROUND_LOCATION权限审核严格的应用商店会要求你提供用途说明旅游应用写“持续追踪用户位置以提供导航服务”就能过。3.2 为什么进度条的实现要用自定义 View热搜词里高频出现“android进度条”“android中协调布局banner”说明很多开发者在做旅行行程展示时卡在“动态进度”的 UI 上。行程时间轴如果只用系统的 HorizontalProgressBar看起来太简陋用协调布局加载轮播图又有大量的嵌套滑动冲突。我在做行程时间轴时选择自定义 View 实现了一个分段进度条把一天的行程按时间段切成若干段每段用不同颜色代表交通、观光、餐饮。核心代码只有三个方法。Override protected void onDraw(Canvas canvas) { float totalWidth getWidth() - paddingLeft - paddingRight; for (int i 0; i segmentCount; i) { float left paddingLeft totalWidth * (i / (float) segmentCount); float right paddingLeft totalWidth * ((i 1) / (float) segmentCount); Paint paint new Paint(); paint.setColor(segmentColors[i]); canvas.drawRoundRect(left, top, right, bottom, 8f, 8f, paint); } }这段代码的关键在于totalWidth要扣掉左右 padding不然进度条首尾会贴边。segmentCount由顶层的旅游路线数决定比如一条“故宫-景山公园-南锣鼓巷”路线就是 3 段。每段的宽度按停留时长比例分配而不是等宽这样用户一眼就能看出哪个景点占了半天。3.3 瀑布流 banner 和协调布局的摩擦点如果首页要做景点推荐天然适合瀑布流卡片嵌套在 CoordinatorLayout 里。这里最容易踩的坑是 NestedScrollView 和 RecyclerView 的滑动冲突。我的统一解法是不用 NestedScrollView 套 RecyclerView而是把整个首页做成一个 RecyclerView用多类型 Item 模拟顶部 banner 和瀑布流区。androidx.coordinatorlayout.widget.CoordinatorLayout com.google.android.material.appbar.AppBarLayout com.google.android.material.appbar.CollapsingToolbarLayout app:layout_scrollFlagsscroll|exitUntilCollapsed / /com.google.android.material.appbar.AppBarLayout androidx.recyclerview.widget.RecyclerView android:layout_widthmatch_parent android:layout_heightmatch_parent app:layout_behaviorstring/appbar_scrolling_view_behavior / /androidx.coordinatorlayout.widget.CoordinatorLayoutexitUntilCollapsed是这几个 flag 里最重要的它保证 banner 在用户向上滑时先收起但保留一个最小高度的逻辑标题栏。如果不加这个 flagbanner 会跟着列表一起滑走用户想看当前城市名就必须滚回顶部旅游场景下这是很糟糕的体验。4. 核心功能落地从 POI 解析到顺路行程编排4.1 用 GeoJSON 做景点边界解析自助旅游系统的地图选点功能需要知道“用户点的是哪个景点”但高德 SDK 的点击返回的是经纬度不是 POI 名称。折中方案是维护一份景点边界 GeoJSON 数据把北京故宫的范围多边形存下来用户点击地图后用射线法判断点击坐标是否落在某个多边形内。public static boolean isInsidePolygon(LatLng point, ListLatLng polygon) { boolean inside false; int n polygon.size(); for (int i 0, j n - 1; i n; j i) { LatLng pi polygon.get(i); LatLng pj polygon.get(j); if ((pi.latitude point.latitude) ! (pj.latitude point.latitude)) { double intersectX (pj.longitude - pi.longitude) * (point.latitude - pi.latitude) / (pj.latitude - pi.latitude) pi.longitude; if (point.longitude intersectX) { inside !inside; } } } return inside; }这个算法的计算量跟多边形顶点数成正比。故宫这种复杂边界的 GeoJSON 有几百个顶点单次判断在毫秒级完全不需要上空间索引。但如果你的系统要覆盖整个城市的所有景点建议把 GeoJSON 按区县切片用地图的缩放级别决定加载哪一层否则启动时解析 5 万个顶点会卡。4.2 行程编排时间和距离的贪心算法旅游行程编排本质是“给定起点、景点列表、每个景点的推荐停留时长求一条时间最优路线”。这是一个类似旅行商问题的场景但景点数量通常不超过 8 个所以我选择了贪心算法而不是动态规划。贪心的策略是从当前位置出发每次选择“到达时间 游览时间”最短的未访问景点作为下一站。public ListPoi planRoute(LatLng start, ListPoi poiList) { ListPoi route new ArrayList(); LatLng current start; long elapsedMinutes 0; while (!poiList.isEmpty() elapsedMinutes maxDailyMinutes) { Poi best null; long bestCost Long.MAX_VALUE; for (Poi p : poiList) { long travelMinutes (long) (distance(current, p.coordinate) / 50.0); long cost travelMinutes p.recommendedDurationMin; if (cost bestCost) { bestCost cost; best p; } } route.add(best); elapsedMinutes bestCost; current best.coordinate; poiList.remove(best); } return route; }距离转时间时/ 50.0代表假设平均车速 50 公里/小时。实际场景里城市拥堵路段根本跑不到这个数所以还要加一个加权系数周一至周五早高峰乘 1.5晚高峰乘 1.2周末根据城市级别调整。这些参数不要写死在代码里而是存到 DataStore 里用户设置里留一个“步行速度”调整项会大幅提升真实可用性。4.3 蓝牙信标做景点近场识别热搜里有“android ble开发实战 心率监测app”Ble 这个方向在旅游系统中同样有用。很多博物馆和景区铺设了蓝牙信标手机经过时会触发语音讲解。原理是扫描 BLE 广播包读取信标的 major/minor 编码映射到景点 ID。扫描 BLE 设备在 Android 上需要运行时权限而且不同 ROM 对后台扫描的冻结策略不同小米尤其激进。要做近场识别建议用前台 Service 持有唤醒锁。每次扫描窗口 1 秒、休息 4 秒这个节奏既省电又能保证 3-5 米内不漏报。ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setReportDelay(0) .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) .build();SCAN_MODE_LOW_LATENCY模式下扫描结果返回最快但功耗也最高。MATCH_NUM_MAX_ADVERTISEMENT是让回调把符合条件的广播包尽量全量上报减少漏检。如果你只在讲解场景才扫描建议在进入景点时手动开关扫描而不是常开。4.4 拍照识别边框与信息提取很多人会在旅游时拍下景点门口的导览图系统如果能自动识别图片里的文字并提取“开放时间”“门票价格”会非常有价值。Android 端的做法是先用 CameraX 的 ImageAnalysis 做实时取景检测检测到文字后用 ML Kit 的 Text Recognition 做 OCR再把识别结果按关键词正则匹配进 expense_item 表。OCR 提取的关键坑是中文识别率。ML Kit 对印刷体中文导览图效果不错但对行书、草书的景区字牌识别率会掉到 50% 以下。我的策略是不要让用户依赖自动提取而是把 OCR 结果放到一个可编辑的预览框里用户确认或修改后才入库。这个交互设计比优化模型参数更有用。5. 调试定位、离线兜底与真机验证的常见处理5.1 用 adb shell 检查应用数据目录自助旅游系统开发中调试最频繁的问题是“数据库到底存进去没有”。这时候不要只靠 logcat直接用 adb 进到应用私有目录看文件。adb shell run-as com.example.tourtrip ls databases/ sqlite3 tour_database.db .tables select * from poi limit 5;run-as在 debug 包上有效release 包签名后就不能用了。所以建议在 debug 构建类型里设置android:debuggabletrue而 release 版本不要开。如果存储路径涉及/storage/emulated/0/Android/data/这个目录Android 11 以上分区存储的强制目录可以用adb shell sh /storage/emulated/0/android/data/...方式访问但更推荐用 Android Studio 的 App Inspection 面板看数据库内容那比敲命令行直观得多。5.2 wifi 强度测试和弱网模拟自助旅游系统经常在景区用景区 wifi 信号不稳定所以要对弱网做专项验证。Android 模拟器自带网络延迟模拟真机可以用手机热点加代理工具做限速。测试场景延迟设置丢包率预期表现正常30ms0%地图加载 2s弱网300ms5%缓存行程正常显示断网-100%离线 POI 可浏览编辑不可提交弱网测试时重点观察行程编排的线程池任务是否超时堆积。如果排队任务超过 128新任务会被 RejectedExecutionException 打断需要在异常处理器里做降级——直接使用上一次规划结果并弹提示“网络较差已展示上次路线”。5.3 用 content:// 协议排查崩溃真机上报的 Crash 日志里经常出现content://com.ss.android.uri.key/external_root/android/data/...这类 Uri这通常是第三方 SDK 或系统 FileProvider 的路径解析问题。旅游系统如果在 Android 11 及以上版本没有正确配置 FileProvider访问外部存储目录就会报这个错。在 AndroidManifest 里配置好 FileProvider 后代码里应该用FileProvider.getUriForFile统一转换 Uri不要直接拼路径。另外一个小技巧是如果 logcat 里出现failed to find provider先看应用是不是 targetSdkVersion 30 以上只要 target 30 以上读写非媒体文件就必须走 SAF 或 FileProvider这是最常见也是通过热搜词反查最多的崩溃原因。5.4 最后的验证技巧断网重连全流程把规划好的行程存入本地后建议在设置页放一个“开发者模式”里面包含“清空缓存并重新拉取”按钮。这个功能在开发期能快速模拟新用户首次进入在生产期也能应对用户报告“数据不对”时的远程排查。清空逻辑用 DialogFragment 做二次确认点击后调用 DataStore 的updateData清空集合。这个流程每次上线前都跑一遍能发现大约 80% 的缓存掩码问题。系统稳定后可以把范围扩大给测试机设置“飞行模式 10 秒后恢复”检查行程页面是否自动重新拉取数据。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →