简介:这是一份面向计算机相关专业学生与初入职场开发者的Android校园运动类APP实战项目资源,适用于课程设计、毕业设计及移动开发入门学习。项目完整实现跑步轨迹记录、运动时长统计、里程计算、心率模拟及数据可视化等核心功能,代码经实测可稳定运行,具备良好的工程结构与模块划分。压缩包共277个文件,含71个Java业务逻辑文件、137个XML界面与配置资源、44张PNG图标素材,辅以Gradle构建脚本、本地SO库及必要Jar依赖,整体体积仅7.66MB,轻量易部署。目前已有232人下载学习,资源结构清晰:包含标准Android Studio工程目录、主入口Activity、LBS定位集成(BaiduLBS_Android.jar)、Gradle多环境配置及基础README说明,特别适合从零理解运动类App的数据采集、UI交互与后台统计逻辑。 做校园运动APP这个项目的时候,我给自己定的标准很直接:不是交一个能跑的Demo,而是真正能拿来用的工具。跑步功能要稳,轨迹不能乱飘,数据要准,统计要直观。这个项目基于Android原生开发,完整源码外加一份说明文档,整体代码量在8000行左右。如果你正在学Android、准备做毕业设计,或者想做一个属于自己的运动类应用,这个项目可以作为一份从零到一的完整参考。它覆盖了定位、前台服务、数据库、图表统计、地图轨迹这些Android开发里比较硬核的知识点,而且都是实际项目里会用到的写法,不是教科书式的demo。
项目里最核心的两块是“跑步”和“统计”。跑步部分需要处理GPS定位、轨迹记录、配速计算、数据持久化;统计部分要把每次跑步的数据拿出来做周报、月报,用图表直观呈现。整条链路涉及的技术点非常全面,我下面按模块拆开讲,顺便把我在开发过程中踩过的坑、做过的取舍一起写出来。
1. 整体设计与技术选型:先想清楚再动手
1.1 技术栈与开发环境
先交代一下项目的基础配置。开发工具用的是Android Studio,语言主体是Kotlin,其中少量工具类用Java保留。Kotlin在空安全和协程上确实比Java省心,尤其是定位回调、数据库操作这些容易出空指针和异步问题的场景,写起来会流畅很多。
项目的SDK配置如下:
compileSdk 33 minSdk 23 targetSdk 33minSdk选23,也就是Android 6.0,主要是为了能比较舒服地处理运行时权限。如果minSdk低于23,动态权限的代码会多出一大堆分支逻辑,对新手不够友好。targetSdk选33是为了适应现在主流应用商店的要求,同时也倒逼自己处理Android 12以上设备的精确位置权限和前台服务类型。
依赖方面,项目主要用了以下几个库:
- Jetpack全家桶:Room做本地数据库,ViewModel和LiveData管理UI数据,Lifecycle感知组件生命周期
- MPAndroidChart 3.1.0:统计图表的绘制
- 高德地图SDK:轨迹展示和地理编码
- Gson:JSON解析,主要用在数据导入导出
- Google Play Services Location:定位服务封装
选这些库的原因很简单:稳定、文档全、社区活跃。MPAndroidChart虽然是老项目,但做折线图、柱状图、饼图仍然是最省事的方案。地图SDK在高德和百度之间我最后选了高德,主要是因为高德的Key申请和SHA1配置在全流程上更顺畅,而且轨迹绘制相关的API比较直观。
1.2 项目模块划分
整个项目按功能划分为四个模块:登录与个人中心、运动(跑步)模块、统计模块、数据管理模块。虽然后端服务也可以接入,但这版源码把数据完全存在本地,这样做的好处是不依赖服务器,clone下来跑通就能看到完整效果,适合学习和二次开发。
每个模块对应一个包,包内再按MVP或MVVM的模式组织。我没有引入太重的架构框架,用的是Google推荐的ViewModel + Repository模式。Repository层负责从Room和定位服务取数据,ViewModel层做加工,Activity和Fragment只负责渲染和交互。这样分层的目的很实际:后期如果要加联网功能,只需要改Repository的实现,UI层不用动。
1.3 核心数据流设计
跑步过程的数据流是这样的:
用户点击开始跑步,App启动前台服务;前台服务向系统申请定位,获得定位结果后每3秒记录一个轨迹点,同时计算累计距离、实时配速;跑步结束时,服务把本次会话的所有信息封装成一个RunRecord对象,连同轨迹点列表一次性写入Room数据库;统计模块读取数据库进行聚合展示。
这个流程里有一个容易忽略的点:轨迹点数据是高频写入的,如果每次定位回调都做一次数据库插入,既慢又费电。我的做法是先放在内存列表里,等跑步结束再一次性提交。这个设计和很多商业化跑步App的做法一致,实测下来可靠性和性能都靠谱。中间如果App被系统杀掉,最多丢失几秒的轨迹数据,这个风险可以接受。
2. 跑步模块实现:GPS轨迹与实时数据的处理细节
2.1 权限申请与Android版本适配
跑步模块涉及的权限有这几项:
| 权限 | 用途 | 注意点 |
|---|---|---|
| ACCESS_FINE_LOCATION | 获取GPS精确定位 | Android 12起需要同时声明ACCESS_COARSE_LOCATION |
| FOREGROUND_SERVICE | 启动前台服务 | Android 14起需要声明具体的前台服务类型 |
| FOREGROUND_SERVICE_LOCATION | 定位类前台服务 | Android 14新增,不声明会直接崩溃 |
| POST_NOTIFICATIONS | 发送常驻通知 | Android 13起通知权限从安装时改为运行时申请 |
| ACTIVITY_RECOGNITION | 识别用户运动状态 | 如果做自动暂停功能需要 |
权限声明是Android开发里最容易出问题的地方,尤其是Android 12和Android 14的变更。targetSdk 33的环境下,必须在Manifest里同时声明前台服务类型和定位权限,漏了任何一个,启动服务时都会抛SecurityException。我在代码里做了一层权限请求封装,把定位权限、通知权限合并成一次引导流程,用户拒绝后还会弹一次二次确认框,避免那种因为权限被拒导致整个功能不可用的情况。
2.2 定位源选择与轨迹记录策略
定位用的是Google的FusedLocationProviderClient,它在底层的原理是综合GPS、Wi-Fi、基站和传感器数据进行融合定位,比直接拿LocationManager的GPS_PROVIDER稳定很多,尤其在城市高楼环境下,GPS信号被遮挡时还能用网络定位兜底。
轨迹记录的核心代码如下:
private val locationCallback = object : LocationCallback() { override fun onLocationResult(result: LocationResult) { val location = result.lastLocation ?: return // 过滤明显漂移点 if (location.accuracy > 30f && location.speed < 2f) { return } // 如果与上一点距离小于3米,忽略,避免产生大量冗余点 if (pointList.isNotEmpty()) { val lastPoint = pointList.last() val distance = haversineDistance( lastPoint.latitude, lastPoint.longitude, location.latitude, location.longitude ) if (distance < 3.0) { return } } val point = TrackPoint( latitude = location.latitude, longitude = location.longitude, timestamp = System.currentTimeMillis(), altitude = location.altitude, speed = location.speed, accuracy = location.accuracy ) pointList.add(point) updateRunData(point) } }两个关键过滤参数:精度小于30米、与上一点距离小于3米。精度过滤是为了干掉GPS漂移点,比如在高架桥下突然跳出的位置;距离过滤是为了控制轨迹点数量。跑10公里如果每1米记一个点,光轨迹点就上万条,内存和数据库压力都会变大。
定位请求参数我这样设置:
val locationRequest = LocationRequest.create().apply { interval = 3000 fastestInterval = 2000 priority = LocationRequest.PRIORITY_HIGH_ACCURACY }interval是3秒一次,这个频率在户外跑步场景下足够生成平滑轨迹,又不会太耗电。如果设置成1秒一次,GPS模块会持续高功耗运转,手机很快发热;设置成5秒以上,转弯多的路线轨迹会明显变形。3秒是平衡点。
2.3 距离、配速与卡路里的计算
距离计算用的是Haversine公式,直接调Location.distanceBetween也能得到同样结果,但自己写公式更通用,方便在没有Location对象时计算坐标点之间的球面距离。
fun haversineDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double { val r = 6371000.0 // 地球半径,单位米 val dLat = Math.toRadians(lat2 - lat1) val dLon = Math.toRadians(lon2 - lon1) val a = sin(dLat / 2) * sin(dLat / 2) + cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) * sin(dLon / 2) * sin(dLon / 2) val c = 2 * atan2(sqrt(a), sqrt(1 - a)) return r * c }这里有个容易踩坑的点:Haversine算的是球面最短距离,忽略地形起伏。如果你的跑道有坡,实际跑过的距离会比计算值多个3%到5%,对业余跑步记录来说可以接受。专业田径场测算会用更复杂的模型,但手机端做运动记录基本都用的这个公式。
实时配速的计算方式也有讲究。最直接的做法是用最近一段距离除以经过的时间,但GPS信号波动会导致瞬时速度忽快忽慢,配速显示就会在5分配到8分配之间疯狂跳。我的做法是滑动窗口平均:取最近15秒内的位移除以时间,算出来的配速比较稳定。跑步结束后的平均配速,直接总距离除以总运动时间,这个没有争议。
卡路里计算用的METs代谢当量:
fun calculateCalories(distanceMeters: Double, durationSeconds: Double, weightKg: Double): Double { val speedKmh = distanceMeters / 1000.0 / (durationSeconds / 3600.0) var mets = when { speedKmh < 8 -> 6.0 speedKmh < 10 -> 8.3 speedKmh < 12 -> 9.8 speedKmh < 14 -> 11.0 else -> 11.8 } val hours = durationSeconds / 3600.0 return mets * 3.5 * weightKg / 200.0 * hours }METs的取值参考了运动医学常用的代谢当量表,虽然每个人代谢率不同,但作为日常参考这个量级是合理的。体重需要用户在个人中心里填,如果没填默认60公斤。
2.4 前台服务与后台保活
跑步是一个长时间进行的操作,用户很可能锁屏、切后台,甚至去刷一下微信。Android系统对后台应用的定位权限限制非常严格,如果不在前台服务里获取定位,几分钟后系统就会杀掉你的进程或者停止定位回调。
前台服务的基本逻辑是先构造一个常驻通知,再通过startForeground启动:
val notification = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("校园运动") .setContentText("正在记录您的跑步轨迹") .setSmallIcon(R.drawable.ic_run) .setOngoing(true) .build() startForeground(NOTIFICATION_ID, notification)通知的channelId需要在应用启动时创建好,否则通知不显示。Android 13以上的设备还要记得在启动服务前检查POST_NOTIFICATIONS权限,不然通知栏是空的,系统会默认认为前台服务的通知不可见。
后台保活这块还有一个现实问题:国产手机(华为、小米、OPPO、vivo)的省电策略非常激进,应用进后台后,即使有前台服务也可能被系统提示"后台高耗电",甚至被用户手动清理掉。这个问题很难从代码层面完全解决,但我做了如下处理:
- 在通知栏实时更新跑步数据(距离、配速),让用户直观看到应用还在运行
- 请求电池优化白名单,代码里用ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS引导用户加入
- 在低内存时不主动释放定位服务,把优先级提到IMPORTANCE_HIGH
这些措施做不到100%防杀,但实测下来,正常使用场景跑完一次10公里不锁后台也基本没问题。
3. 数据持久化与统计模块:从记录到报表
3.1 数据库表设计与Room接入
统计模块的前提是数据存得规范。这个项目用的是Room,物理上就是一个SQLite数据库,但用注解的方式省去了手写大量样板代码的麻烦。
数据库里两张核心表:run_record(跑步记录表)和track_point(轨迹点表)。
run_record表结构:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long | 主键,自增 |
| startTime | Long | 开始时间(毫秒时间戳) |
| duration | Long | 运动时长(秒) |
| distance | Double | 总距离(米) |
| avgPace | String | 平均配速,如"5'30" |
| calories | Double | 消耗热量(千卡) |
| steps | Int | 预估步数 |
| status | Int | 0进行中,1已完成,2已取消 |
track_point表结构:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long | 主键 |
| runId | Long | 外键,关联run_record表的id |
| latitude | Double | 纬度 |
| longitude | Double | 经度 |
| timestamp | Long | 记录时间 |
| speed | Float | 瞬时速度(米/秒) |
两个表通过runId建立一对多关系。每次跑步记录对应几十到几百个轨迹点,这个量级用SQLite存储完全没压力。Room的DAO里写了Insert和Query方法,插入时用@Transaction注解包住整批点,保证数据的一致性。
3.2 统计查询的SQL设计与数据聚合
统计模块的SQL直接决定了报表的准确性,这里有几个坑要说清楚。
按天聚合跑步数据,需要把毫秒时间戳转换成本地时区的日期字符串。SQLite里的datetime函数默认处理的是UTC时间,如果直接用,统计出来的日期会偏移8小时。正确的写法是先加时区偏移再格式化:
// 先偏移8小时再按天分组 SELECT strftime('%Y-%m-%d', datetime(startTime / 1000 + 28800, 'unixepoch')) as day, COUNT(*) as runCount, SUM(distance) as totalDistance, AVG(duration) as avgDuration FROM run_record WHERE status = 1 GROUP BY day ORDER BY day DESC LIMIT 7这里28800秒就是东八区的8小时偏移量。如果你用SQLite的datetime函数直接格式化毫秒时间戳,得到的时间永远是UTC时间,导致跨天统计错位。这是个非常隐蔽的坑,不实际跑一遍数据很难发现。
周统计和月统计的核心思路一样,只是GROUP BY的粒度不同。周统计我用的ISO周,星期一到星期日为一周,这样更符合运动习惯。注意SQLite里有一个麻烦的地方:直接按年份和周数分组需要处理跨年问题,比如第53周跨越了两个年份,这个边界情况我在代码里做了特殊处理,确保统计不会出现"凭空多出一周"的数据。
3.3 图表可视化:MPAndroidChart的实战配置
统计模块的图表我选了MPAndroidChart,三张图:近7天运动时长柱状图、近30天跑步里程折线图、配速区间分布饼图。
柱状图的实现代码:
val barEntries = ArrayList<BarEntry>() dailyStats.forEachIndexed { index, stat -> barEntries.add(BarEntry(index.toFloat(), stat.distance.toFloat())) } val barDataSet = BarDataSet(barEntries, "每日里程").apply { color = Color.parseColor("#4CAF50") valueTextSize = 12f valueFormatter = object : ValueFormatter() { override fun getFormattedValue(value: Float): String { return String.format("%.1fkm", value / 1000f) } } } val barData = BarData(barDataSet).apply { barWidth = 0.6f setValueTextColor(Color.parseColor("#333333")) } barChart.apply { data = barData description.isEnabled = false xAxis.position = XAxis.XAxisPosition.BOTTOM axisRight.isEnabled = false setTouchEnabled(false) setScaleEnabled(false) animateY(800) }几个配置值得说:description.isEnabled = false是去掉图表左下角默认的"Description"文字,这是MPAndroidChart新手经常忽略的细节。setTouchEnabled和setScaleEnabled关闭手势,避免统计页面里图表被意外缩放导致体验混乱。xAxis.labelCount要手动设置,否则X轴标签会自动间隔,显示不全。
配速区间饼图的价值在于直观展示用户的速度分布,比如用户经常跑在"5分30秒到6分配速"这个区间,那说明他的日常节奏比较稳定,这个数据对跑者改进训练计划有参考意义。饼图中心可以加一个总里程数,这样一眼就能看到核心指标。
4. 实操过程与关键环节实现
4.1 从零搭建项目骨架
我这里把搭建过程完整写出来,照做就能跑起来。
第一步,Android Studio里新建项目,选择Empty Activity模板,语言选Kotlin,Minimum SDK选23。
第二步,在build.gradle(Module级别)添加依赖:
dependencies { implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // Room数据库 implementation 'androidx.room:room-runtime:2.5.2' implementation 'androidx.room:room-ktx:2.5.2' kapt 'androidx.room:room-compiler:2.5.2' // 图表 implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' // 定位 implementation 'com.google.android.gms:play-services-location:21.0.1' // 生命周期组件 implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' }第三步,配置高德地图SDK。在项目根目录的build.gradle里添加高德的仓库地址,然后在AndroidManifest.xml里添加Key和所需权限。
高德Key申请时有一步容易漏:需要把开发机的SHA1值填到控制台。SHA1获取方式是在Gradle Task里执行signingReport,然后把输出里的SHA1和包名一一对应。如果你用同一个Key调不同包名的APK,地图会显示空白,这个我经常遇到。
第四步,创建数据库和实体类。Room的注解处理器会自动生成实现代码,但是如果你改了实体类没有重新编译,会出现找不到已生成类的报错,Build菜单里的Clean Project和Rebuild Project可以解决。
4.2 跑步页面的状态管理
跑步页面是核心交互入口,状态流转是这样的:空闲 -> 运行中 -> 暂停 -> 运行中 -> 已结束。这个状态机的实现要处理几个细节。
开始按钮点击后调用startRun(),此时创建一个新的RunRecord实例,记录开始时间,启动前台服务并开始定位请求。同时记录一个runId,这个id在结束写入数据库时用。
暂停和恢复的处理要注意时间计算。用户跑了5分钟暂停2分钟再去买水,这两分钟不应该计入运动时长。所以暂停时要记录pauseStartTime,恢复时要累加totalPausedTime,最后的有效运动时长 = 总耗时 - 暂停时长。
结束跑步行,除了停止定位和前台服务,还要把数据写入数据库。这里Room的插入和更新操作建议放到协程的IO线程里执行,避免主线程卡顿。写法如下:
fun finishRun() { runRecord.duration = getTotalActiveDuration() runRecord.distance = totalDistance runRecord.avgPace = calculateAvgPace() runRecord.calories = calculateCalories() runRecord.status = 1 viewModelScope.launch(Dispatchers.IO) { val runId = runRecordDao.insert(runRecord) trackPoints.forEach { it.runId = runId } trackPointDao.insertAll(trackPoints) } }4.3 轨迹地图展示:重跑路线
跑步结束后,用户能看到本次路线的地图回放。这一步用高德地图的Polyline来画轨迹线。
val polylineOptions = PolylineOptions() points.forEach { point -> polylineOptions.add(LatLng(point.latitude, point.longitude)) } polylineOptions.width(12f) polylineOptions.color(Color.rgb(50, 150, 250)) amap.addPolyline(polylineOptions)这里要注意一点:轨迹线颜色是整条线统一的,如果想让速度快的路段标成绿色、慢的标成红色,需要按颜色分段画多条Polyline,每条线段的颜色根据这段的平均速度计算。所以不能直接使用PolylineOptions把全部点一次性画完,需要按速度区间拆分段。项目里实现了匀速绿、偏慢黄、过慢红的配色方案,这样视觉上能很清楚看出哪里跑得快哪里掉速了。
地图还有一个细节:加载完轨迹后,需要调用moveCamera把视角缩放到能完整显示整条轨迹的范围,并设定合适的缩放级别。用LatLngBounds.Builder把全部点包进去,然后setPadding留出边距,最后animateCamera平滑过渡。
5. 常见问题与排查技巧实录
5.1 定位不到或定位结果为空
这个是最常见的问题,没有之一。排查思路按顺序来:
第一,检查权限是否真的授予了。特别是Android 12以上,精确位置权限和模糊位置权限是两个独立的权限,即使用户授权了模糊位置,App依然无法拿到GPS精确定位。需要用checkSelfPermission分别验证。
第二,检查系统定位服务是否开启。模拟器上经常出现"设备定位已关闭"的提示,真机上也会因为用户在系统设置里关了位置服务导致onLocationResult不回调。代码里可以用SettingsClient检查LocationSettings是否满足请求条件。
第三,检查是否在室内。GPS信号穿不过钢筋混凝土,室内基本无法定位。测试时可以跑到窗边,等状态栏定位图标出现后再开始记录。
第四,看看是不是立刻调用了requestLocationUpdates但没等几秒。GPS冷启动时间在十几秒到几十秒不等,第一次定位会慢一些。可以先用getLastLocation拿一次缓存位置兜底,保证界面第一时间有数据。
5.2 应用被后台杀死,跑步记录丢失
这个问题在国产ROM上尤其明显。我的经验是三层防护:
第一层,前台服务加常驻通知,提高进程优先级。第二层,引导用户将应用加入电池优化白名单,代码里弹一个系统对话框让用户确认。第三层,绑定到系统的"自启动管理",这个需要跳转到具体的系统设置页面,不同品牌手机的跳转Action不一样,要在代码里做机型适配。
即使做了这些,在极端情况下(用户手动上滑清理),应用还是会死。所以我在数据库里加了一个保护逻辑:每次更新轨迹点时,把当前累计距离、累计时长同步写入一个独立的status表。如果App被杀后重新启动,检测到上一次有未完成的状态,会弹出提示"检测到未保存的跑步记录,是否恢复?"用户确认后可以继续之前的状态。这个体验优化虽然代码量不大,但给人感觉就是专业。
5.3 统计图表数据不对,日期错乱
统计数据的日期错乱,十有八九是时区问题。SQLite的datetime函数默认用UTC,国内用户比UTC快8小时,如果直接对时间戳做格式化,就会出现"昨天跑的步显示在后天"这种诡异情况。解决办法是在SQL里手动加偏移秒数,或者把时间戳统一先换算成本地时区的日期字符串再分组。
除了时区,还有一个隐藏的坑:SQLite的strftime('%Y-%m-%d', ...)返回的是字符串,字符串比较是按字典序的,因为'2024-01-05'这种格式大小比较恰好等于时间先后比较,所以排序没有问题。如果你改用别的格式,比如'2024/01/05',排序就会出错,因为'2024/10/05'字典序小于'2024/9/05',但实际上10月比9月晚。
5.4 图表无法显示或显示空白
MPAndroidChart出现空白,大多数情况下是因为数据更新后没有调用invalidate()。在使用LiveData观察数据变化时,数据回调后要执行chart.clear()或chart.setData(),然后调用chart.invalidate()刷新视图。新手经常会忘记这个关键步骤。
另一个问题是图表里数据超过100个点后,绘制变得卡顿。如果是月视图,每天一个点才30个点,问题不大;但你如果做年视图,每天都横坐标,365个点上屏会明显变卡。MPAndroidChart对大量点位做了降采样处理,性能尚可,但我在数据层做了按月分页,一次最多加载30天的数据,既保证性能也避免视觉噪声。
还有一个细节:柱状图和折线图的X轴标签默认是数值型的,如果直接用"1、2、3"当标签,需要把它转成自定义字符串。MPAndroidChart的IAxisValueFormatter可以把整数索引映射成"周一""周二"这样可读的标签。记得关闭xAxis.setGranularityEnabled(false),否则你看到的永远是隔一个显示一个。
5.5 导出分享功能与后续扩展方向
这个版本的项目还做了一个简单的数据导出功能,把跑步记录和轨迹点序列化成JSON,通过系统分享发送到文件管理器。做法是生成JSON字符串后用FileProvider共享文件,注意FileProvider的file_paths.xml里要配置好缓存目录的路径,否则文件分享时会报"无法打开文件"的错误。这段代码适合作为FileProvider用法的入门参考,很多项目都会用到这个组件。
现在项目比较适合做毕业设计或课程实验,如果想要继续延伸,建议在以下几个方向扩展:加一个云端同步功能,把本地数据用Retrofit同步到后端;给跑步模块添加音乐播放控制的能力;集成微信或支付宝运动步数导入;把登录从单机的SharedPreferences改为真正的账号体系,配合后端做校园跑排行榜。
我在实际开发这个项目时体会最深的一点是:运动类App表面看似简单,但坑都藏在细节里。GPS漂移、时区错位、后台杀进程、厂商ROM的省电策略,每一个问题单拎出来都是很考验排查经验的。把这些坑走一遍,你对Android的系统组件、权限模型、数据库设计和生命周期管理这几块的理解,会比刷几十篇教程都扎实。
最后再分享一个小技巧:调试定位功能时,不要只在模拟器上测。模拟器虽然有模拟GPS坐标的工具栏,但它的传感器数据和真机差太多,轨迹平顺性、速度计算这些都没法验证。我强烈建议你至少找一台真机装上去跑几百米试试,感受一下轨迹效果,再根据自己的需求调整那3米阈值和3秒间隔。数字是死的,体验是活的,跑出来的数据反馈会告诉你该怎么调。
本文还有配套的精品资源,点击获取