HarmonyOS 鸿蒙Next ArkUI入门训练营—健康生活实战 基于ArkUI的运动记录App

发布于 1周前 作者 phonegap100 来自 鸿蒙OS

HarmonyOS 鸿蒙Next ArkUI入门训练营—健康生活实战 基于ArkUI的运动记录App

前言

在参加了"HarmonyOS ArkUI入门训练营——健康生活实战"后,了解并学习了声明式UI开发框架及组件用法,本文是对笔者结营作品的介绍分享。

概述

  • 本案例的API版本为8,使用了路由跳转API(比如点击搜索按键,可以跳转至搜索结果的页面);一次开发多段部署API,使用其中介绍的自适应布局能力和响应式布局能力进行多设备(或多窗口尺寸)适配,保证应用在不同设备或不同窗口尺寸下可以正常显示。
  • 这是一个运动记录的应用,主要用于管理健康记录运动。可以添加运动信息,包括运动名称、运动时长,并自动计算消耗的卡路里,在记录页面可以查看所添加的运动记录。

运行效果图如下:

运行效果图

| | |

代码

源码见官方demo仓

正文

一、文件架构说明

文件架构图

二、主要的开发说明

本应用一共有“启动动画页面”、“主页面”和“搜索结果显示页面”三个页面。

1、/pages/logo.ets/

启动动画页面

这是启动动画页面,使用Shape绘制组件和绘制命令,切割绘制了一个图案,并添加显式动画,加载页面后设置定时器自动跳转至主页面。背景色及图案都使用了渐变色。以下为图案的绘制及动画设置的主要代码(完整代码请参考代码仓的源码)。pathCommand为预先定义好的路径绘制命令,clip裁剪后得到图案,然后设置放缩和透明度的属性动画。

Shape() {
  Path()
    .commands(this.pathCommand1)
    .fill('none')
    .linearGradient({ angle: 90, colors: [['#FBDBBB',0.0],['#F6A95C',1]] })
    .clip(new Path().commands(this.pathCommand1))

  Path()
    .commands(this.pathCommand2)
    .fill('none')
    .linearGradient({ angle: 90, colors: [['#FBDBBB',0.0],['#F6A95C',1]] })
    .clip(new Path().commands(this.pathCommand2))
}
.height('640px')
.width('640px')
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
  animateTo({
    duration: 1000,
    curve: this.curve1,
    delay: 100,
    onFinish: () =>{
      setTimeout(() =>{
        router.replace({ url: 'pages/SportsCategoryList' })
      }, 2000);
    }
  }, () =>{
    this.opacityValue = 1
    this.scaleValue = 1
  })
})

2、/pages/SportsCategoryList.ets/

这是主页面,使用Tabs组件把主页面分为两个页签:“主页”页签和“记录”页签。“主页”页签展示所有运动项目,按运动类别分类。在/model/SportsData中定义了运动类和纪录类,在/model/SportsDataModel中定义了将项目中定义的运动项数据初始化存放在一个数组的函数。

在主页面,点击运动项会弹出添加运动的弹窗。自定义弹窗组件Record并用@CustomDialog标识, calculate() 函数计算该项运动合计消耗的卡路里, @Builder valueInput() 定义了四行三列的使用 Grid 去布局数字输入器的组件。

先定义一个数组存放0到9的数字、“删除记录”字符串和回车删除输入的图片,然后在Grid容器中判断数据类型,再相应定义其点击事件,其中数字输入范围设定为0~999,按确认时若数字为0则会弹窗提示。

主页面截图

此外还定义了一个 mode 参数,为0时是添加运动记录模式,此时“删除记录”按键无响应事件,按确定按键时响应记录数组增加记录项;mode为1时是修改运动记录模式,点击“删除记录”会弹窗“是否确认删除”,然后从记录数组中移除该记录项;若是修改记录项的时间,笔者实现的逻辑是先从记录数组中删除原记录项,然后再插入新的记录项。以下为主要代码及实现逻辑的说明:

[@CustomDialog](/user/CustomDialog)
export struct Record {
  private mode: number = 0  //0:添加,1:修改
  private controller: CustomDialogController
  @State time: string = '0'
  @State sum: number = 0
  private Valueinput: any[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '删除记录', '0', $r('app.media.Back')]

  calculate() {
    if (this.time.length != 0) {
      this.sum = Math.round(parseInt(this.time) * this.sportsItem.value / 60)
    } else {
      this.sum = 0
    }
  }

  @Builder RecordItem(image: Resource, name: string, value: number) {
    //记录项的布局
  }

  @Builder valueInput() {
    Column() {
      Grid() {
        ForEach(this.Valueinput, (item: any) => {
          GridItem() {
            if (typeof (item) == 'string') {
              Text(item)  //显示数字
              //其他属性
                .onClick(() => {
                  if(item.length<2){ //即是数字按键
                  //避免‘023’这样的显示
                    if (this.time == '0') {
                      this.time = item       
                    }
                    //判断数值是否<=999,若否,则用拼接字符串的形式显示
                    else if (parseInt(this.time) < 999 && parseInt(this.time + item) < 999) {
                      this.time = this.time + item
                    }
                    else {
                      this.time = '999'
                    }
                    this.calculate()   //实时计算合计卡路里
                  }
                  //对于“删除记录”按键
                  else{
                    if(this.mode==1){
                      AlertDialog.show(
                        {
                          message: '确认删除这条运动记录吗?',
                          primaryButton: {
                            value: '取消',
                            action: () =>{}
                          },
                          secondaryButton: {
                            value: '确定',
                            action: () =>{
                            //删除记录项
                              RecordDataArray.splice(this.item_index, 1)
                              RecordSports.splice(this.item_index, 1)
                              this.controller.close()
                            }
                          },
                          cancel: () =>{}
                        }
                      )
                    }
                  }
                })

            } else if (typeof (item) == 'object') {
              Image(item).width(20).aspectRatio(1).objectFit(ImageFit.Contain)
              //回车删除的逻辑,对字符串取子串,然后再实时计算总消耗的卡路里
                .onClick(() => {
                  if (this.time.length > 1) {
                    this.time = this.time.substring(0, this.time.length - 1)
                  }
                  else if (this.time.length == 1) {
                    this.time = '0'
                  }
                  this.calculate()
                })
            }
          }
        })
      }
      .backgroundColor(Color.White)
      .columnsTemplate('1fr 1fr 1fr')
      .rowsTemplate('1fr 1fr 1fr 1fr')
      .columnsGap(0)
      .rowsGap(0)
      .width('100%')
      .height('35%')
    }
  }

  build() {
    //弹窗的整体布局
  }
}

//列表的运动项
@Component
export struct SportsGridItem {
  private sportsItem: SportsData
  private controller: CustomDialogController

  //弹窗的使用
  aboutToAppear() {
    this.controller = new CustomDialogController({
      builder: Record({ sportsItem: this.sportsItem }),
      alignment: DialogAlignment.Center
    })
  }

  build() {
    //运动项的布局
    }
    .onClick(() => {
      this.controller.open()
    })
  }
}

3、/pages/search_result.ets/

这是搜索结果显示页面,根据主页面搜索的词来检索是否有相关运动项,若有则显示此运动项,若无就显示"没有查到相关结果"。由于有运动项是同名但是不同配速,于是增加了关键字匹配。

struct Search_result {
  @State name:string = router.getParams()['sports']
  private sportsItem: SportsData[] = initializeOnStartup()
  private ResultDataArray: Array<SportsData> = []

  aboutToAppear() {
    let item;
    for (item of this.sportsItem) {
    //匹配关键字
      if (item.name.length >= this.name.length) {
        if (this.name == item.name.substring(0, this.name.length)) {
          this.ResultDataArray.push(item);
        }
      }
      else {
        if (this.name == item.name) {
          this.ResultDataArray.push(item);
        }
      }
    }
  }
  build(){
  Column() {
    //其他组件
    Scroll() {
    Column() {
      if (this.ResultDataArray.length != 0) {
        SportsGrid({ sportsItems: this.ResultDataArray })
      }
      else {
        Text('没有查到与此相关的结果').fontSize(19).width('100%').height(20).margin({ top: 12, left: 20 })
      }
    }
  }
  .scrollBar(BarState.Off)
  }
 }

结语

该Demo还是有待继续优化的,比如在记录页面修改或删除记录项时尚未能实时更新,但由于时间关系,还待优化解决。以上就是本次的小分享了~❀❀。


更多关于HarmonyOS 鸿蒙Next ArkUI入门训练营—健康生活实战 基于ArkUI的运动记录App的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html

9 回复

学习了

更多关于HarmonyOS 鸿蒙Next ArkUI入门训练营—健康生活实战 基于ArkUI的运动记录App的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:BV1S4411E7LY/?p=17

明天记得发动朋友为你的帖子投票哦~另外代码不要忘记提交代码仓哈!

ArkUI是HarmonyOS(鸿蒙)系统为开发者提供的一套用于构建用户界面的声明式前端框架。它支持使用TypeScript和eTS(Enhanced TypeScript,增强的TypeScript)语言进行开发,允许开发者通过简洁的语法描述UI界面及其逻辑。

在基于ArkUI开发运动记录App时,你需要关注以下几个关键点:

  1. 页面布局:利用ArkUI提供的布局组件(如FlexboxLayout、GridLayout等)来设计运动记录页面的整体布局。

  2. 数据绑定:通过ArkUI的数据绑定机制,将运动数据(如步数、运动时长等)实时展示在界面上。

  3. 组件交互:实现用户与界面之间的交互,如点击按钮开始/停止记录运动,滑动页面查看历史记录等。

  4. 样式定制:利用ArkUI的样式系统,为运动记录App定制独特的视觉风格。

  5. 生命周期管理:了解并管理ArkUI页面的生命周期,确保App在不同状态下的行为符合预期。

如果你在开发过程中遇到具体问题,建议查阅HarmonyOS官方文档或参考相关开发教程。如果问题依旧没法解决请联系官网客服,官网地址是:https://www.itying.com/category-93-b0.html

回到顶部