资讯详情

资讯详情

Android跑步App开发实战:从9文件工程到轨迹抽稀与SQLite存储

简介这是一套基于 Android Studio 与 Java 开发的运动跑步类 App 完整源码面向具备一定安卓基础、希望练习定位与数据可视化开发的在校学生或移动开发初学者。项目围绕跑步场景实现实时速度记录、跑步路径绘制、历史数据履历管理与单次运动详情查看等核心功能并集成百度地图 SDK 完成轨迹展示适合作为课程设计、毕业设计或练手项目的参考实现。压缩包共 129 个文件约 18.03MB其中 22 个 java 源文件承载业务逻辑32 个 xml 负责界面布局与资源定义56 个 png 提供图标与图片素材另含 so 库、jar 包及 gradle 构建脚本工程结构完整可直接导入。目前已有 2540 人学习下载读者可借此理解定位数据采集、轨迹绘制与本地数据存储的完整链路并参考其模块划分与地图接入方式快速搭建自己的跑步应用雏形。1. 从一份只有 9 个文件的 Android 跑步工程说起如果你手头正好有一份 Android Studio 跑步 App 的源码包解压后只看到gradlew.bat、两个build.gradle、settings.gradle、gradle-wrapper.jar、一个Android_Map3D_SDK_V6.8.0_20190401.jar、一个mapActivity.java外加两个.gitignore第一反应大概是这也太素了。但恰恰是这种“骨架级”工程最能看清一个运动跑步 App 的底层逻辑——实时速度怎么算、跑步路径怎么画、履历数据怎么存、详情页怎么读。它适合两类人一类是想用 Android Studio Java 从零搭一个可跑通的运动记录 Demo 的开发者另一类是手里已有类似工程但被地图 SDK 集成、定位回调、数据持久化卡住的从业者。这份资源的价值不在界面多华丽而在于它把“定位 → 速度 → 轨迹 → 存储 → 回看”这条链路用最少的文件摆了出来方便你直接改、直接调、直接验证。2. 工程骨架与地图 SDK 集成9 个文件怎么撑起一个跑步 App2.1 先认清每个文件在 Gradle 体系里的角色拿到工程别急着点 Run先把文件按职责分堆。settings.gradle决定有哪些模块参与构建根目录build.gradle管全局仓库和插件版本模块级build.gradle管依赖、compileSdk、minSdk和AndroidManifest里的权限声明。gradlew与gradlew.bat是 Gradle Wrapper 的启动脚本前者给 macOS/Linux后者给 Windowsgradle-wrapper.jar是它们真正调用的执行体。两个.gitignore一个管根目录、一个管模块目录避免把build/、.idea/、本地local.properties提交上去。真正跟业务相关的只有mapActivity.java和那个Android_Map3D_SDK_V6.8.0_20190401.jar。常见做法是先确认settings.gradle里include :app的模块名跟实际目录一致再打开模块级build.gradle看dependencies里有没有用implementation files(libs/Android_Map3D_SDK_V6.8.0_20190401.jar)把本地 jar 引进来。如果 jar 放在libs/下却没写这行编译期不会报错运行到地图初始化才会抛ClassNotFoundException这是最典型的“编译过、运行崩”。// 模块级 build.gradle 关键片段 android { compileSdk 30 defaultConfig { applicationId com.example.runner minSdk 21 // 地图 SDK 一般要求 21 以上 targetSdk 30 versionCode 1 versionName 1.0 } // 本地 jar 不走远程仓库必须显式声明 sourceSets { main { jniLibs.srcDirs [libs] } } } dependencies { implementation fileTree(dir: libs, include: [*.jar]) implementation files(libs/Android_Map3D_SDK_V6.8.0_20190401.jar) implementation androidx.appcompat:appcompat:1.3.1 }上面这段里fileTree会把libs下所有 jar 都纳入编译files(...)是更精确的单文件引用两者留一个即可重复引用不会报错但会让依赖关系变模糊。jniLibs.srcDirs指向libs是为了让 SDK 自带的.so动态库能被正确打包进 APK——很多地图 SDK 的定位、渲染能力在 native 层漏了这行地图能显示但定位回调永远不来。2.2 地图 SDK 初始化的顺序不能乱mapActivity.java是整个工程里唯一承载业务逻辑的文件它通常继承Activity或AppCompatActivity在onCreate里完成地图初始化、定位监听注册、UI 控件绑定三件事。顺序很关键必须先SDKInitializer.initialize(getApplicationContext())再mapView findViewById(...)最后mapView.onCreate(savedInstanceState)。如果先拿 MapView 再初始化 SDK部分版本会直接空指针。// mapActivity.java 初始化骨架 public class mapActivity extends AppCompatActivity { private MapView mapView; private AMap aMap; private LocationClient locationClient; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 1. 隐私合规接口必须先调用否则 SDK 拒绝工作 SDKInitializer.initialize(getApplicationContext()); setContentView(R.layout.activity_map); // 2. 绑定地图控件并走生命周期 mapView findViewById(R.id.map_view); mapView.onCreate(savedInstanceState); if (aMap null) { aMap mapView.getMap(); } // 3. 定位客户端设置回调间隔 locationClient new LocationClient(getApplicationContext()); LocationClientOption option new LocationClientOption(); option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy); option.setInterval(2000); // 2 秒一次跑步场景够用 locationClient.setLocationOption(option); locationClient.startLocation(); } }setInterval(2000)是跑步记录的常见取值太密500ms会让轨迹抖动明显、耗电飙升太疏10s会漏掉转弯点画出来的路径像折线。Hight_Accuracy模式会同时用 GPS 和网络定位室内测试时可能漂移建议真机户外验证。注意SDKInitializer.initialize必须在setContentView之前且要在隐私合规弹窗确认后调用否则新版本 SDK 会静默不返回定位结果——这个坑我在三个不同项目里都踩过。3. 实时速度与轨迹绘制从定位回调到折线落图3.1 速度不是定位 SDK 直接给的要自己算很多新手以为定位回调里带getSpeed()就能直接用实际上跑步场景下这个值经常为 0 或跳变。稳妥做法是用两次定位点的距离除以时间差。距离计算用Location.distanceBetween它内部走的是 Haversine 公式比手写经纬度差值靠谱。// 速度计算与轨迹点收集 private LatLng lastPoint null; private long lastTime 0; private ListLatLng trackPoints new ArrayList(); private void onLocationChanged(AMapLocation location) { if (location.getErrorCode() ! 0) return; // 定位失败直接丢弃 LatLng current new LatLng(location.getLatitude(), location.getLongitude()); long now System.currentTimeMillis(); if (lastPoint ! null) { float[] results new float[1]; Location.distanceBetween( lastPoint.latitude, lastPoint.longitude, current.latitude, current.longitude, results); float distance results[0]; // 单位米 long dt now - lastTime; // 单位毫秒 if (dt 0) { double speed distance / (dt / 1000.0); // 米/秒 updateSpeedUI(speed * 3.6); // 转 km/h } } lastPoint current; lastTime now; trackPoints.add(current); drawTrack(trackPoints); }distanceBetween的results[0]是米dt/1000.0把毫秒转秒乘 3.6 得 km/h。这里有个细节如果两次定位间隔内用户原地停留distance可能只有 1~2 米算出来速度接近 0UI 上会闪一下建议加个阈值过滤比如distance 3时不更新速度显示。3.2 轨迹绘制用 Polyline别每次重建地图画路径最直接的方式是aMap.addPolyline()但如果在每个定位回调里都clear()再重画地图会闪、性能也差。正确做法是维护一个Polyline对象用setPoints()增量更新。// 轨迹折线增量更新 private Polyline trackLine; private void drawTrack(ListLatLng points) { if (trackLine null) { PolylineOptions options new PolylineOptions() .width(12) // 线宽跑步轨迹建议 10~15 .color(Color.parseColor(#FF3B30)) .geodesic(true); // 按地球曲率绘制长距离更准 trackLine aMap.addPolyline(options); } trackLine.setPoints(points); // 整体替换内部做差量渲染 }geodesic(true)在几公里以上的跑步轨迹里能明显减少直线偏差短距离感知不强但建议开着。width(12)是像素值不同屏幕密度下视觉粗细不同严格做法是用dp转px但 Demo 阶段直接给固定值也能跑。注意setPoints传入的列表不要用同一个可变 List 反复 add 后直接传部分 SDK 版本会持有引用导致渲染异常稳妥做法是传new ArrayList(points)。3.3 定位权限与后台保活的最小配置跑步 App 最怕锁屏后定位断掉。AndroidManifest.xml里至少要声明ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION、FOREGROUND_SERVICEAndroid 10 以上还要加ACCESS_BACKGROUND_LOCATION。运行时权限申请用ActivityCompat.requestPermissions用户拒绝后要引导去设置页否则定位回调永远不触发。!-- AndroidManifest.xml 权限片段 -- 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.INTERNET /INTERNET权限容易被忽略但地图 SDK 加载瓦片、做网络定位都要它漏了会表现为地图白屏、定位只返回网络粗略点。后台保活这块常见做法是起一个前台 Service 并绑定通知栏把定位逻辑放 Service 里Activity 只做展示。Demo 工程里如果只在 Activity 里跑定位锁屏几分钟后系统就会限制回调频率这是“跑着跑着轨迹断了”的头号原因。4. 跑步履历存储与详情页SQLite 表设计与数据读取4.1 一次跑步存一条主记录轨迹点单独存表履历管理最忌讳把整条轨迹序列化成字符串塞进一个字段查详情时解析慢、也没法做统计。合理设计是两张表run_record存每次跑步的汇总开始时间、总距离、总时长、平均速度run_point存轨迹点关联 record_id、经纬度、时间戳、瞬时速度。-- 跑步履历表结构 CREATE TABLE run_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, start_time INTEGER NOT NULL, -- 毫秒时间戳 end_time INTEGER, total_distance REAL DEFAULT 0, -- 米 total_duration INTEGER DEFAULT 0, -- 秒 avg_speed REAL DEFAULT 0 -- km/h ); CREATE TABLE run_point ( id INTEGER PRIMARY KEY AUTOINCREMENT, record_id INTEGER NOT NULL, latitude REAL NOT NULL, longitude REAL NOT NULL, timestamp INTEGER NOT NULL, speed REAL DEFAULT 0, FOREIGN KEY (record_id) REFERENCES run_record(id) );run_record的total_distance在跑步结束时由trackPoints累加算出avg_speed用总距离除以总时长再乘 3.6。run_point加record_id索引详情页按record_id查点序列时不会全表扫。注意 SQLite 没有真正的FOREIGN KEY强制约束除非开PRAGMA foreign_keysON删除记录时要手动删关联点否则会留孤儿数据。4.2 详情页读取先查汇总再按需加载轨迹详情页不要一进去就把所有点全查出来画图几万条点的跑步记录会让页面卡死。常见做法是分两步先查run_record拿汇总数据渲染头部再异步查run_point画轨迹或者只查抽稀后的点比如每 10 个取 1 个做预览。// 详情页数据读取 public RunDetail loadDetail(int recordId) { RunDetail detail new RunDetail(); // 1. 汇总 Cursor c db.rawQuery( SELECT * FROM run_record WHERE id ?, new String[]{String.valueOf(recordId)}); if (c.moveToFirst()) { detail.totalDistance c.getDouble(c.getColumnIndexOrThrow(total_distance)); detail.totalDuration c.getInt(c.getColumnIndexOrThrow(total_duration)); detail.avgSpeed c.getDouble(c.getColumnIndexOrThrow(avg_speed)); } c.close(); // 2. 轨迹点按时间排序 ListLatLng points new ArrayList(); Cursor pc db.rawQuery( SELECT latitude, longitude FROM run_point WHERE record_id ? ORDER BY timestamp ASC, new String[]{String.valueOf(recordId)}); while (pc.moveToNext()) { points.add(new LatLng(pc.getDouble(0), pc.getDouble(1))); } pc.close(); detail.trackPoints points; return detail; }getColumnIndexOrThrow比getColumnIndex更安全列名写错会直接抛异常而不是返回 -1 导致读到脏数据。轨迹点查询加ORDER BY timestamp ASC是必须的否则画出来的线会乱跳。如果点数超过 5000建议在 SQL 层做抽稀比如WHERE id % 5 0视觉上几乎无差别但渲染快很多。4.3 履历列表的排序与分页履历页通常按start_time DESC排最新的跑步在最上面。数据量大了要分页LIMIT 20 OFFSET n是标准做法但OFFSET很大时性能下降可以用WHERE start_time 上一页最后一条的时间做游标分页。-- 游标分页比 OFFSET 更稳 SELECT * FROM run_record WHERE start_time ? ORDER BY start_time DESC LIMIT 20;这个查询配合start_time上的索引翻到第 100 页也不会慢。run_record表建议在start_time上建索引CREATE INDEX idx_start_time ON run_record(start_time DESC);。很多 Demo 工程忘了建索引几十条数据时无感上千条后列表加载明显变慢这是典型的“小数据量掩盖的性能债”。5. 避坑与排查跑步 App 最容易翻车的五个点5.1 地图一片空白定位图标也不出现现象Activity 起来了MapView 区域全白日志没有明显报错。原因通常是三选一AndroidManifest里漏了INTERNET权限地图 SDK 的 Key 没配或配错jniLibs.srcDirs没指向libs导致.so没打包。解决先adb logcat | grep -i map看 SDK 有没有输出鉴权失败再检查build.gradle的sourceSets最后确认 Key 的包名和签名 SHA1 跟当前构建一致。Debug 和 Release 签名不同Key 要分别申请。5.2 速度显示忽大忽小静止时也有数值现象手机放桌上不动速度在 0~5 km/h 之间跳。原因GPS 冷启动阶段精度差定位点漂移几十米两次漂移点一算就有速度。解决用location.getAccuracy()过滤精度大于 30 米的点直接丢弃同时给速度加滑动平均比如取最近 3 次的速度均值再显示。跑步场景下这个处理能让数值稳定很多。5.3 锁屏后轨迹断成几段现象跑步中途锁屏回来发现轨迹中间缺了一大段。原因Activity 进入后台后定位回调被系统限制或者进程被回收。解决把定位逻辑挪到前台 Service通知栏常驻LocationClientOption里设置setLocationMode为高精度并允许后台定位Android 10 以上引导用户授予“始终允许”定位权限。这是跑步 App 的必修课没有捷径。5.4 履历详情页打开就卡死现象点进一条长距离跑步记录页面白屏几秒后 ANR。原因主线程里查了几万个轨迹点并一次性画到地图上。解决轨迹查询放子线程画图前做抽稀Polyline用setPoints而不是循环add单点。如果地图 SDK 支持开启setCustomTexture或简化渲染模式也能缓解。5.5 换手机后数据全没了现象用户换设备履历清空。原因数据只存在本地 SQLite没有同步机制。解决Demo 阶段可以接受但产品化必须加导出/导入功能比如把run_record和run_point序列化成 JSON 存到外部存储或云端。常见做法是用Room替代裸 SQLite配合WorkManager做后台上传但那是另一个工程量级了。6. 进阶技巧用抽稀算法把轨迹点压到十分之一轨迹点抽稀是跑步 App 从“能跑”到“好用”的分水岭。原始定位每 2 秒一个点跑一小时就是 1800 个点画在地图上没问题但存库、上传、跨端同步都会变重。我一般用 Douglas-Peucker 算法做抽稀它能在保留路径形状的前提下把点数压到 10%~20%。// Douglas-Peucker 轨迹抽稀 public static ListLatLng simplify(ListLatLng points, double tolerance) { if (points.size() 3) return points; int first 0, last points.size() - 1; ListLatLng result new ArrayList(); result.add(points.get(first)); simplifySection(points, first, last, tolerance, result); result.add(points.get(last)); return result; } private static void simplifySection(ListLatLng pts, int start, int end, double tol, ListLatLng out) { double maxDist 0; int index -1; for (int i start 1; i end; i) { double d perpendicularDistance(pts.get(i), pts.get(start), pts.get(end)); if (d maxDist) { maxDist d; index i; } } if (maxDist tol) { simplifySection(pts, start, index, tol, out); out.add(pts.get(index)); simplifySection(pts, index, end, tol, out); } }tolerance是容忍距离单位跟坐标一致。跑步场景我一般取 5~10 米太小压不掉点太大轨迹会变成折线。perpendicularDistance是点到线段的垂直距离实现时注意经纬度不能直接当平面坐标算短距离内误差可接受长距离建议先投影到米制坐标。抽稀后的点存库详情页画图时再按需插值视觉上几乎看不出差别。验证抽稀效果有个笨办法但很管用把原始轨迹和抽稀轨迹用不同颜色叠在同一张地图上跑几圈看两条线是否贴合。如果抽稀线明显切弯道内角说明tolerance给大了。我自己的习惯是每次改完抽稀参数都强制在户外跑一公里对比一次室内 GPS 漂移会让判断完全失真。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →