简介:这是一套基于原生Android与Java实现的仿Keep健身打卡App完整源码,面向毕业设计、课程设计及Android初学者。项目从真实需求出发,页面简洁实用,功能覆盖登录注册、个人信息维护、系统设置、搜索、日历健身打卡、健身图文视频教程、收藏评论点赞与购买教程、社区分享等完整业务链路,代码注释较多,方便理解各模块实现与二次开发。压缩包内共含约2000个文件,以Java源码、XML布局与配置、JSON数据、APK安装包、SO动态库和图片资源为主,总大小约71.8MB,工程目录结构规范,便于定位入口与核心逻辑。目前已有1687人学习下载,适合需要快速搭建可演示的移动端打卡项目、完善毕设功能或系统梳理Android开发流程的读者参考,也可作为课程设计与项目实战的素材。
1. 从Keep反推原生Android的边界
健身打卡App是毕设选题里“看起来简单、做起来绕”的一类。Keep的核心体验不是视频播放,而是运动记录——它涉及传感器读取、计时逻辑、轨迹绘制、日历打卡四个模块,彼此还有状态依赖。用原生Android做这套东西,难点不在写界面,而在怎么把“一次完整的锻炼”建模成一串可恢复、可校验的状态变化。
这篇博文只讲一件事:不依赖第三方运动SDK,用Android原生的SensorManager、前台Service、Room数据库和自定义View,实现一个具备同类型产品核心体验的健身打卡应用,并给出一套能直接跑通的源码组织方案。内容按“数据模型→传感器采集→轨迹绘制→打卡闭环”的顺序推演,最后补上Service保活与电量优化的工程细节。适合正在做毕设、想把“仿Keep”写进论文但不想被视频模块拖垮的同学,也适合想看看运动类App在Android上真实技术栈的开发者。
2. 原生Android的运动数据采集与持久化设计
2.1 为什么必须自己维护运动记录而不是复用系统计步器
原生Android自带Sensor.TYPE_STEP_DETECTOR和TYPE_STEP_COUNTER两个传感器类型,但它们的前提是设备上有对应的硬件芯片,且数据归系统计步服务统一管理,第三方应用无法直接读取历史步数。考虑到这个约束,仿Keep的项目必须自己维护一套运动数据采集链路,把传感器事件转成业务数据落库。
常见的实现思路是:录音布局里用SensorManager注册TYPE_STEP_DETECTOR监听,每检测到一步就回调一次,在此基础上记录时间戳、累加步数和运动状态。核心代码如下:
public class StepRecorder implements SensorEventListener { private int stepCount = 0; private long lastStepTime = 0L; @Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() == Sensor.TYPE_STEP_DETECTOR) { stepCount++; lastStepTime = System.currentTimeMillis(); // 每步回调一次,UI层可通过回调接口刷新实时数据 if (stepListener != null) { stepListener.onStep(stepCount, lastStepTime); } } } }这段代码的逻辑是:先判定事件源传感器类型,再用局部变量累加步数,最后通过回调接口把结果抛给界面层或记录模块。这里的关键参数是lastStepTime,它决定后续计算瞬时配速和步频的数据基础,建议用System.currentTimeMillis()而非event.timestamp,因为后者是纳秒时间戳且受系统时钟调整影响。
需要注意,TYPE_STEP_DETECTOR有硬件功耗和灵敏度差异,在模拟器上完全无数据,必须在真机调试。若教学环境只提供模拟器,可以添加一个“手动模拟步数”的开发者入口,便于PPT演示。
2.2 用Room建三张核心表:运动记录、日记、打卡日历
数据层是毕设论文里最容错、最容易写出深度的部分。使用Room把运动数据划分为三张表:运动过程记录表(含起止时间、时长、步数、消耗估算)、文本日记表(记录用户当天的主观状态)、日历打卡表(按天标记是否完成训练)。建表代码示意如下:
@Entity(tableName = "exercise_session") data class ExerciseSession( @PrimaryKey(autoGenerate = true) val sessionId: Long = 0, val startTime: Long, val endTime: Long, val totalSeconds: Long, val stepCount: Int, val calorieEstimate: Double ) @Entity(tableName = "checkin_day") data class CheckinDay( @PrimaryKey val dateString: String, // "2026-05-20" val completed: Boolean, val sessionId: Long? )日期字段建议使用“yyyy-MM-dd”格式的字符串而非时间戳,这样查询某月打卡集合时,SQL的LIKE匹配或BETWEEN比较都更直观,日界线处理也不容易出错。由于毕设项目往往不需要高并发,这里不引入DataStore或跨进程同步,使用LiveData暴露Room查询结果即可满足UI刷新需求。
表的关联策略是:运动完成时插入ExerciseSession和CheckinDay两行记录,两者通过sessionId外键关联。日记表单独存在,对应记录当天的体重或心情字段,界面设计成“打卡成功后弹出日记编辑页”,这样用户的操作流恰好与Keep一致:动完—打卡—记录。
2.3 运动状态机:空闲、运动中、暂停、结束
运动记录最怕的就是用户锁屏或切后台导致状态丢失。原生Android的Activity在onStop时并不代表应用退出,所以运动状态不能存在Activity的成员变量里,而应该持久化到ViewModel或数据仓库中。推荐用枚举定义状态机:
public enum WorkoutState { IDLE, // 初始或结束 RUNNING, // 运动中 PAUSED // 手动暂停 }每次传感器事件到达时,先判断状态机是否处于RUNNING。只有RUNNING状态才累加步数。PAUSED状态下传感器仍然注册监听,但忽略步数事件,同时用SystemClock.elapsedRealtime()计算暂停的累积时长,运动结束时从总时长中扣减掉暂停时长,这也是真实运动App的通用做法。
为了避免Activity重建导致状态丢失,把WorkoutState放到ViewModel配合SavedStateHandle做进程级恢复。如果时间充裕,还可以用SharePreferences保存运行中的状态快照,每次启动应用检查快照并提示用户“上次运动未保存,是否恢复”。
3. 运动中的GPS轨迹与前台Service实现
3.1 选型:FusedLocationProviderClient还是原生LocationManager
原生Android有两个定位方案:LocationManager是老牌API,需要手动指定Provider并处理不同Provider的切换;FusedLocationProviderClient来自Google Play服务,自动聚合GPS、Wi-Fi和基站信号。毕设项目若面向国内设备市场,需要处理没有GMS的品类,此时应当直接使用LocationManager手动请求GPS_PROVIDER,确保兼容性和源码可解释性。
关键点有三个:申请ACCESS_FINE_LOCATION权限、设置LocationRequest的最小时间间隔和最小距离间隔、在onLocationChanged回调中实时记录经纬度。代码如下:
LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return; } lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, // 最小时间间隔,单位毫秒 5f, // 最小距离间隔,单位米 locationListener);参数说明:2000毫秒的间隔兼顾了轨迹平滑度和电量消耗,5米的最小距离用于过滤静止时的微小抖动。如果数值设置过小,长时间运动时会产生几千个点,绘制轨迹的性能和存储体积都难以接受;设置过大,轨迹在转弯时会切角。
3.2 前台Service绑定通知栏:锁屏不断记录
Android 8.0(API 26)以后,后台Service在锁屏或应用切后台后可能被系统回收,所以运动计时必须依托startForegroundService启动前台Service,并在5秒内调用startForeground方法并传入通知。这个通知栏提示本身也是仿Keep应用里“运动进行中”的视觉锚点。核心代码:
Intent serviceIntent = new Intent(this, WorkoutService.class); ContextCompat.startForegroundService(this, serviceIntent); // 在Service的onStartCommand中 Notification notification = new Notification.Builder(this, CHANNEL_ID) .setContentTitle("运动打卡中") .setContentText("已记录 " + durationText + ",继续加油") .setSmallIcon(R.drawable.ic_fitness) .setOngoing(true) .build(); startForeground(1001, notification);注意,这个通知必须通过NotificationChannel创建,否则在Android 8.0以上会直接崩溃。渠道ID需要固定常量,允许用户手动关闭通知栏文字,但渠道一旦创建,修改重要性等级就要卸载应用才能生效。
3.3 轨迹数据的本地缓存与回放
每收到一次onLocationChanged,就把经纬度与当前时间戳追加写入一个内存列表,同时每30秒批量写一次Room数据库的运动轨迹表。这样设计比每次回调都INSERT一条记录性能更优,Room的事务频率也会低很多。轨迹表结构只需四个字段:经纬度、时间、运动会话ID。
地图绘制用Google地图或高德地图都行,但毕设答辩场景中高德更直观且无需GMS。回放时按时间顺序把点连成线,并圈出起点和终点。若不想引入地图SDK,也可以把轨迹画成相对坐标的折线图,用自定义View实现,同样能展示“户外跑步”的数据成果。
4. 仿Keep核心闭环:训练计划、打卡日历与激励体系
4.1 训练计划的数据建模:周计划与单次训练
计划页是Keep类应用的首页入口。它由两部分组成:固定的周计划模板和当天可以执行的具体动作列表。用Room的Relation注解建立一对多关系,一个DayPlan对应多个WorkoutItem。数据模型定义如下:
@Entity(tableName = "daily_plan") data class DailyPlan( @PrimaryKey val date: String, val title: String, val targetMinutes: Int ) @Entity(tableName = "workout_item") data class WorkoutItem( @PrimaryKey(autoGenerate = true) val itemId: Long = 0, val planDate: String, val name: String, val durationSeconds: Int, val isCompleted: Boolean = false )其中planDate的取值按每星期的星期几计算。训练计划页加载时先判断今天是否已有记录,如果没有则自动生成当天计划,这也在UI层面实现了“每天打开App都有新内容”的体验。
4.2 打卡逻辑:运动时间达成才能点亮日历
打卡是仿Keep的情绪核心点,必须设置硬规则:单次有效运动时长达到目标(默认20分钟)才能点亮当天的打卡图标;未达标只记录运动数据,不写入打卡表。这个严格判定过滤掉了“打开App玩两下就算打卡”的假数据,答辩时也更能体现设计严谨性。判定逻辑写在Repository层,Activity不直接操作打卡表:
fun finishWorkout(session: ExerciseSession) { if (session.totalSeconds >= dailyPlan.targetMinutes * 60) { val checkin = CheckinDay( dateString = session.getDateString(), completed = true, sessionId = session.sessionId ) checkinDao.insert(checkin) } }这里有几个延伸:打卡成功后弹出成就通知,或者用SharedPreferences持久化连续打卡天数,用于月历视图上展示“已坚持N天”的标语。Keep的用户黏性和晒图传播大多来自这些成就感设计,放在毕设项目里是妥妥的加分项。
4.3 月历视图实现:RecyclerView嵌套GridView的锯齿问题
打卡日历通常用月份网格展示,常见做法是RecyclerView包GridView,但滚动时会产生高度测量冲突和卡顿。更稳的方案是直接用RecyclerView + GridLayoutManager,让每一天作为一个条目,用ItemDecoration绘制分割线,保持整个列表的滚动一致性。日期格子用TextView展示数字,背景色根据打卡布尔值切换。
val layoutManager = GridLayoutManager(this, 7) recyclerView.layoutManager = layoutManager recyclerView.adapter = CalendarAdapter(checkinMap)日历数据查询要注意时间范围:某月日历需要查询当月所有打卡记录,Room用BETWEEN语句即可完成。如果数据量少,直接查询全表并按日期映射到Map<String, Boolean>,内存开销也不会大。
4.4 按时长统计和步频图表
图表模块适合用自定义View绘制,避免引入MPAndroidChart造成包体积增大和答辩被追问“这个库的原理是什么”的风险。绘制两个统计维度:最近7天运动时长柱状图、单次运动每5分钟步频折线图。自定义View在onDraw中根据数据集合计算柱状位置和折线路径,并在onMeasure里处理wrap_content高度。后端统计直接用SQL聚合即可,不需要本地内存二次计算。SQL示例如下:
SELECT date(startTime / 1000, 'unixepoch') as day, SUM(totalSeconds) as total FROM exercise_session WHERE startTime >= ? GROUP BY day ORDER BY day ASC5. 源码工程组织与构建配置:一份能直接提交的Android项目
5.1 包名规划与模块边界
为了答辩时能清楚讲解,建议用简单的分层包名:data(Room、Repository)、ui(Activity、Adapter、自定义View)、service(前台Service、传感器监听)。不要用MVP或MVVM的框架名撑场面,直接说“按数据层、业务层、展示层分离”,代码里用ViewModel+Livedata把Activity的代码控制在一百行以内,反而更经得起追问。
5.2 build.gradle关键配置:最小SDK版本与依赖
原生Android开发的环境需要适配不同版本,最小SDK定为21(Android 5.0)覆盖绝大多数机型,targetSdk建议设为33或34。依赖配置如下:
android { compileSdk 34 defaultConfig { applicationId "com.example.fitcheckin" minSdk 21 targetSdk 34 } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } } dependencies { implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.room:room-runtime:2.6.1" annotationProcessor "androidx.room:room-compiler:2.6.1" implementation "androidx.lifecycle:lifecycle-viewmodel:2.6.2" }如果用的是Kotlin,Room的注解处理器要替换成kapt插件。需要确认Android Studio版本不低于Hedgehog,否则JDK17的兼容性会报错。
5.3 模拟数据入口:没有真机也能演示完整流程
不少学生答辩时用的是模拟器,而模拟器没有计步硬件。一种常见做法是在AndroidManifest里添加一个DEBUG开关的BroadcastReceiver,当接收到特定ADB广播时往数据库插入一条模拟运动记录和打卡记录,这样就算展示的运动App功能有限,月历和统计页也有数据可看。
adb shell am broadcast -a com.example.fitcheckin.DEBUG_INSERT5.4 避免的坑:传感器在模拟器和国产ROM上的差异
国产ROM对后台传感器读取的策略严格,华为和小米会在锁屏一段时间后主动冻结应用CPU。即便用了前台Service,也可能出现步数不走的现象。有两条保险策略:在Service的onTaskRemoved里用START_STICKY重建;在运动开始前检查电池优化白名单,提示用户手动允许后台运行。这两个点写进论文的“异常处理”章节,既真实又实用。
6. 验证打卡闭环的三个关键场景与参数调优
6.1 场景一:模拟一次20分钟运动
手动输入一组模拟步数和时间到数据库,通过ADB命令或隐藏的调试Activity触发“结束运动”流程。验证标准是:运动时长达到1200秒,打卡表新增一条记录,月历视图当天出现高亮圆点。如果没有达标记,检查ExerciseSession的totalSeconds是否包含暂停时长。
6.2 场景二:杀死应用后步数是否丢失
运动开始后,用后台清理工具杀掉应用进程,重新打开看是否提示“恢复未完成运动”。当前实现通过SavedStateHandle只能恢复ViewModel状态,进程被杀掉后需要用StartService的START_STICKY拉起Service,并读取SharePreferences中实时保存的stepCount临时值。这个验证项最能体现工程完整性,建议在演示时主动展示。
6.3 参数调优:传感器采样与GPS间隔的平衡表
| 参数 | 建议值 | 原因 |
|---|---|---|
| STEP_DETECTOR采样 | 默认 | 硬件自动触发,无需设置 |
| GPS最小时长间隔 | 2000ms | 兼顾轨迹平滑和电量 |
| GPS最小距离间隔 | 5m | 滤除原地抖动 |
| Room批量写入周期 | 30s | 减少IO压力 |
| 前台Service通知ID | 1001 | 全局唯一即可 |
GPS耗电是最直接影响体验的参数。在低电量模式下,可以把GPS间隔动态调整为10秒,并降低轨迹精度,通过注册BatteryManager的ACTION_BATTERY_CHANGED广播实现动态切换。
6.4 答辩追问:直接回答“哪里用了原生特性”
传感器框架、自定义View绘制轨迹、前台Service通知栏、Room数据库升级,四个技术点都是Android平台独立实现的,不依赖云服务或第三方运动SDK。在论文中把运动记录准确性这个不足写透——硬件传感器误差、GPS漂移、无后台保活白名单,反而比堆砌“完整闭环”让老师更认可。
本文还有配套的精品资源,点击获取