资讯动态

基于Android Studio的旅游轨迹记录与分享APP开发实战

发布时间:2026/10/6 5:44:31 来源:尧图企业网站定制
简介基于Android Studio开发的旅游记录与分享APP源码定位为Android进阶学习者与毕业设计学生的完整参考项目。项目围绕路线规划、旅行记录与社交分享三条主线展开利用Google Maps API与GPS定位自动绘制行程、记录位置点并生成时间轴日志同时实现分享、评论、点赞和关注机制构成可运行的旅游社区雏形。功能实现上贯穿数据存储SQLite、网络请求OkHttp/Retrofit、Android运行时权限管理以及MVP/MVVM架构并考虑位置服务节能策略展示从需求分析到最终打包APK的完整开发链路。包体共432个文件以150个Java源码、143个XML界面布局、88个PNG图片以及so动态库、JAR库和Gradle脚本为主压缩包仅19.17MB目录模块清晰便于按功能拆解钻研。已有2876人学习下载适合想通过实战案例掌握地图集成、轨迹绘制与移动端社交功能开发的读者。1. 基于 Android Studio 的旅游记录与分享 APP先搞懂它解决什么网上能下载到很多基于 Android Studio 开发的旅游记录与分享 APP 源码但多数人拿到手的第一步就错了先改界面、换地图样式、换按钮颜色却不关心轨迹是怎么从定位芯片一路存进数据库的。这类项目的核心不是晒照片而是三件事记录时后台定位能不能撑一天不掉点回放时路线跟真实游玩顺序对不对得上分享后别人拿到的路书有没有阅读价值。下边的内容按“把源码真正改懂”来写从定位权限、前台服务、数据表设计到路线回放与分享落地最后是真机上反复踩过的五个坑。适合三类人做毕设或课程设计需要完整 Android 项目的学生、用现有源码改产品原型的团队、已经跑通 demo 但想让它在真实场景里稳定的 Android 开发。方向对了照着改出一个可用版本两三天足够。2. 先把定位和轨迹记准前台服务、权限与坐标采集策略2.1 地图 SDK 与定位权限Android Studio 项目里先配这几项这类源码最常见的卡点不在业务代码而在地图 Key 和权限配置。国内做旅游记录地图 SDK 基本在高德和百度之间选它们都拆成了定位 SDK 和地图 SDK可以分开接。我一般建议先只接定位 SDK把坐标拿到本地再说地图显示往后放。一来减少集成报错二来定位才是记录类 APP 的地基。打开下载到手的 Android Studio 工程先别急着跑按下面顺序核对在开放平台申请 Key包名填你工程里 build.gradle 的 applicationIdSHA1 要 debug 和 release 各取一份。SDK 接入方式看工程已有配置有的把 jar/aar 放在 app/libs有的走 Maven 依赖两种都能跑通别重复添加。核对 targetSdkVersion这决定权限和后台限制走哪套规则。权限声明是第一个容易漏的地方。minSdk 和 targetSdk 不同时需要的权限不一样下面是一份覆盖 Android 8 到 Android 14 的清单uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_BACKGROUND_LOCATION / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_LOCATION /ACCESS_BACKGROUND_LOCATION 从 Android 10 开始单列必须在用户已经同意前台定位之后才能申请不能一上来就弹。Android 14 要求前台服务声明具体类型FOREGROUND_SERVICE_LOCATION 就是给轨迹记录用的。Android 11 以后如果用户只在弹窗里选了“仅在使用时允许”后台定位的开关会被系统隐藏只能引导用户去设置页单独授权这个交互流程源码里往往没做要自己补。提示Key 配不上最典型的症状是定位能用、地图白屏。取 debug 签名 SHA1 用这条命令keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkeyrelease 签名打正式包时再取一次两个都填到开放平台后台。新手阶段建议先把 Android Studio 基础设置顺一遍装个中文语言包菜单里的“运行”“调试”位置搞清楚不然后面看报错日志都得找半天这是我们带人入门时最先解决的事。2.2 前台服务与坐标采集让轨迹跑完一整天的写法一条旅游路线通常连续记录两三个小时以上锁屏后普通 Service 很快会被系统回收。想在 Android 8 以上长期运行只有前台服务加常驻通知这一条公认稳妥的路。前台服务不单是技术需要通知栏上“正在记录旅程”也在告诉用户定位还在工作这是产品体验的一部分。class TrackRecordService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { startForeground(NOTIFICATION_ID, buildNotification()) tracker LocationTracker(this) tracker?.start(intervalMillis 3000L) return START_STICKY } private fun buildNotification(): Notification { val channelId track_record val manager getSystemService(NotificationManager::class.java) if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { manager.createNotificationChannel( NotificationChannel(channelId, 轨迹记录, NotificationManager.IMPORTANCE_LOW) ) } return NotificationCompat.Builder(this, channelId) .setContentTitle(正在记录旅程) .setContentText(回放和分享都在这里生成) .setSmallIcon(R.drawable.ic_track) .setOngoing(true) .build() } }onStartCommand 返回 START_STICKY系统把进程杀掉后有机会重新拉起 Service但不保证完全不丢点。通知渠道在 Android 8.0 以后必须显式创建否则前台服务一启动就闪。IMPORTANCE_LOW 让通知不响铃避免记录轨迹时打扰用户。intervalMillis 传 3000 是步行推荐值骑行建议 2000开车 1000越密线越平滑但电量涨得快还要配合距离阈值移动不足 10 米不写库能省不少电。启动前台服务在 Android 8 以上有专门入口val intent Intent(context, TrackRecordService::class.java) if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { startForegroundService(intent) } else { startService(intent) }Android 8 以上不能直接 startService 一个前台服务必须走 startForegroundService然后在 onStartCommand 里及时调 startForeground。如果 5 秒内没调系统会抛 ForegroundServiceDidNotStartInTimeException这个异常在源码集成时很常见注意调用时机要在 App 处于前台时发起。2.3 坐标过滤与抽稀别把轨迹画成乱麻GPS 在城市里会跳点人站在原地坐标也能在几十米范围内乱飘。再就是记录一天可能攒下上万点地图折线绘制会很卡。所以原始点写库前要过滤展示前要抽稀这两步不能省。public boolean shouldSave(Location newLoc, Location lastLoc) { if (lastLoc null) return true; float distance newLoc.distanceTo(lastLoc); // 单位米 float deltaTime (newLoc.getTime() - lastLoc.getTime()) / 1000f; // 单位秒 float speed distance / Math.max(deltaTime, 0.1f); if (distance 5f) return false; // 原地漂移 if (speed 30f deltaTime 5f) return false; // 单点速度突变 return true; }distance 小于 5 米的点直接丢掉能解决大部分“站在原地画圈”的怪相。speed 大于 30 米每秒且间隔小于 5 秒的点多半是瞬时漂移先缓存不落库等下一个点回来再判断。这个过滤逻辑看不出业务价值但它直接决定回放效果不然后面所有分享素材都是乱线。抽稀算法通常用 Douglas-Peucker核心思路是保留曲线形状去掉冗余点public static ListLatLng simplify(ListLatLng points, double toleranceMeters) { if (points.size() 2) return points; LatLng start points.get(0); LatLng end points.get(points.size() - 1); double maxDist 0; int index -1; for (int i 1; i points.size() - 1; i) { double d distanceFromSegment(points.get(i), start, end); if (d maxDist) { maxDist d; index i; } } if (maxDist toleranceMeters index ! -1) { ListLatLng left simplify(points.subList(0, index 1), toleranceMeters); ListLatLng right simplify(points.subList(index, points.size()), toleranceMeters); ListLatLng result new ArrayList(left); result.addAll(right.subList(1, right.size())); return result; } return Arrays.asList(start, end); }递归逻辑是每次找离首尾连线最远的点如果超过容差就按该点切开左右递归否则整段用直线代替。容差给 5 米适合步行路线骑行给 10 米车行可以给 15 米给得越大点越少路线越粗糙。这个算法在回放和绘制前调用能把上万点压到几百个点地图滑动才不卡。3. 从坐标点到一条能看的路线Room 存储、地图绘制与轨迹回放3.1 路线数据结构怎么设计三张表还是两张表有些 demo 为了省事把一条轨迹的所有经纬度拼成 JSON 字符串塞进一个字段。点少的时候没问题真旅游一天几千点上万点后读写都会变慢断点续录还要整串重写。常见做法是把轨迹拆成主表和点表另外加一张媒体表结构清晰查询也快。表作用关键字段track一条旅程的总记录id, title, startTime, endTime, distanceMeters, coverUri, statustrack_point这条旅程的坐标点id, trackId, lat, lng, altitude, speed, timestamp, orderNotrack_media路上的照片与随笔id, trackId, mediaUri, mediaType, timestamp, lat, lngtrackId 建索引回放时按 orderNo 排序。media 表存照片经纬度这样照片可以钉在地图对应位置分享时长图里也能把照片缩略图贴到路线拐点上。Entity( tableName track_point, indices [Index(trackId), Index(orderNo)] ) data class TrackPoint( PrimaryKey(autoGenerate true) val id: Long 0, val trackId: Long, val lat: Double, val lng: Double, val altitude: Float, val speed: Float, val timestamp: Long, val orderNo: Int )orderNo 在插入时按递增写回放不依赖自增主键 id因为批量插入时自增顺序和实际顺序可能不一致will 出现乱序回放。索引 orderNo 是让“按顺序取点”这条查询稳定走索引轨迹越长越明显。媒体表不建议单独存图片本体存 URI 就够了图片文件放应用私有目录或者由系统相册管理数据库只记地址。这样表体积小备份也快。3.2 把轨迹画到地图上折线、起点终点与多段线颜色很多源码把地图页做成 MapView 加底部信息卡片套一个 ScrollView 就够用。我也见过为了视觉效果硬上 CoordinatorLayout 加 Banner 的复杂结构结果地图生命周期一乱白屏、内存泄漏全冒出来。地图页布局能简单就别复杂重点在画线。拿高德举例折线绘制思路是所有地图 SDK 通用的类名略有差异PolylineOptions options new PolylineOptions(); options.addAll(simplifiedPoints); options.width(14f); options.color(Color.parseColor(#33AA66)); options.geodesic(true); aMap.addPolyline(options);width 14f 在多数手机上是一条清晰的线太细的时候拍照截图发朋友圈路线几乎看不清。geodesic(true) 表示用球面大圆连线短距离看不出差别但跨城路线不会画穿地心。分段着色也常用已经走过的一段实线还没走的一段虚线只需把点集按时间切段对每段分别 addPolyline。起点终点用两个 Marker 区分底部信息卡片显示总里程和用时。注意 Marker 的 anchor 参数图标底部尖角要对准坐标点不然起点标记看起来总偏离实际位置一点这个小细节在截图分享时会被放大。3.3 轨迹回放用进度条控制坐标序列播放回放本质是让摄像头沿着轨迹点序列动起来。定时器推进索引把一个小 Marker 移到新点同时把地图中心 moveCamera 过去。播放控制条用 Android 原生的进度条组件就可以进度值绑定当前索引与总点数之比。class TrackPlayer( private val points: ListTrackPoint, private val callback: (index: Int, point: TrackPoint) - Unit ) { private var handler Handler(Looper.getMainLooper()) private var index 0 private var speedRate 1f fun play() { if (index points.size - 1) return handler.removeCallbacksAndMessages(null) handler.postDelayed(tick, (1000 / speedRate).toLong()) } private val tick object : Runnable { override fun run() { if (index points.size) { stop(); return } callback.invoke(index, points[index]) index handler.postDelayed(this, (1000 / speedRate).toLong()) } } }每次 tick 推进一个点回调里更新 Marker 位置并移动镜头。speedRate 为 2 时1000 除以 2 等于 500 毫秒走一个点也就是 2 倍速。想要按真实速度播放把间隔改成相邻点的时间戳差值就行但一天的行程按真实速度播根本播不完所以源码里一般提供 2 倍速、4 倍速、16 倍速几档本质就是改 postDelayed 的时长。播放器停止时记得 removeCallbacksAndMessagesActivity 退出后 Handler 还在跑会报空指针这是回放功能最常见的崩溃来源尤其用户切后台再回来的时候。4. 把路线做成可分享的内容路线卡片、长图和文本分享的落地做法4.1 生成可分享的路线卡片文本、缩略图与 ACTION_SEND分享按钮在源码里的实现核心是一个 Intent.ACTION_SEND。先拼一段可读性强的文本再决定传文字还是图片。文本必须能独立成段这样图片加载失败时别人也能看懂这条路线讲了什么。fun shareTrack(context: Context, track: Track) { val text buildString { append(我在${track.startTime}走了一条${formatDistance(track.distance)}的路线) append(起点${track.startName}终点${track.endName}。) append(全程${track.photoCount}张照片${track.noteCount}条随笔。) } val sendIntent Intent(Intent.ACTION_SEND).apply { type text/plain putExtra(Intent.EXTRA_SUBJECT, track.title) putExtra(Intent.EXTRA_TEXT, text) } context.startActivity(Intent.createChooser(sendIntent, 分享路线)) }formatDistance 里把里程换算成公里保留一位小数用户看到的是“6.3 公里”而不是“6300 米”这个差别直接影响分享文案的观感。到这一步分享链路已经很完整用户复制文字、转发到微信或微博都行。但想让分享更抓眼球还得加一张路线图。4.2 把地图当前视野截成长图Canvas 与快照两条路路线图最稳的做法是地图截图。地图 SDK 提供 snapshot 方法回调返回当前视野的 Bitmap这是生成分享图的基础素材。拿到 Bitmap 后把标题、里程、日期拼到底部组成一张卡片。fun buildShareCard(mapBitmap: Bitmap, trackText: String): Bitmap { val width mapBitmap.width val height mapBitmap.height 120 val card Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas Canvas(card) canvas.drawBitmap(mapBitmap, 0f, mapBitmap.height.toFloat(), null) val paint Paint(Paint.ANTI_ALIAS_FLAG).apply { color Color.WHITE } canvas.drawRect(0f, mapBitmap.height.toFloat(), width.toFloat(), height.toFloat(), paint) paint.color Color.BLACK paint.textSize 18.dp2px canvas.drawText(trackText, 16.dp2px, mapBitmap.height 36.dp2px, paint) return card }第一种是用地图快照 Canvas 拼卡片文字位置全靠手动算但可控性最强。第二种是把 MapView 和文字控件放进一个容器布局等布局测量完成用 Canvas 把整个布局画出来好处是文案排版用 Android 控件排坏处是布局尺寸不稳定时容易截出空白。我一般用第一种写死底部 120 像素文字区出图稳定。卡片存到应用缓存目录通过 FileProvider 拿 content:// URI 交给分享 Intent。Android 7 以后直接传 file:// 会抛 FileUriExposedException这个坑源码里反复出现后面避坑章还会细说。4.3 分享后别人怎么打开深链、落地页与未登录兜底发图片和文本分享闭环已经成立。想引导朋友装 App 看完整路线就需要深链。在 Manifest 里给目标 Activity 加一个 intent-filter分享文本里带一个自定义协议链接。intent-filter action android:nameandroid.intent.action.VIEW / category android:nameandroid.intent.category.DEFAULT / category android:nameandroid.intent.category.BROWSABLE / data android:schemetour android:hosttrack / /intent-filter分享文本里带上 tour://track/123 这种地址装了 App 的用户点开直接进详情页。没装 App 的人点了会报错所以常见做法是配一个 HTTP 落地页页面判断设备是否安装应用装了就唤起深链没装就引导下载。源码里如果没有现成落地页至少要做到被分享的路线允许游客浏览编辑才要求登录。强登录会砍掉大部分分享转化这个判断和功能无关是产品经验。分享形式实现成本适合场景注意点纯文本最低微信群、微博文案要包含路线标题和关键数字单张长图中朋友圈宽高比控制在 3:4 到 9:16深链较高促进 App 回流未登录时允许游客浏览5. 避坑旅游路线 APP 从开发到上真机的 5 次翻车记录这些坑不是地图 SDK 独有的是做定位记录类应用绕不开的血泪经验。每一条都按现象、原因、解决的顺序写对照着查能省大半天时间。5.1 地图白屏调试版能开测试机黑屏现象模拟器和自己的调试机一切正常换一台测试机后地图白屏只有网格和标准控件。 原因地图 Key 的包名或 SHA1 不匹配。debug 签名和 release 签名是两套申请 Key 时只填了其中一套。多人协作时每个人本机 debug 签名还不一样。 解决到开放平台后台把 debug 和 release 的 SHA1 都加上用前面给的 keytool 命令分别取。打正式包前用 product flavors 或者至少打一个 release 包验证。这个问题最坑的地方在于定位功能不受影响看起来像手机兼容性问题实际上是 Key 校验失败回到开发平台看鉴权日志就能确认。5.2 锁屏半小时后轨迹断流Service 被系统回收现象通知栏提示还在回来一看轨迹中间少了两小时路线活生生断成两截。 原因部分手机 ROM 的省电策略会清理自启动服务前台服务也不是绝对保险。另一个高频原因是 targetSdk 31 以上时前台服务必须由 App 在前台的状态下启动时机不对直接被系统拦住。 解决向用户申请“忽略电池优化”权限把 Service 的 onDestroy 兜底做成重新拉起同时记录上次写入时间。App 回到前台时发现断点超过一分钟自动把前后两段轨迹合并不要把一条路线劈成两半展示。这个兜底逻辑要从数据层面解决只靠 Service 重启不保证点不丢。5.3 路线图跨越河面坐标漂移没过滤现象轨迹从起点正常走突然一个点跨到河对岸再跳回原路线地图上出现一条不存在的飞跃线。 原因过隧道、高架桥或高楼密集区时定位芯片拿到坏值入库前没有过滤。 解决把第 2.3 节的 shouldSave 过滤逻辑加上。如果发现整段信号丢失比如隧道里没 GPS要按“记录中断”处理回放时显示“信号中断”提示片头而不是把两端用直线硬连起来。坐标过滤不是可有可无的优化它决定路线能不能真实反映游玩过程。5.4 图片发不出去分区存储下 file:// 失效现象App 内看照片没毛病点分享到微信图片发送失败日志里看到 FileUriExposedException。 原因Android 7 起禁止跨应用直接分享 file:// 路径targetSdk 29 以上又强制分区存储外部存储访问受限。 解决把图片复制到 app 的 cache 目录用 FileProvider.getUriForFile 生成 content:// URI分享 Intent 加上 FLAG_GRANT_READ_URI_PERMISSION。顺带解决一个边界问题用户删掉原图片后分享卡片依然能用缓存的副本生成不会崩。5.5 Release 包闪退混淆规则忘给地图 SDK 加 keep现象debug 包没问题release 签名包一进地图页面就闪退报 ClassNotFoundException 或 so 库加载失败。 原因build.gradle 里开了 minifyEnabled true但没给地图 SDK 加混淆白名单。R8 把 SDK 里通过反射调用的类删掉了。 解决在 proguard-rules.pro 里统一加上地图 SDK 的 keep 规则比如 -keep class com.amap.api.** {*;}。从源码开始改的时候不要只盯功能代码先把混淆配置完整看一遍这通常是改包第一步要做的事不然所有功能白调。安卓打包的正式流程里混淆配置检查应该排在签名之前。6. 进阶验证你的轨迹到底算不算一条好路线功能写完只是开始验证才是把项目从“能跑”变成“能用”的分水岭。这里分享一个我一直在用的自检方法用 SQL 直接查轨迹连续性。SELECT a.timestamp AS current_ts, b.timestamp AS prev_ts FROM track_point a JOIN track_point b ON a.orderNo b.orderNo 1 WHERE a.trackId :id AND (a.timestamp - b.timestamp) 60000;这条查询找出所有相邻点间隔超过 60 秒的位置也就是轨迹断点。间隔越大说明当时丢点越严重。每版本发布前跑一遍把断点列出来看断在哪个区域、持续多久再决定是调采样频率、补前台服务兜底还是加过滤逻辑。这个自检脚本比肉眼盯地图回放高效得多。真机验证也有一套固定流程打开开发者选项里的“模拟位置”间隔调成 2 秒铺一条人工轨迹先在办公室里验证回放、分享、深链这些链路是否正常。模拟位置能测逻辑测不了天线性能所以发布前还要带着手机出去骑一圈共享单车把真实路线录回来和实际走法对照。重点看三件事路线是否连续、过桥和隧道时有没有飞跃、拐弯处轨迹是否圆润。这些体验问题在代码层面很难提前发现只有真实场景能暴露。我自己后来养成一个习惯每次大版本改动后先跑一遍 SQL 断点检查再骑单车实测同一段路回来导出轨迹比对。这个流程帮我拦住了不少发版事故比临时翻代码管用得多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑