> 技术文档 > 116.[HarmonyOS NEXT 实战案例:健康应用] 进阶篇 - 健康数据仪表盘的交互功能与状态管理

116.[HarmonyOS NEXT 实战案例:健康应用] 进阶篇 - 健康数据仪表盘的交互功能与状态管理


[HarmonyOS NEXT 实战案例:健康应用] 进阶篇 - 健康数据仪表盘的交互功能与状态管理

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

116.[HarmonyOS NEXT 实战案例:健康应用] 进阶篇 - 健康数据仪表盘的交互功能与状态管理

引言

在基础篇中,我们学习了如何使用HarmonyOS NEXT的RowSplit组件构建健康数据仪表盘的基本布局。本篇教程将进一步深入,讲解如何为健康数据仪表盘添加交互功能和状态管理,使应用更加动态和用户友好。我们将重点关注数据切换、状态同步、动画效果等进阶特性,帮助你构建一个功能完善的健康数据仪表盘。

状态管理概述

在HarmonyOS NEXT应用开发中,状态管理是实现交互功能的关键。本案例中,我们主要使用以下状态管理机制:

状态类型 描述 使用场景 @State 组件内部状态,当状态变化时会触发组件重新渲染 用于管理当前选中的标签页、数据列表等 @Prop 父组件传递给子组件的状态,子组件不能修改 用于在子组件中展示父组件传递的数据 @Link 双向绑定状态,子组件可以修改父组件的状态 用于在子组件中修改父组件的状态 @Watch 状态监听器,当状态变化时触发回调函数 用于监听状态变化并执行相应操作

在本案例中,我们主要使用@State装饰器来管理组件内部状态:

@State currentTab: string = \'steps\'@State datalist: DataItem[] = [...]@State Status: StatusItem[] = [...]

交互功能实现

1. 标签页切换优化

在基础篇中,我们已经实现了基本的标签页切换功能。现在,我们将对其进行优化,添加过渡动画和更多交互细节:

@State private animationDuration: number = 300@State private opacityValue: number = 1ForEach(this.datalist, (item: DataItem) => { Button(item.name) .width(\'80%\') .height(50) .fontSize(16) .backgroundColor(this.currentTab === item.id ? \'#4CAF50\' : \'transparent\') .fontColor(this.currentTab === item.id ? \'#ffffff\' : \'#333333\') .onClick(() => { // 添加过渡动画 animateTo({ duration: this.animationDuration, curve: Curve.EaseInOut, iterations: 1, playMode: PlayMode.Normal, }, () => { this.opacityValue = 0 }) // 延迟切换标签页,等待淡出动画完成 setTimeout(() => { this.currentTab = item.id // 淡入动画 animateTo({  duration: this.animationDuration,  curve: Curve.EaseInOut,  iterations: 1,  playMode: PlayMode.Normal, }, () => {  this.opacityValue = 1 }) }, this.animationDuration) }) .margin({ bottom: 10 })})

在主内容区域,我们添加不透明度属性以实现淡入淡出效果:

Column() { if (this.currentTab === \'steps\') { // 步数内容 } else if (this.currentTab === \'heart\') { // 心率内容 } else { // 睡眠内容 }}.padding(20).opacity(this.opacityValue) // 添加不透明度属性

2. 步数数据交互

在步数页面,我们可以添加一个滑块组件,允许用户调整目标步数:

@State stepsGoal: number = 10000@State currentSteps: number = 7500if (this.currentTab === \'steps\') { Column() { Text(\'今日步数\') .fontSize(18) .margin({ bottom: 20 }) Progress({ value: this.currentSteps, total: this.stepsGoal, type: ProgressType.Linear }) .width(\'90%\') .height(20) Text(`${this.currentSteps}/${this.stepsGoal}`) .fontSize(16) .margin({ top: 10, bottom: 20 }) Text(\'调整目标步数\') .fontSize(14) .margin({ bottom: 10 }) Row() { Slider({ value: this.stepsGoal / 1000, min: 5, max: 20, step: 1, style: SliderStyle.OutSet }) .width(\'80%\') .showTips(true) .onChange((value: number) => {  this.stepsGoal = value * 1000 }) Text(`${this.stepsGoal}`) .fontSize(14) .margin({ left: 10 }) } .width(\'100%\') } .height(\'100%\') .justifyContent(FlexAlign.Center)}

3. 心率数据交互

在心率页面,我们可以添加一个时间范围选择器,允许用户查看不同时间段的心率数据:

@State timeRange: string = \'今日\'@State timeOptions: string[] = [\'今日\', \'本周\', \'本月\']@State heartRateData: { time: string, rate: number }[] = [ { time: \'08:00\', rate: 68 }, { time: \'10:00\', rate: 72 }, { time: \'12:00\', rate: 75 }, { time: \'14:00\', rate: 70 }, { time: \'16:00\', rate: 73 }, { time: \'18:00\', rate: 71 }]@State averageHeartRate: number = 72if (this.currentTab === \'heart\') { Column() { Text(\'心率监测\') .fontSize(18) .margin({ bottom: 20 }) Row() { ForEach(this.timeOptions, (option: string) => { Button(option)  .backgroundColor(this.timeRange === option ? \'#4CAF50\' : \'transparent\')  .fontColor(this.timeRange === option ? \'#ffffff\' : \'#333333\')  .height(30)  .margin({ right: 10 })  .onClick(() => { this.timeRange = option // 在实际应用中,这里可以根据选择的时间范围加载不同的数据 this.updateHeartRateData(option)  }) }) } .width(\'100%\') .margin({ bottom: 20 }) Image($r(\'app.media.big20\')) .width(\'90%\') .height(200) Text(`平均心率: ${this.averageHeartRate} bpm`) .fontSize(16) .margin({ top: 20 }) // 显示心率数据列表 Column() { ForEach(this.heartRateData, (item) => { Row() {  Text(item.time) .fontSize(14) .width(\'50%\')  Text(`${item.rate} bpm`) .fontSize(14) .width(\'50%\') .textAlign(TextAlign.End) } .width(\'100%\') .padding(10) .borderRadius(8) .backgroundColor(\'#f5f5f5\') .margin({ bottom: 10 }) }) } .width(\'90%\') .margin({ top: 20 }) } .height(\'100%\') .justifyContent(FlexAlign.Center)}

我们还需要添加一个方法来更新心率数据:

private updateHeartRateData(timeRange: string) { // 在实际应用中,这里可以根据选择的时间范围从服务器或本地数据库加载不同的数据 // 这里仅作为示例,生成一些模拟数据 if (timeRange === \'今日\') { this.heartRateData = [ { time: \'08:00\', rate: 68 }, { time: \'10:00\', rate: 72 }, { time: \'12:00\', rate: 75 }, { time: \'14:00\', rate: 70 }, { time: \'16:00\', rate: 73 }, { time: \'18:00\', rate: 71 } ] this.averageHeartRate = 72 } else if (timeRange === \'本周\') { this.heartRateData = [ { time: \'周一\', rate: 70 }, { time: \'周二\', rate: 71 }, { time: \'周三\', rate: 73 }, { time: \'周四\', rate: 72 }, { time: \'周五\', rate: 74 }, { time: \'周六\', rate: 69 }, { time: \'周日\', rate: 68 } ] this.averageHeartRate = 71 } else { this.heartRateData = [ { time: \'第1周\', rate: 71 }, { time: \'第2周\', rate: 72 }, { time: \'第3周\', rate: 70 }, { time: \'第4周\', rate: 73 } ] this.averageHeartRate = 72 }}

4. 睡眠数据交互

在睡眠页面,我们可以添加一个日期选择器,允许用户查看不同日期的睡眠数据:

@State selectedDate: string = \'今天\'@State dateOptions: string[] = [\'今天\', \'昨天\', \'前天\']@State sleepDuration: number = 7.5 // 小时if (this.currentTab === \'sleep\') { Column() { Text(\'睡眠分析\') .fontSize(18) .margin({ bottom: 20 }) Row() { ForEach(this.dateOptions, (option: string) => { Button(option)  .backgroundColor(this.selectedDate === option ? \'#4CAF50\' : \'transparent\')  .fontColor(this.selectedDate === option ? \'#ffffff\' : \'#333333\')  .height(30)  .margin({ right: 10 })  .onClick(() => { this.selectedDate = option // 在实际应用中,这里可以根据选择的日期加载不同的数据 this.updateSleepData(option)  }) }) } .width(\'100%\') .margin({ bottom: 20 }) Text(`睡眠时长: ${this.sleepDuration} 小时`) .fontSize(16) .margin({ bottom: 20 }) // 睡眠状态图表 Row() { ForEach(this.Status, (item: StatusItem) => { Column() {  Text(`${item.value}%`) .fontSize(14) .margin({ bottom: 5 })  Stack() { Column() .width(30) .height(100 * item.value / 100) .backgroundColor(item.color) .borderRadius(8)  }  .width(30)  .height(100)  .backgroundColor(\'#f0f0f0\')  .borderRadius(8)  Text(item.label) .fontSize(14) .margin({ top: 5 }) } .margin({ right: 20 }) }) } .margin({ bottom: 20 }) // 睡眠状态图例 Column() { ForEach(this.Status, (item: StatusItem) => { Row() {  Circle() .width(15) .height(15) .fill(item.color) .margin({ right: 10 })  Text(`${item.label}: ${item.value}%`) .fontSize(14) } .margin({ bottom: 5 }) }) } } .height(\'100%\') .justifyContent(FlexAlign.Center)}

我们还需要添加一个方法来更新睡眠数据:

private updateSleepData(date: string) { // 在实际应用中,这里可以根据选择的日期从服务器或本地数据库加载不同的数据 // 这里仅作为示例,生成一些模拟数据 if (date === \'今天\') { this.Status = [ { value: 30, color: \'#4CAF50\', label: \'深睡\' }, { value: 50, color: \'#8BC34A\', label: \'浅睡\' }, { value: 20, color: \'#CDDC39\', label: \'清醒\' } ] this.sleepDuration = 7.5 } else if (date === \'昨天\') { this.Status = [ { value: 35, color: \'#4CAF50\', label: \'深睡\' }, { value: 45, color: \'#8BC34A\', label: \'浅睡\' }, { value: 20, color: \'#CDDC39\', label: \'清醒\' } ] this.sleepDuration = 8.0 } else { this.Status = [ { value: 25, color: \'#4CAF50\', label: \'深睡\' }, { value: 55, color: \'#8BC34A\', label: \'浅睡\' }, { value: 20, color: \'#CDDC39\', label: \'清醒\' } ] this.sleepDuration = 7.0 }}

高级状态管理

1. 使用@Watch监听状态变化

我们可以使用@Watch装饰器监听状态变化,并执行相应的操作:

@Watch(\'currentTab\')onCurrentTabChange(newValue: string, oldValue: string) { console.info(`Tab changed from ${oldValue} to ${newValue}`) // 可以在这里执行一些与标签页切换相关的操作,如加载数据、更新UI等 if (newValue === \'steps\') { // 加载步数数据 } else if (newValue === \'heart\') { // 加载心率数据 this.updateHeartRateData(this.timeRange) } else { // 加载睡眠数据 this.updateSleepData(this.selectedDate) }}

2. 使用@StorageLink实现数据持久化

在实际应用中,我们可能需要将用户的设置和数据保存到持久存储中。HarmonyOS NEXT提供了@StorageLink装饰器,可以将状态与持久存储链接起来:

@StorageLink(\'stepsGoal\') stepsGoal: number = 10000

使用@StorageLink装饰器后,当stepsGoal状态变化时,它会自动保存到持久存储中。当应用重新启动时,它会从持久存储中恢复状态。

3. 使用@Provide@Consume实现跨组件状态共享

在大型应用中,我们可能需要在多个组件之间共享状态。HarmonyOS NEXT提供了@Provide@Consume装饰器,可以实现跨组件状态共享:

// 在父组件中提供状态@Provide(\'currentTab\') currentTab: string = \'steps\'// 在子组件中消费状态@Consume(\'currentTab\') currentTab: string

使用@Provide@Consume装饰器后,子组件可以直接访问和修改父组件提供的状态,而不需要通过属性传递。

动画效果

1. 标签页切换动画

我们已经在前面的代码中实现了标签页切换的淡入淡出动画。这里我们再添加一个滑动动画,使标签页切换更加流畅:

@State private translateX: number = 0// 在标签页切换时更新translateXonCurrentTabChange(newValue: string, oldValue: string) { // 计算滑动方向 const oldIndex = this.datalist.findIndex(item => item.id === oldValue) const newIndex = this.datalist.findIndex(item => item.id === newValue) const direction = newIndex > oldIndex ? 1 : -1 // 设置初始位置 this.translateX = direction * 100 // 执行滑动动画 animateTo({ duration: this.animationDuration, curve: Curve.EaseInOut, iterations: 1, playMode: PlayMode.Normal, }, () => { this.translateX = 0 })}// 在主内容区域添加transform属性Column() { if (this.currentTab === \'steps\') { // 步数内容 } else if (this.currentTab === \'heart\') { // 心率内容 } else { // 睡眠内容 }}.padding(20).opacity(this.opacityValue).transform({ translate: { x: `${this.translateX}%` } }) // 添加transform属性

2. 进度条动画

我们可以为步数进度条添加一个动画效果,使其从0逐渐增加到当前值:

@State animatedSteps: number = 0// 在组件的aboutToAppear生命周期函数中启动动画aboutToAppear() { // 启动进度条动画 this.startProgressAnimation()}private startProgressAnimation() { // 重置动画值 this.animatedSteps = 0 // 计算动画步长 const steps = 50 const stepValue = this.currentSteps / steps const stepDuration = 10 // 启动动画 let currentStep = 0 const timer = setInterval(() => { currentStep++ this.animatedSteps = Math.min(stepValue * currentStep, this.currentSteps) if (currentStep >= steps) { clearInterval(timer) } }, stepDuration)}// 在Progress组件中使用animatedStepsProgress({ value: this.animatedSteps, total: this.stepsGoal, type: ProgressType.Linear})

组件封装

为了提高代码的可维护性和复用性,我们可以将一些常用的UI元素封装成独立的组件。

1. 导航按钮组件

@Componentstruct NavButton { @Prop icon: Resource @Prop name: string @Prop id: string @Link currentTab: string build() { Button(this.name) .width(\'80%\') .height(50) .fontSize(16) .backgroundColor(this.currentTab === this.id ? \'#4CAF50\' : \'transparent\') .fontColor(this.currentTab === this.id ? \'#ffffff\' : \'#333333\') .onClick(() => { this.currentTab = this.id }) .margin({ bottom: 10 }) }}

使用这个组件可以简化导航按钮的创建:

ForEach(this.datalist, (item: DataItem) => { NavButton({ icon: item.icon, name: item.name, id: item.id, currentTab: $currentTab })})

2. 数据卡片组件

@Componentstruct DataCard { @Prop title: string @BuilderParam content: () => void build() { Column() { Text(this.title) .fontSize(18) .margin({ bottom: 20 }) this.content() } .width(\'100%\') .padding(20) .backgroundColor(\'#ffffff\') .borderRadius(10) .shadow({ radius: 5, color: \'#0000001A\', offsetX: 0, offsetY: 2 }) }}

使用这个组件可以创建统一风格的数据卡片:

DataCard({ title: \'今日步数\', content: () => { Column() { Progress({ value: this.animatedSteps, total: this.stepsGoal, type: ProgressType.Linear }) .width(\'90%\') .height(20) Text(`${this.currentSteps}/${this.stepsGoal}`) .fontSize(16) .margin({ top: 10 }) } }})

总结

在本教程中,我们学习了如何为健康数据仪表盘添加交互功能和状态管理,包括:

  1. 使用@State@Watch@StorageLink@Provide@Consume等装饰器管理状态
  2. 实现标签页切换、数据筛选等交互功能
  3. 添加淡入淡出、滑动等动画效果
  4. 封装导航按钮、数据卡片等可复用组件