简介:基于Android平台、采用Java开发的跑步App完整项目源码,面向Android初学者和需要完成课程设计的学生,可用于快速掌握移动端应用开发流程。资源内置用户注册登录、计步传感器监测、运动计时、任务目标设定、跑步记录持久化存储等功能模块,并附带演示视频和工程代码。整个压缩包共330个文件,大小33.01MB,主要包含48个java源文件、145个xml布局与配置、60个png素材、11个so动态库以及2个mp4演示视频,另有gradle构建脚本、jar依赖库和readme说明文档,项目结构清晰,适合导入Android Studio直接运行调试。目前已有397人学习下载,通过对照视频和源码,可以深入理解Android生命周期管理、SensorManager传感器数据处理、SQLite数据库操作以及异步任务等核心知识点;同时,从布局设计、事件响应到本地存储的完整链路,也为后续独立开发其他类型App提供了可复用的思路与经验。
1. 基于Android的跑步App源码包,拿到手先看这三处
在GitHub、CSDN和各类源码分享站上,基于Android的跑步App源码包是下载量靠前的品类,zip里通常是一份Android Studio工程、一段演示视频和一份或详或略的README。跑步App是Android开发里典型的综合练习,定位、地图、前台服务、传感器、数据库和图表几乎全沾边,所以很多人拿它当期末大作业、毕设甚至面试作品。这篇不去评价某个具体包的好坏,而是把这类源码该有的模块、几个关键数字的算法、让工程在新版Android Studio里跑起来的步骤,以及真机验证时最容易踩的GPS漂移和进程被杀这两个坑一次讲清。手里正好有这个zip、或者打算从零搭一个的人,都能按这条线走完。
2. 跑步App的核心架构:定位、前台服务与地图三块怎么搭
跑步App的骨架可以抽象成一条链路:定位模块持续产出坐标,前台服务保证采集进程不被系统回收,数据落库后算出距离和配速,最后地图SDK把坐标点连成轨迹。绝大多数源码包的工程结构都围绕这条链路展开,先把这条链看懂,再改代码就不容易迷路。
2.1 定位方案的选型:LocationManager与FusedLocationProvider的取舍
安卓定位有两条主流路线。老工程常用原生LocationManager,直接在onLocationChanged回调里收坐标,代码直观、无额外依赖;Google系工程则用FusedLocationProviderClient(GMS)做融合定位,同时用GPS、Wi-Fi和基站信号,冷启动出位置快、相对省电,但设备上没装GMS就跑不起来。
| 对比项 | LocationManager | FusedLocationProviderClient |
|---|---|---|
| 额外依赖 | 无 | 需引入play-services-location |
| 首次定位速度 | 纯GPS冷启动慢 | 融合定位,几秒内出粗略位置 |
| 真机兼容性 | 所有Android设备 | 依赖GMS,部分国行机型不可用 |
| 典型场景 | 教学演示、离线源码 | 面向海外市场的产品级App |
我拿到源码包,第一件事是看build.gradle里有没有com.google.android.gms.location这个依赖。如果是LocationManager版本,改一下定位频率和最小位移阈值就能跑通;如果是GMS版本,先确认测试机带不带GMS,否则定位回调永远不会触发,界面就停在"正在定位"上。
提示:如果包用的是老式LocationManager,建议保留原实现先把流程跑通,不要一上来就迁到FusedLocationProvider。依赖、GMS检测、回调时机三处要一起改,调试成本直接翻倍。
2.2 前台服务是跑步App存活的前提
定位本身不难,难在"持续定位"。Android 8.0之后后台执行被严格限制,普通Service里长时间盯GPS,几分钟内就会被系统回收。跑步App的标准做法是启动前台服务,配合一条常驻通知,让系统知道"用户正在计时跑步"。
class RunService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val channelId = "run_channel" val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager manager.createNotificationChannel( NotificationChannel(channelId, "跑步记录", NotificationManager.IMPORTANCE_LOW) ) val notification = Notification.Builder(this, channelId) .setContentTitle("正在跑步") .setContentText("累计时间 00:00:00") .setSmallIcon(R.drawable.ic_run) .setOngoing(true) .build() startForeground(1, notification) // 在这里注册定位监听,把坐标写入Repository长期持有 return START_STICKY } }这段代码的关键是startForeground(1, notification)。从Android 8.0开始,前台服务必须在启动后5秒内调用它,否则抛出ForegroundServiceStartNotAllowedException;Android 13又把通知权限改成了运行时申请,老源码包里经常漏掉POST_NOTIFICATIONS这一条,结果通知不显示、服务被视为非法。START_STICKY表示进程被系统杀掉后会尝试重建服务,但重建时定位监听和已记录的坐标都要自己恢复,严谨的做法是把上一段轨迹缓存到SharedPreferences里,启动时重新读取。
2.3 轨迹地图:Polyline把坐标点串成路线
地图展示一般用高德或百度,国内源码包里高德占多数,因为它不需要GMS、在国行设备上兼容性更好。跑步轨迹的本质是Polyline:拿到一串LatLng点按顺序连成折线,再在起点和终点各画一个Marker。这里有个容易被忽略的点:定位点不是均匀分布的,等红绿灯时坐标会堆在一起,画出来的轨迹边缘发毛,这个留到轨迹平滑部分处理。
地图SDK接入有三步:加依赖、在AndroidManifest里填API Key、在Application里做初始化。初始化代码通常是这样的:
class RunApplication : Application() { override fun onCreate() { super.onCreate() AMapLocationClient.updatePrivacyShow(this, true, true) AMapLocationClient.updatePrivacyAgree(this, true) MapsInitializer.initialize(this) } }updatePrivacyShow和updatePrivacyAgree是高德SDK近几个版本的硬性合规要求,必须在定位前调用,否则SDK直接抛异常拒绝工作。很多源码包是两三年前写的,SDK版本落后,这段初始化代码往往是导入后第一个要补的地方;如果包里的高德SDK版本正好是旧的,加上这两行也不会冲突。
3. 距离、配速和卡路里:跑步数据是用这几个公式算出来的
演示视频里跑一圈,界面上的距离、配速、卡路里同步在跳。这些数字不是地图SDK白给的,是源码自己算的。算得对不对,直接决定这个App能不能真用来记录跑步。
3.1 用Haversine计算相邻定位点距离
球面上两点间距离不能直接套勾股定理。常见做法是Haversine公式,把经纬度转成弧度后按大圆距离计算,公里级距离上误差约0.5%,记录跑步完全够用。
fun haversineMeters(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double { val radius = 6371000.0 val dLat = Math.toRadians(lat2 - lat1) val dLon = Math.toRadians(lon2 - lon1) val a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2) return 2 * radius * Math.asin(Math.sqrt(a)) }总距离是相邻两点逐段累加。这个函数有三处值得注意:一是radius取6371000米是平均半径,对跑步场景够用;二是返回单位是米,累加时别和公里混用;三是不要在循环里频繁创建Location对象调distanceTo(),虽然它内部也是球面算法,但每次都要new对象,在1秒一次的高频采集中会产生明显GC压力。另一个大坑是静止漂移:手机放桌上GPS也会跳几米到十几米,跑步前站着系个鞋带,如果不做过滤,能凭空累积出三五十米"假距离"。
3.2 配速的平滑处理
配速是跑步App的核心指标,常规口径是"移动时间除以移动距离",单位min/km。难点在"移动"的定义:停下来等红灯的那几十秒算不算?业界常见做法是设定一个速度阈值,低于阈值判定为静止,静止时长不计入移动时间,这样算出来的才是moving time配速。
data class TrackPoint( val lat: Double, val lon: Double, val time: Long, val speed: Float ) fun calcPace(points: List<TrackPoint>): String { var moveSec = 0.0 var totalMeters = 0.0 for (i in 1 until points.size) { val prev = points[i - 1] val cur = points[i] val dist = haversineMeters(prev.lat, prev.lon, cur.lat, cur.lon) val dt = (cur.time - prev.time) / 1000.0 val spd = if (dt > 0) dist / dt else 0.0 if (spd >= 0.5) { moveSec += dt totalMeters += dist } } val paceMin = (moveSec / 60.0) / (totalMeters / 1000.0) val mm = paceMin.toInt() val ss = ((paceMin - mm) * 60).toInt() return "%d'%02d".format(mm, ss) }这段里的speed取自Location.getSpeed(),也有源码用distance/dt自己算,两者在GPS信号差的时候差异很大。0.5 m/s这个阈值不是铁律,常规做法是抽成常量按场景可调:慢走模式要降到0.3,跑完拉伸时如果阈值太低,拉伸的时间会被算进移动时间,配速虚低。另一个常见误用是直接用总时长除总距离,这叫elapsed pace,和显示在表盘上的实时配速混为一谈,产品定位完全不同。
3.3 卡路里估算口径与身高体重参数
卡路里没有精确公式,所有App都是估算。常见口径是MET代谢当量法:热量(kcal)=MET值×体重(kg)×时长(h)。跑步的MET按速度分档,源码里通常维护一张速度区间到MET值的映射表,体重从用户设置里读。
| 速度区间 | MET值 | 对应场景 |
|---|---|---|
| 小于6 km/h | 6.0 | 快走、慢跑热身 |
| 6~10 km/h | 8.3 | 常规慢跑 |
| 10~14 km/h | 11.0 | 中高速跑 |
MET表和分档方式各家差异很大,同一个包换一张表,结果能差20%。如果是期末大作业或面试作品,卡路里没必要追求绝对准确,但一定要在设置页或接口注释里写清"基于MET法估算",否则被追问"怎么验证你的卡路里准不准"时很难收场。年龄、性别、心率对卡路里的影响在这个口径下被忽略了,这是可接受的简化,但要在文档里声明。
4. 把源码跑起来:Android Studio、权限和API Key三个必调参数
源码包解压后第一件事不是点Run。老工程在新版Android Studio里直接跑,十有八九挂在Gradle同步或SDK版本上。按下面的顺序排查,基本能在十分钟内见到首屏。
4.1 导入工程后的Gradle与SDK版本适配
先看gradle/wrapper/gradle-wrapper.properties里的distributionUrl,再看根build.gradle里的AGP版本和compileSdk。很多分享的源码compileSdk还停在30或31,新装的Android Studio默认JDK 17、Gradle 8,AGP低于7.0直接不兼容。常见做法不是改工程去迁新版,而是装一个与工程匹配的JDK版本。
# 在工程根目录确认当前Gradle版本 cat gradle/wrapper/gradle-wrapper.properties # 常见匹配关系(按这个组合试最快) # Gradle 7.4 + AGP 7.x: 适合 compileSdk 32/33,JDK 11 # Gradle 8.2 + AGP 8.x: 适合 compileSdk 33/34,JDK 17| 典型报错 | 原因 | 处理方式 |
|---|---|---|
| Could not find com.android.tools.build:gradle | 工程指定的AGP版本号有问题或已被仓库清理 | 手改AGP版本并确认google()仓库在repositories里 |
| Unsupported class file major version 61 | JDK太新,AGP太老 | File→Project Structure里把JDK切到11 |
| INSTALL_FAILED_INSUFFICIENT_STORAGE | 模拟器数据分区太小 | AVD Manager里编辑模拟器,扩大data分区 |
工具链匹配这事,演示视频基本不会讲,因为它录制的环境和你现在差了好几个版本。我的做法是先看包内有没有local.properties或jdk配置记录,没有就按上表从JDK版本开始逐个试,比在build.gradle里盲改Gradle版本快得多。
4.2 权限声明与运行时申请
跑步App的权限比普通App多,而且Android大版本不同,声明位置和政策都不同。AndroidManifest里至少要包含这些:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />FOREGROUND_SERVICE_LOCATION是Android 14新增的,老源码包里没有,targetSdk升到34后不加这一条,前台服务里申请定位权限会直接崩溃。ACTIVITY_RECOGNITION是Android 10引入的,用于识别用户处于跑步还是静止状态,如果包里还接了蓝牙心率带,通常还要补BLUETOOTH_SCAN。运行时申请用Activity Result API写最省事:
val launcher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { result -> val fine = result[android.Manifest.permission.ACCESS_FINE_LOCATION] ?: false if (!fine) { // 跳转设置页让用户手动打开,光弹Toast没用 } } launcher.launch(arrayOf( android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACTIVITY_RECOGNITION ))权限这关最常见的故障是只弹了定位申请、没弹通知权限,Android 13上通知直接不显示,前台服务失去常驻通道,锁屏后进程很容易被杀。面试或答辩时能把Android 10、13、14三个版本的权限变化说清楚,比背概念得分高。
4.3 地图SDK的API Key配置与签名匹配
高德和百度地图的Key是按"包名+签名SHA1"绑定的。源码包里通常带着作者自己的Key,你直接打包运行,地图区域会显示"鉴权失败"水印或者干脆白屏。自己的Key要去开放平台申请,配置在AndroidManifest的application节点里:
<meta-data android:name="com.amap.api.v2.apikey" android:value="你的Key" />SHA1这个坑尤其隐蔽。Android Studio跑调试包用的是系统生成的debug签名,打release包用的是你自己的签名文件,两套SHA1不一样,同一个Key只绑了一个SHA1的话,就会"调试能显示、发布不能显示"。我一般把debug和release配置两个不同Key,或者统一用同一个签名文件签名调试包。
注意:直接改包名SDK也会失效。如果源码里的applicationId和你自己的包名不一致,要去开放平台重新申请绑定新包名的Key,只改SHA1不够。
5. 真机验证:排掉GPS漂移和后台被杀这两个坑
5.1 用模拟器定位模拟做初步验证
Android Studio模拟器的Extended Controls里有Location面板,可以设置多个经纬度坐标点作为线路循环发送,也可以直接输入坐标做单点跳转。注意模拟器默认位置是固定的,要勾选循环发送坐标,否则距离永远是0。模拟器适合验证UI流程和数据计算,但GPS精度值、速度值这类硬件相关的逻辑必须在真机上验,模拟的定位点精度值一般都返回一个固定大值,会把精度过滤逻辑带偏。
5.2 GPS漂移过滤的三个阈值
漂移过滤是跑步App源码里最值得改的业务逻辑。我一般维护三个阈值:精度阈值,坐标的Accuracy大于20米的点不入库;速度阈值,瞬时速度超过25 m/s(90 km/h)判定为异常,可能是坐车或GPS跳变;单点位移阈值,相邻两点距离超过500米且时间间隔小于10秒判定为跳变,直接回退到上一个有效点。三个阈值配合,轨迹基本不会出现"穿楼"或"瞬间闪现到隔壁街区"的现象。阈值要写成可在设置页调试的配置,不要硬编码在算法类里。
5.3 后台定位保活与电池优化白名单
Android 6.0之后的Doze模式会限制后台定位频率,Android 12之后系统还会自动重置未使用应用的电池优化白名单。跑步App一旦锁屏,定位频率可能被系统拉低到几乎不更新。正规做法是引导用户把App加入电池优化白名单,通过Intent跳转:
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) intent.data = Uri.parse("package:$packageName") startActivity(intent)这个Intent需要在运行时申请,且先判断isIgnoringBatteryOptimizations返回是否为false,AndroidManifest里不能直接声明这个权限。真机上验证保活是否生效,最直接的办法是锁屏跑10分钟再解锁,打开轨迹页看相邻定位点的时间间隔:如果中间出现一大段空白但进程没被杀,说明只是定位被降频;如果前台服务被系统重建,要看START_STICKY之后的恢复逻辑有没有把之前的轨迹接上,接不上就会在轨迹中间出现一条直线。
最后补一条验收技巧:跑完在轨迹页截图,和演示视频里的路线形态对比。如果视频里轨迹平滑、你的却方方正正,多半是定位更新间隔被系统或代码拉长到十几秒以上,把LocationRequest的setInterval从10秒调到3秒再看,同时盯住电池曲线,找到精度和耗电的平衡点,这一步做完,跑步App才算真正能拿出门记录一次完整的跑步。
本文还有配套的精品资源,点击获取