HarmonyOS5 运动健康app(二):健康跑步(附代码)

一、数据模型:构建运动记录的数字骨架

代码通过RunRecord接口定义了跑步数据的核心结构:

interface RunRecord {id: string;         // 记录唯一标识date: Date;         // 跑步日期distance: number;   // 距离(公里)duration: number;   // 时长(分钟)pace: number;       // 配速(分钟/公里)
}

这一模型以"距离-时长-配速"为核心维度,通过iddate建立时间轴索引。在实际跑步场景中,当用户点击"结束跑步"时,系统会基于实时数据生成RunRecord对象并添加到runRecords数组中:

const newRecord: RunRecord = {id: Date.now().toString(),date: new Date(),distance: this.currentDistance,duration: this.currentDuration,pace: this.currentPace
};
this.runRecords = [newRecord, ...this.runRecords];

这种设计遵循了"最小必要数据"原则,既满足基础运动分析需求,又降低了数据存储与计算的复杂度。

二、状态管理:实现运动数据的实时响应

应用通过@State装饰器管理五大核心状态,构建起数据流转的中枢系统:

  • isRunning:标记跑步状态(进行中/已结束),控制界面按钮与数据展示逻辑
  • currentDistance/duration/pace:实时跑步数据,通过定时器模拟GPS更新
  • showHistory:控制历史记录列表的展开/收起状态

核心状态更新逻辑体现在startRunendRun方法中。当用户点击"开始跑步"时,系统启动定时器以100ms为周期更新数据:

this.intervalId = setInterval(() => {this.currentDistance += 0.01;    // 模拟每100ms增加10米this.currentDuration += 0.1;     // 模拟每100ms增加0.1秒if (this.currentDistance > 0) {this.currentPace = this.currentDuration / this.currentDistance / 60;}
}, 100);

endRun方法则负责清除定时器、保存记录并重置状态,形成"数据采集-存储-重置"的闭环。

三、UI组件:打造直观的运动数据可视化体验

应用采用"统计头部-数据卡片-历史列表"的三层布局,通过ArkTS的声明式UI特性实现动态渲染:

  1. 统计头部组件(StatsHeader)
    采用三栏式布局展示总跑步次数、总距离与平均配速,通过reduce方法对历史记录进行聚合计算:
Text(`${this.runRecords.reduce((sum, r) => sum + r.distance, 0).toFixed(1)}km`)

数据展示遵循"数值+单位"的层级结构,大号字体突出核心数字,小号字体标注单位与说明,符合用户"先看结果再辨含义"的认知习惯。

  1. 跑步数据卡片(RunDataCard)
    通过isRunning状态动态切换显示逻辑:跑步中时突出距离与时长数据,搭配配速与时长的分栏展示;跑步结束后则展示今日汇总数据。按钮设计采用绿色(开始)与红色(结束)的高对比度配色,强化操作反馈。
  2. 历史记录列表(HistoryList)
    通过showHistory状态控制显示/隐藏,使用ForEach循环渲染历史记录,每条记录包含日期、时长、距离与配速信息。当记录为空时显示空状态提示,提升用户体验的完整性。

四、核心算法:从原始数据到运动洞察

代码中包含三个关键数据处理函数,将原始运动数据转化为可解读的运动指标:

  • formatTime:将秒数转换为"分:秒"格式(如123秒转为2:03),便于用户快速理解运动时长
  • getTodayDistance/duration/pace:通过日期筛选与数组聚合,计算今日运动数据,支持用户查看短期运动趋势
  • formatDate:将Date对象转换为"月/日"格式(如6/15),简化历史记录的时间展示

getTodayPace为例,其核心逻辑是通过筛选今日记录并计算平均配速:

const totalDistance = this.getTodayDistance();
const totalDuration = this.getTodayDuration();
if (totalDistance === 0) return 0;
return totalDuration / totalDistance / 60;

五、附:代码
import promptAction from '@ohos.promptAction';// 跑步记录接口
interface RunRecord {id: string;date: Date;distance: number; // 公里duration: number; // 分钟pace: number;     // 配速(分钟/公里)
}
@Entry
@Component
struct Index {@State runRecords: RunRecord[] = [];  // 跑步记录列表@State isRunning: boolean = false;    // 是否正在跑步@State currentDistance: number = 0;   // 当前跑步距离@State currentDuration: number = 0;   // 当前跑步时长@State currentPace: number = 0;       // 当前配速@State showHistory: boolean = false;  // 是否显示历史记录private intervalId: number = -1;      // 定时器ID// 开始跑步private startRun() {this.isRunning = true;this.currentDistance = 0;this.currentDuration = 0;this.currentPace = 0;// 模拟GPS定位更新this.intervalId = setInterval(() => {this.currentDistance += 0.01;  // 模拟每100ms增加10米this.currentDuration += 0.1;   // 模拟每100ms增加0.1秒// 更新配速if (this.currentDistance > 0) {this.currentPace = this.currentDuration / this.currentDistance / 60;}}, 100);}// 结束跑步private endRun() {clearInterval(this.intervalId);// 创建新记录const newRecord: RunRecord = {id: Date.now().toString(),date: new Date(),distance: this.currentDistance,duration: this.currentDuration,pace: this.currentPace};// 添加到记录列表this.runRecords = [newRecord, ...this.runRecords];// 重置状态this.isRunning = false;this.currentDistance = 0;this.currentDuration = 0;this.currentPace = 0;promptAction.showToast({ message: '跑步记录已保存' });}// 格式化时间为分:秒private formatTime(seconds: number): string {const minutes = Math.floor(seconds / 60);const secs = Math.floor(seconds % 60);return `${minutes}:${secs < 10 ? '0' : ''}${secs}`;}// 获取今日跑步距离private getTodayDistance(): number {const today = new Date();today.setHours(0, 0, 0, 0);const todayRuns = this.runRecords.filter(record => {const recordDate = new Date(record.date);recordDate.setHours(0, 0, 0, 0);return recordDate.getTime() === today.getTime();});return todayRuns.reduce((sum, record) => sum + record.distance, 0);}// 获取今日跑步时长private getTodayDuration(): number {const today = new Date();today.setHours(0, 0, 0, 0);const todayRuns = this.runRecords.filter(record => {const recordDate = new Date(record.date);recordDate.setHours(0, 0, 0, 0);return recordDate.getTime() === today.getTime();});return todayRuns.reduce((sum, record) => sum + record.duration, 0);}// 获取今日平均配速private getTodayPace(): number {const totalDistance = this.getTodayDistance();const totalDuration = this.getTodayDuration();if (totalDistance === 0) return 0;return totalDuration / totalDistance / 60;}// 格式化日期private formatDate(date: Date): string {return `${date.getMonth() + 1}/${date.getDate()}`;}// 头部统计组件@BuilderStatsHeader() {Column() {Text('跑步统计').fontSize(18).fontWeight(FontWeight.Bold).margin({ bottom: 15 })Row() {Column() {Text(`${this.runRecords.length}`).fontSize(24).fontWeight(FontWeight.Bold)Text('总次数').fontSize(12).fontColor('#888').margin({top: 5})}.width('33%')Column() {Text(`${this.runRecords.reduce((sum, r) => sum + r.distance, 0).toFixed(1)}km`).fontSize(24).fontWeight(FontWeight.Bold)Text('总距离').fontSize(12).fontColor('#888').margin({top: 5})}.width('33%')Column() {Text(`${(this.runRecords.reduce((sum, r) => sum + r.pace, 0) / this.runRecords.length || 0).toFixed(2)}min/km`).fontSize(24).fontWeight(FontWeight.Bold)Text('平均配速').fontSize(12).fontColor('#888').margin({top: 5})}.width('33%')}.width('100%')}.width('100%').padding(15).backgroundColor('#F8F9FC').borderRadius(12)}// 跑步数据卡片@BuilderRunDataCard() {Column() {Text(this.isRunning ? '跑步中' : '今日跑步数据').fontSize(18).fontWeight(FontWeight.Bold).margin({ bottom: 25 })if (this.isRunning) {// 跑步中数据显示Column() {Text(`${this.currentDistance.toFixed(2)}km`).fontSize(42).fontWeight(FontWeight.Bold).margin({ bottom: 15 })Text(`${this.formatTime(this.currentDuration)}`).fontSize(24).margin({ bottom: 25 })Row() {Column() {Text(`${this.currentPace.toFixed(2)}min/km`).fontSize(16).fontWeight(FontWeight.Bold)Text('配速').fontSize(12).fontColor('#888').margin({top: 5})}.width('50%')Column() {Text(`${this.formatTime(this.currentDuration)}`).fontSize(16).fontWeight(FontWeight.Bold)Text('时长').fontSize(12).fontColor('#888').margin({top: 5})}.width('50%')}.width('100%')}.width('100%').alignItems(HorizontalAlign.Center).margin({ bottom: 25 })} else {// 跑步后数据显示Row() {Column() {Text(`${this.getTodayDistance().toFixed(2)}km`).fontSize(24).fontWeight(FontWeight.Bold)Text('距离').fontSize(12).fontColor('#888').margin({top: 5})}.width('33%')Column() {Text(`${this.formatTime(this.getTodayDuration())}`).fontSize(24).fontWeight(FontWeight.Bold)Text('时长').fontSize(12).fontColor('#888').margin({top: 5})}.width('33%')Column() {Text(`${this.getTodayPace().toFixed(2)}min/km`).fontSize(24).fontWeight(FontWeight.Bold)Text('配速').fontSize(12).fontColor('#888').margin({top: 5})}.width('33%')}.width('100%').margin({ bottom: 25 })}if (this.isRunning) {Button('结束跑步').width('100%').height(45).backgroundColor('#E53935').fontColor(Color.White).fontSize(16).borderRadius(8).onClick(() => this.endRun())} else {Button('开始跑步').width('100%').height(45).backgroundColor('#2E7D32').fontColor(Color.White).fontSize(16).borderRadius(8).onClick(() => this.startRun())}}.width('100%').padding(15).backgroundColor(Color.White).borderRadius(12).shadow({ radius: 3, color: '#0000001A' })}// 历史记录列表@BuilderHistoryList() {if (this.showHistory) {Column() {Text('跑步历史').fontSize(16).fontWeight(FontWeight.Bold).margin({ bottom: 15 })if (this.runRecords.length === 0) {Text('暂无跑步记录').fontSize(14).fontColor('#AAA').margin({ top: 40 })} else {List() {ForEach(this.runRecords, (record: RunRecord) => {ListItem() {Row() {Column() {Text(this.formatDate(record.date)).fontSize(14)Text(`${this.formatTime(record.duration)}`).fontSize(12).fontColor('#888').margin({top: 4})}.width('40%')Column() {Text(`${record.distance}km`).fontSize(14).fontWeight(FontWeight.Bold)Text(`${record.pace.toFixed(2)}min/km`).fontSize(12).fontColor('#888').margin({top: 4})}.width('60%')}.width('100%').padding(8)}})}}}.width('100%').padding(15).backgroundColor('#F8F9FC').borderRadius(12).layoutWeight(1)}}build() {Column() {// 统计头部this.StatsHeader()// 跑步数据卡片this.RunDataCard()// 历史记录this.HistoryList()// 底部按钮Button(this.showHistory ? '隐藏历史' : '显示历史').width('100%').margin({ top: 15 }).height(40).fontSize(14).borderRadius(8).backgroundColor('#E0E0E0').fontColor('#333').onClick(() => {this.showHistory = !this.showHistory;})}.width('100%').height('100%').padding(12).backgroundColor('#FCFDFF')}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/news/909316.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

29-Oracle 23ai Flashback Log Placement(闪回日志灵活配置)

小伙伴们有没有被各种存储路径满导致的业务崩&#xff0c;半夜起来清理的经历。一不小心 FRA写满了&#xff0c;导致了实例hang住。 OCM考试&#xff0c;时不时就会冒出来这个直接给instance hang&#xff0c;本就卡的环境中脑袋都卡壳、无从下手&#xff0c;一脸懵直接崩。 …

React表单处理:如何获取输入框(input)的值?(受控组件)

系列回顾&#xff1a; 在前面的文章中&#xff0c;我们已经掌握了State、Props、事件处理、列表渲染和条件渲染。我们的应用已经能展示动态内容并响应用户的点击。现在&#xff0c;我们要 tackling 一个非常常见的需求&#xff1a;如何获取用户在表单输入框&#xff08;<inp…

探索现代 Web 开发:从 HTML5 到 Vue.js 的全栈之旅

在当今快速发展的互联网时代&#xff0c;Web 开发已经成为构建数字世界的重要基石。无论是企业级应用、社交媒体平台&#xff0c;还是个人博客和电商平台&#xff0c;Web 技术都在背后默默支撑着这些系统的运行。随着前端技术的不断演进&#xff0c;开发者们已经不再局限于传统…

ElasticSearch聚合查询从15秒到1.2秒的深度优化实践

一、问题背景 在金融风控场景中,我们需要对90天内的交易数据进行多维度聚合分析(按风险等级、地区、金额分段等)。随着数据量增长到日均3000万+记录,原有查询响应时间逐渐恶化至15秒以上,严重影响了业务决策效率。 二、原始架构性能分析 1. 集群拓扑 # 原单节点配置 N…

2025.06.09【读书笔记】|PromptBio:让生信分析更简单的AI平台

文章目录 一、PromptBio 是什么&#xff1f;二、主要功能介绍1. 对话式智能体&#xff0c;像聊天一样做分析2. 自动化工作流&#xff0c;省时省力3. 数据管理一站式搞定4. 机器学习也能一键搞定5. “无代码”到“全代码”&#xff0c;人人都能用 三、适合哪些人用&#xff1f;四…

实战解析:如何用克魔(KeyMob)等工具构建iOS应用稳定性与数据可观测体系

在iOS开发项目逐渐走向复杂化的今天&#xff0c;团队对“可观测性”的要求正不断提升。开发者不仅要知道App是否运行正常&#xff0c;更要明确“为什么异常、在哪里异常、是否可复现”。传统的调试工具往往侧重单一维度&#xff0c;要么是资源监控、要么是日志分析&#xff0c;…

如何轻松实现多源混算报表

报表作为综合业务&#xff0c;数据来源多种多样。传统实现多源混合查询报表要通过 ETL 将数据同库&#xff0c;但这种方式数据时效性太差使用场景受限。通过逻辑数仓能获得较强的数据实时性&#xff0c;但体系又过于沉重&#xff0c;为报表业务搭建逻辑数仓有点得不偿失。需要一…

Docker|简单入门

文章目录 Docker简介Docker和虚拟机的联系和区别基本原理和概念镜像容器仓库 Docker安装配置容器化和Dockerfile实践环节Docker Compose Docker简介 Docker是一个用于构建build、运行run、传送share应用程序的平台&#xff0c;可以把应用程序打包成一个个的集装箱&#xff0c;…

阿里云云原生数据库PolarDB和普通云数据库的区别?

文章目录 前言一、云数据库的演进&#xff1a;从“托管”到“原生”的跨越二、PolarDB的核心创新&#xff1a;重新定义云数据库的能力边界1. 存算分离架构&#xff1a;打破资源绑定的“枷锁”2. 多模引擎与兼容生态&#xff1a;降低应用迁移成本3. 智能化运维&#xff1a;让数据…

SNN学习(4):真实的生物神经学中神经元和人脑结构学习

目录 一、基础知识 1 简单神经元回路中的信号运作 2 高级功能相关的复杂神经元回路 3 细胞体、树突和轴突 3.1 神经元细胞 3.2 非神经元细胞 3.3 神经胶质细胞 3.4 神经细胞的信号传递 3.4.1 动作电位的特性 3.4.2 兴奋和抑制 3.4.3 电传递 二、大脑皮层及视觉系统…

第六天 界面操作及美化(6.1 建立菜单及异步调用)

6.1 建立菜单及异步调用 在程序中&#xff0c;菜单&#xff08;Menu&#xff09;是一种常见的用户界面元素&#xff0c;在程序中起到了组织功能、提高用户体验、提供快捷方式和帮助文档等重要作用。通过合理使用菜单&#xff0c;可以使程序的功能更加清晰、操作更加便捷&#…

论文解析:一文弄懂ResNet(图像识别分类、目标检测)

目录 一、相关资源 二、Motivation 三、技术细节 1.残差学习过程 2.快捷连接类型 (1)Identity Shortcuts&#xff08;恒等捷径&#xff09; (2)Projection Shortcuts&#xff08;投影捷径&#xff09; (3)两种捷径对比 3.深层瓶颈结构Deeper Bottleneck Architectures…

动态规划算法的欢乐密码(二):路径问题

专栏&#xff1a;算法的魔法世界 个人主页&#xff1a;手握风云 一、例题讲解 1.1. 不同路径 题目要求是计算从网格的左上角&#xff08;起点&#xff09;到右下角&#xff08;终点&#xff09;的所有不同路径的数量。机器人每次只能向下或向右移动一步。如下图所示&#xff0…

嵌入式相关开源项目、库、资料------持续更新中

嵌入式相关开源项目、库、资料------持续更新中 学习初期最难找的就是找学习资料了&#xff0c;本贴精心汇总了一些嵌入式相关资源&#xff0c;包括但不限于编程语言、单片机、开源项目、物联网、操作系统、Linux、计算机等资源&#xff0c;并且在不断地更新中&#xff0c;致力…

图像处理与机器学习项目:特征提取、PCA与分类器评估

图像处理与机器学习项目:特征提取、PCA与分类器评估 项目概述 本项目将完成一个完整的图像处理与机器学习流程,包括数据探索、特征提取、主成分分析(PCA)、分类器实现和评估五个关键步骤。我们将使用Python的OpenCV、scikit-learn和scikit-image库来处理图像数据并实现机器…

MATLAB | 如何使用MATLAB获取《Nature》全部绘图 (附23-25年图像)

文末有全部图片资源 我在两年前更过如何用 MATLAB 爬取 《Nature》全部插图&#xff0c;最近又有人问我有没有下载好的24&#xff0c;25年插图的压缩包&#xff0c;于是又去拿代码运行了一下&#xff0c;发现两年前写的代码今天居然还能用&#xff0c;代码如下&#xff1a; f…

中国老年健康调查(CLHLS)数据挖掘教程(1)--CLHLS简介和数据下载

北京大学“中国老年健康影响因素跟踪调查&#xff08;简称‘中国老年健康调查’&#xff1b;英文名称为Chinese Longitudinal Healthy Longevity Survey (CLHLS)&#xff09;”及交叉学科研究由国家自然科学基金委主任基金应急项目、重大项目、重点项目及国际合作项目。1998-20…

基本多线程编译make命令

背景&#xff1a; 在ffmpeg源码编译的时候要等很久&#xff0c;快下班了&#xff0c;等不及。 解决方法&#xff1a; 使用多线程编译。 make -j{n} 如&#xff1a; make -j8详解&#xff1a;&#xff08;没时间看的可以返回了&#xff01;&#xff09; 在编译 FFmpeg 时使用…

MNIST数据集上朴素贝叶斯分类器(MATLAB例)

MNIST数据集上朴素贝叶斯分类器 Naive Bayes Classification fitcnb Train multiclass naive Bayes model Syntax Mdl fitcnb(Tbl,ResponseVarName) Mdl fitcnb(Tbl,formula) Mdl fitcnb(Tbl,Y) Mdl fitcnb(X,Y) Mdl fitcnb(___,Name,Value) [Mdl,AggregateOptimization…

网站设计小技巧:利用交互设计提升用户体验

现在很多企业朋友都会感觉到&#xff0c;做网站设计掌握不好设计网页的魂&#xff0c;换了很多设计方式可能效果都不理想。蒙特网站专注高端网站建设20多年&#xff0c;基于为华为、字节跳动、海康威视等头部企业打造网站的经验&#xff0c;今天将近期用户比较喜欢的网页设计方…