0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

HarmonyOS开发案例:【生活健康app之首页启动】(1)

jf_46214456 来源:jf_46214456 作者:jf_46214456 2024-05-10 11:49 次阅读

介绍

如何实现一个简单的健康生活应用,主要功能包括:

  1. 用户可以创建最多6个健康生活任务(早起,喝水,吃苹果,每日微笑,刷牙,早睡),并设置任务目标、是否开启提醒、提醒时间、每周任务频率。
  2. 用户可以在主页面对设置的健康生活任务进行打卡,其中早起、每日微笑、刷牙和早睡只需打卡一次即可完成任务,喝水、吃苹果需要根据任务目标量多次打卡完成。
  3. 主页可显示当天的健康生活任务完成进度,当天所有任务都打卡完成后,进度为100%,并且用户的连续打卡天数加一。
  4. 当用户连续打卡天数达到3、7、30、50、73、99天时,可以获得相应的成就。成就在获得时会以动画形式弹出,并可以在“成就”页面查看。
  5. 用户可以查看以前的健康生活任务完成情况。

本应用的运行效果如下图所示:

%E5%81%A5%E5%BA%B7%E7%94%9F%E6%B4%BB.gif

相关概念

  • [@Observed 和 @ObjectLink]:@Observed适用于类,表示类中的数据变化由UI页面管理;@ObjectLink应用于被@Observed装饰类的对象。
  • [@Consume 和 @Provide]:@Provide作为数据提供者,可以更新子节点的数据,触发页面渲染。@Consume检测到@Provide数据更新后,会发起当前视图的重新渲染。
  • [Flex]:一个功能强大的容器组件,支持横向布局,竖向布局,子组件均分和流式换行布局。
  • [List]:List是很常用的滚动类容器组件之一,它按照水平或者竖直方向线性排列子组件, List的子组件必须是ListItem,它的宽度默认充满List的宽度。
  • [TimePicker]:TimePicker是选择时间的滑动选择器组件,默认以00:00至23:59的时间区创建滑动选择器。
  • [Toggle]:组件提供勾选框样式、状态按钮样式及开关样式。
  • [后台代理提醒]:使用后台代理提醒能力后,应用可以被冻结或退出,计时和弹出提醒的功能将被后台系统服务代理。
  • [关系型数据库(Relational Database,RDB)]:一种基于关系模型来管理数据的数据库。

环境搭建

软件要求

  • [DevEco Studio]版本:DevEco Studio 3.1 Release。
  • OpenHarmony SDK版本:API version 9。

硬件要求

  • 开发板类型:[润和RK3568开发板]。
  • OpenHarmony系统:3.2 Release。

环境搭建

完成本篇Codelab我们首先要完成开发环境的搭建,本示例以RK3568开发板为例,参照以下步骤进行:

  1. [获取OpenHarmony系统版本]:标准系统解决方案(二进制)。以3.2 Release版本为例:
  2. 搭建烧录环境。
    1. [完成DevEco Device Tool的安装]
    2. [完成RK3568开发板的烧录]
  3. 搭建开发环境。
    1. 开始前请参考[工具准备],完成DevEco Studio的安装和开发环境配置。
    2. 开发环境配置完成后,请参考[使用工程向导]创建工程(模板选择“Empty Ability”),选择JS或者eTS语言开发。
    3. 工程创建完成后,选择使用[真机进行调测]。
    4. 开发前请熟悉鸿蒙开发指导文档:[gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md]

代码结构解读

本篇Codelab只对核心代码进行讲解,完整代码可以直接从gitee获取。

├─entry/src/main/ets                 // 代码区
│  ├─common
│  │  ├─constants
│  │  │  └─CommonConstants.ets       // 公共常量
│  │  ├─database
│  │  │  ├─rdb                       // 数据库
│  │  │  │  ├─RdbHelper.ets
│  │  │  │  ├─RdbHelperImp.ets
│  │  │  │  ├─RdbUtil.ets
│  │  │  │  └─TableHelper.ets
│  │  │  └─tables                    // 数据库接口
│  │  │     ├─DayInfoApi.ets
│  │  │     ├─GlobalInfoApi.ets
│  │  │     └─TaskInfoApi.ets
│  │  └─utils
│  │     ├─BroadCast.ets             // 通知
│  │     ├─GlobalContext.ets         // 全局上下文
│  │     ├─HealthDataSrcMgr.ets      // 数据管理单例
│  │     ├─Logger.ets                // 日志类
│  │     └─Utils.ets                 // 工具类
│  ├─entryability
│  │  └─EntryAbility.ets             // 程序入口类
│  ├─model                           // model
│  │  ├─AchieveModel.ets
│  │  ├─DatabaseModel.ets            // 数据库model
│  │  ├─Mine.ets
│  │  ├─NavItemModel.ets             // 菜单栏model
│  │  ├─RdbColumnModel.ets           // 数据库表数据
│  │  ├─TaskInitList.ets
│  │  └─WeekCalendarModel.ets        // 日历model
│  ├─pages
│  │  ├─AdvertisingPage.ets          // 广告页
│  │  ├─MainPage.ets                 // 应用主页面
│  │  ├─MinePage.ets                 // 我的页面
│  │  ├─SplashPage.ets               // 启动页
│  │  ├─TaskEditPage.ets             // 任务编辑页面
│  │  └─TaskListPage.ets             // 任务列表页面
│  ├─service
│  │  └─ReminderAgent.ets            // 后台提醒
│  ├─view
│  │  ├─dialog                       // 弹窗组件
│  │  │  ├─AchievementDialog.ets     // 成就弹窗
│  │  │  ├─CustomDialogView.ets      // 自定义弹窗
│  │  │  ├─TaskDetailDialog.ets      // 打卡弹窗
│  │  │  ├─TaskDialogView.ets        // 任务对话框
│  │  │  ├─TaskSettingDialog.ets     // 任务编辑相关弹窗
│  │  │  └─UserPrivacyDialog.ets
│  │  ├─home                         // 主页面相关组件
│  │  │  ├─AddBtnComponent.ets       // 添加任务按钮组件
│  │  │  ├─HomeTopComponent.ets      // 首页顶部组件
│  │  │  ├─TaskCardComponent.ets     // 任务item组件件
│  │  │  └─WeekCalendarComponent.ets // 日历组件
│  │  ├─task                         // 任务相关组件
│  │  │  ├─TaskDetailComponent.ets   // 任务编辑详情组件
│  │  │  ├─TaskEditListItem.ets      // 任务编辑行内容
│  │  │  └─TaskListComponent.ets     // 任务列表组件
│  │  ├─AchievementComponent.ets     // 成就页面
│  │  ├─BadgeCardComponent.ets       // 勋章卡片组件
│  │  ├─BadgePanelComponent.ets      // 勋章面板组件
│  │  ├─HealthTextComponent.ets      // 自定义text组件
│  │  ├─HomeComponent.ets            // 首页页面
│  │  ├─ListInfo.ets                 // 用户信息列表
│  │  ├─TitleBarComponent.ets        // 成就标题组件
│  │  └─UserBaseInfo.ets             // 用户基本信息
│  └─viewmodel                       // viewmodel
│     ├─AchievementInfo.ets          // 成就信息
│     ├─AchievementMapInfo.ets       // 成就map信息
│     ├─AchievementViewModel.ets     // 成就相关模块
│     ├─BroadCastCallBackInfo.ets    // 通知回调信息
│     ├─CalendarViewModel.ets        // 日历相关模块
│     ├─CardInfo.ets                 // 成就卡片信息
│     ├─ColumnInfo.ets               // 数据库表结构
│     ├─CommonConstantsInfo.ets      // 公共常量信息
│     ├─DayInfo.ets                  // 每日信息
│     ├─GlobalInfo.ets               // 全局信息
│     ├─HomeViewModel.ets            // 首页相关模块
│     ├─PublishReminderInfo.ets      // 发布提醒信息
│     ├─ReminderInfo.ets             // 提醒信息
│     ├─TaskInfo.ets                 // 任务信息
│     ├─TaskViewModel.ets            // 任务设置相关模块
│     ├─WeekCalendarInfo.ets         // 日历信息
│     └─WeekCalendarMethodInfo.ets   // 日历方法信息
└─entry/src/main/resources           // 资源文件夹

`HarmonyOSOpenHarmony鸿蒙文档籽料:mau123789v直接拿`

搜狗高速浏览器截图20240326151450.png

应用架构分析

本应用的基本架构如下图所示,数据库为其他服务提供基础的用户数据,主要业务包括:用户可以查看和编辑自己的健康任务并进行打卡、查看成就。UI层提供了承载上述业务的UI界面。

应用主页面

本节将介绍如何给应用添加一个启动页,设计应用的主界面,以及首页的界面开发和数据展示。

启动页

首先我们需要给应用添加一个启动页,启动页里我们需要用到一个定时器来实现启动页展示固定时间后跳转应用主页的功能,效果图如下:

打开应用时会进入此页面,具体实现逻辑是:

通过修改/entry/src/main/ets/entryability里的loadContent路径可以改变应用的入口文件,我们需要把入口文件改为我们写的SplashPage启动页面。

// EntryAbility.ets
windowStage.loadContent('pages/SplashPage', (err, data) = > {    
  if (err.code) {...}    
  Logger.info('windowStage','Succeeded in loading the content. Data: ' + JSON.stringify(data));
});

在SplashPage启动页的文件里通过首选项来实现是否需要弹“权限管理”的弹窗,如果需要弹窗的情况下,用户点击同意权限后通过首选项对用户的操作做持久化保存。相关代码如下:

// SplashPage.ets
import data_preferences from '@ohos.data.preferences';
onConfirm() {
  let preferences = data_preferences.getPreferences(this.context, H_STORE);
  preferences.then((res) = > {
    res.put(IS_PRIVACY, true).then(() = > {
      res.flush();
      Logger.info('SplashPage','isPrivacy is put success');
    }).catch((err: Error) = > {
      Logger.info('SplashPage','isPrivacy put failed. Cause:' + err);
    });
  })
  this.jumpAdPage();
}
exitApp() {
  this.context.terminateSelf();
}
jumpAdPage() {
  setTimeout(() = > {
    router.replaceUrl({ url: 'pages/AdvertisingPage' });
  }, Const.LAUNCHER_DELAY_TIME);
}
aboutToAppear() {
  let preferences = data_preferences.getPreferences(this.context, H_STORE);
  preferences.then((res) = > {
    res.get(IS_PRIVACY, false).then((isPrivate) = > {
      if (isPrivate === true) {
        this.jumpAdPage();
      } else {
        this.dialogController.open();
      }
    });
  });
}

APP功能入口

我们需要给APP添加底部菜单栏,用于切换不同的应用模块,由于各个模块之间属于完全独立的情况,并且不需要每次切换都进行界面的刷新,所以我们用到了Tabs,TabContent组件。

本应用一共有首页(HomeIndex),成就(AchievementIndex)和我的(MineIndex)三个模块,分别对应Tabs组件的三个子组件TabContent。

// MainPage.ets
TabContent() {
  HomeIndex({ homeStore: $homeStore, editedTaskInfo: $editedTaskInfo, editedTaskID: $editedTaskID })
    .borderWidth({ bottom: 1 })
    .borderColor($r('app.color.primaryBgColor'))
}
.tabBar(this.TabBuilder(TabId.HOME))
.align(Alignment.Start)
TabContent() {
  AchievementIndex()
}
.tabBar(this.TabBuilder(TabId.ACHIEVEMENT))
TabContent() {
  MineIndex()
    .borderWidth({ bottom: 1 })
    .borderColor($r('app.color.primaryBgColor'))
}
.tabBar(this.TabBuilder(TabId.MINE))

首页

首页包含了任务信息的所有入口,包含任务列表的展示,任务的编辑和新增,上下滚动的过程中顶部导航栏的渐变,日期的切换以及随着日期切换界面任务列表跟着同步的功能,效果图如下:

%E9%A6%96%E9%A1%B5.gif
具体代码实现我们将在下边分模块进行说明:

  1. 导航栏背景渐变
    Scroll滚动的过程中,在它的onScroll方法里我们通过计算它Y轴的偏移量来改变当前界面的@State修饰的naviAlpha变量值,进而改变顶部标题的背景色,代码实现如下:
    // HomeComponent.ets
    // 视图滚动的过程中处理导航栏的透明度
    onScrollAction() {  
      this.yOffset = this.scroller.currentOffset().yOffset;  
      if (this.yOffset > Const.DEFAULT_56) {    
        this.naviAlpha = 1; 
      } else {    
        this.naviAlpha = this.yOffset / Const.DEFAULT_56;
      }
    }
    
  2. 日历组件
    日历组件主要用到的是一个横向滑动的Scroll组件。
// WeekCalendarComponent.ets
   build() {    
     Row() {      
       Column() {        
         Row() {...}             
         Scroll(this.scroller) {          
           Row() {            
             ForEach(this.homeStore.dateArr, (item: WeekDateModel, index?: number) = > {              
               Column() {                
                 Text(item.weekTitle)                  
                   .fontColor(sameDate(item.date, this.homeStore.showDate) ? $r('app.color.blueColor') : $r('app.color.titleColor'))                                 
                 Divider()
                   .color(sameDate(item.date, this.homeStore.showDate) ? $r('app.color.blueColor') : $r('app.color.white'))                
                 Image(this.getProgressImg(item))                               
               } 
               .onClick(() = > WeekCalendarMethods.calenderItemClickAction(item, index, this.homeStore))            
             })          
           }       
         }
         ...               
         .onScrollEdge((event) = > this.onScrollEdgeAction(event))      
       }
       ...       
     }
     ...    
   }

手动滑动页面时,我们通过在onScrollEnd方法里计算Scroll的偏移量来实现分页的效果,同时Scroll有提供scrollPage()方法可供我们点击左右按钮的时候来进行页面切换。

// WeekCalendarComponent.ets
import display from '@ohos.display';
...
// scroll滚动停止时通过判断偏移量进行分页处理
onScrollEndAction() {
  if (this.isPageScroll === false) {
    let page = Math.round(this.scroller.currentOffset().xOffset / this.scrollWidth);
    page = (this.isLoadMore === true) ? page + 1 : page;
    if (this.scroller.currentOffset().xOffset % this.scrollWidth != 0 || this.isLoadMore === true) {
      let xOffset = page * this.scrollWidth;
      this.scroller.scrollTo({ xOffset, yOffset: 0 } as ScrollTo);
      this.isLoadMore = false;
    }
    this.currentPage = this.homeStore.dateArr.length / Const.WEEK_DAY_NUM - page - 1;
    Logger.info('HomeIndex', 'onScrollEnd: page ' + page + ', listLength ' + this.homeStore.dateArr.length);
    let dayModel: WeekDateModel = this.homeStore.dateArr[Const.WEEK_DAY_NUM * page+this.homeStore.selectedDay];
    Logger.info('HomeIndex', 'currentItem: ' + JSON.stringify(dayModel) + ', selectedDay  ' + this.homeStore.selectedDay);
    this.homeStore!.setSelectedShowDate(dayModel!.date!.getTime());
  }
  this.isPageScroll = false;
}

我们在需要在Scroll滑动到左边边缘的时候去请求更多的历史数据以便Scroll能一直滑动,通过Scroll的onScrollEdge方法我们可以判断它是否已滑到边缘位置。

// WeekCalendarComponent.ets
onScrollEdgeAction(side: Edge) {
  if (side === Edge.Top && this.isPageScroll === false) {
    Logger.info('HomeIndex', 'onScrollEdge: currentPage ' + this.currentPage);
    if ((this.currentPage + 2) * Const.WEEK_DAY_NUM >= this.homeStore.dateArr.length) {
      Logger.info('HomeIndex', 'onScrollEdge: load more data');
      let date: Date = new Date(this.homeStore.showDate);
      date.setDate(date.getDate() - Const.WEEK_DAY_NUM);
      this.homeStore.getPreWeekData(date, () = > {});
      this.isLoadMore = true;
    }
  }
}

homeStore主要是请求数据库的数据并对数据进行处理进而渲染到界面上。

// HomeViewModel.ets
public getPreWeekData(date: Date, callback: Function) {
  let weekCalendarInfo: WeekCalendarInfo = getPreviousWeek(date);
    
  // 请求数据库数据
  DayInfoApi.queryList(weekCalendarInfo.strArr, (res: DayInfo[]) = > {

    // 数据处理
    ...  
    this.dateArr = weekCalendarInfo.arr.concat(...this.dateArr);
  })
}

同时我们还需要知道怎么根据当天的日期计算出本周内的所有日期数据。

// WeekCalendarModel.ets
export function getPreviousWeek(showDate: Date): WeekCalendarInfo {
  Logger.debug('WeekCalendarModel', 'get week date by date: ' + showDate.toDateString());
  let weekCalendarInfo: WeekCalendarInfo = new WeekCalendarInfo();
  let arr: Array< WeekDateModel > = [];
  let strArr: Array< string > = [];
  let currentDay = showDate.getDay() - 1;
  // 由于date的getDay()方法返回的是0-6代表周日到周六,我们界面上展示的周一-周日为一周,所以这里要将getDay()数据偏移一天
  let currentDay = showDate.getDay() - 1;
  if (showDate.getDay() === 0) {
    currentDay = 6;
  }
  // 将日期设置为当前周第一天的数据(周一)
  showDate.setDate(showDate.getDate() - currentDay);
  for (let index = WEEK_DAY_NUM; index > 0; index--) {
    let tempDate = new Date(showDate);
    tempDate.setDate(showDate.getDate() - index);
    let dateStr = dateToStr(tempDate);
    strArr.push(dateStr);
    arr.push(new WeekDateModel(WEEK_TITLES[tempDate.getDay()], dateStr, tempDate));
  }
  Logger.debug('WeekCalendarModel', JSON.stringify(arr));
  weekCalendarInfo.arr = arr;
  weekCalendarInfo.strArr = strArr;
  return weekCalendarInfo;
}
  1. 悬浮按钮
    由于首页右下角有一个悬浮按钮,所以首页整体我们用了一个Stack组件,将右下角的悬浮按钮和顶部的title放在滚动组件层的上边。

    // HomeComponent.ets
    build() {  
      Stack() {    
        Scroll(this.scroller) {      
          Column() {     
            ...   // 上部界面组件
            Column() {          
              ForEach(this.homeStore.getTaskListOfDay(), (item: TaskInfo) = > {            
                TaskCard({
                  taskInfoStr: JSON.stringify(item),
                  clickAction: (isClick: boolean) = > this.taskItemAction(item, isClick)
                })
                ...
              }, (item: TaskInfo) = > JSON.stringify(item))} 
          }   
        }
      }
      .onScroll(() = > {
        this.onScrollAction()
      })
      // 悬浮按钮
      AddBtn({ clickAction: () = > {
        this.editTaskAction()
      } }) 
      // 顶部title 
      Row() {
        Text($r('app.string.EntryAbility_label'))
          .titleTextStyle()
          .fontSize($r('app.float.default_24'))
          .padding({ left: Const.THOUSANDTH_66 })
      }
      .width(Const.THOUSANDTH_1000)
      .height(Const.DEFAULT_56)
      .position({ x: 0, y: 0 })
      .backgroundColor(`rgba(${WHITE_COLOR_0X},${WHITE_COLOR_0X},${WHITE_COLOR_0X},${this.naviAlpha})`)
      CustomDialogView()
    } 
    .allSize() 
    .backgroundColor($r('app.color.primaryBgColor'))
    
  2. 界面跳转及传参
    首页任务列表长按时需要跳转到对应的任务编辑界面,同时点击悬浮按钮时需要跳转到任务列表页面。
    页面跳转需要在头部引入router。

    // HomeComponent.ets
    import router from '@ohos.router';
    

    任务item的点击事件代码如下

    // HomeComponent.ets
    taskItemAction(item: TaskInfo, isClick: boolean): void {
      if (!this.homeStore.checkCurrentDay()) {
        return;
      }
      if (isClick) {  
        // 点击任务打卡    
        let callback: CustomDialogCallback = { confirmCallback: (taskTemp: TaskInfo) = > {
          this.onConfirm(taskTemp)
        }, cancelCallback: () = > {
        } };
        this.broadCast.emit(BroadCastType.SHOW_TASK_DETAIL_DIALOG, [item, callback]); 
      } else {   
        // 长按编辑任务    
        let editTaskStr: string = JSON.stringify(TaskMapById[item.taskID - 1]);
        let editTask: ITaskItem = JSON.parse(editTaskStr);
        ...
        router.pushUrl({ url: 'pages/TaskEditPage', params: { params: JSON.stringify(editTask) } });
      }
    }
    

任务创建与编辑

本节将介绍如何创建和编辑健康生活任务。

功能概述

用户点击悬浮按钮进入任务列表页,点击任务列表可进入对应任务编辑的页面中,对任务进行详细的设置,之后点击完成按钮编辑任务后将返回首页。实现效果如下图:

任务列表与编辑任务

这里主要为大家介绍添加任务列表页的实现、任务编辑的实现、以及具体弹窗设置和编辑完成功能的逻辑实现。

任务列表页

任务列表页由包括上部分的标题、返回按钮以及正中间的任务列表组成。实现效果如图:

使用Navigation以及List组件构成元素,ForEach遍历生成具体列表。这里是Navigation构成页面导航:

// TaskListPage.ets
Navigation() {
  Column() {
    // 页面中间的列表
    TaskList() 
  }
  .width(Const.THOUSANDTH_1000)
  .justifyContent(FlexAlign.Center)
}
.size({ width: Const.THOUSANDTH_1000, height: Const.THOUSANDTH_1000 })
.title(Const.ADD_TASK_TITLE)
.titleMode(NavigationTitleMode.Mini)

列表右侧有一个判断是否开启的文字标识,点击某个列表需要跳转到对应的任务编辑页里。具体的列表实现如下:

// TaskListComponent.ets

@Component
export default struct TaskList {
  ...
  build() {
    List({ space: Const.LIST_ITEM_SPACE }) {
      ForEach(this.taskList, (item: ITaskItem) = > {
        ListItem() {
          Row() {
            Row() {
              Image(item?.icon)
                ...
              Text(item?.taskName).fontSize(Const.DEFAULT_20).fontColor($r('app.color.titleColor'))
            }.width(Const.THOUSANDTH_500)

            Blank()
              ...
			// 状态改变
            if (item?.isOpen) {
              Text($r('app.string.already_open'))
                ...
            }
            Image($r('app.media.ic_right_grey'))
              ...
          }
          ...
        }
        ...
        .onClick(() = > {
          router.pushUrl({
            url: 'pages/TaskEditPage',
            params: {
              params: formatParams(item),
            }
          })
        })
        ...
      }, (item: ITaskItem) = > JSON.stringify(item))
    }
  ...
  }
}

任务编辑页

任务编辑页由上方的“编辑任务”标题以及返回按钮,主体内容的List配置项和下方的完成按钮组成,实现效果如图:

由于每一个配置项功能不相同,且逻辑复杂,故将其拆分为五个独立的组件。

这是任务编辑页面,由Navigation和一个自定义组件TaskDetail构成:

// TaskEditPage.ets
Navigation() {
  Column() {
    TaskDetail()
  }
  .width(Const.THOUSANDTH_1000)
  .height(Const.THOUSANDTH_1000)
}
.size({ width: Const.THOUSANDTH_1000, height: Const.THOUSANDTH_1000 })
.title(Const.EDIT_TASK_TITLE)
.titleMode(NavigationTitleMode.Mini)

自定义组件由List以及其子组件ListItem构成:

// TaskDetailComponent.ets
List({ space: Const.LIST_ITEM_SPACE }) {
  ListItem() {
    TaskChooseItem()
  }
  ...
  ListItem() {
    TargetSetItem()
  }
  ...
  ListItem() {
    OpenRemindItem()
  }
  ...
  ListItem() {
    RemindTimeItem()
  }
  ...
  ListItem() {
    FrequencyItem()
  }
  ...
}
.width(Const.THOUSANDTH_940)

其中做了禁用判断,需要任务打开才可以点击编辑:

// TaskDetailComponent.ets
.enabled(
  this.settingParams?.isOpen
)

一些特殊情况的禁用,如每日微笑、每日刷牙的目标设置不可编辑:

// TaskDetailComponent.ets
.enabled(
  this.settingParams?.isOpen
    && this.settingParams?.taskID !== taskType.smile
    && this.settingParams?.taskID !== taskType.brushTeeth
)

提醒时间在开启提醒打开之后才可以编辑:

// TaskDetailComponent.ets
.enabled(this.settingParams?.isOpen && this.settingParams?.isAlarm)

设置完成之后,点击完成按钮,会向数据库更新现在进行改变的状态信息,并执行之后的逻辑判断:

// TaskDetailComponent.ets
addTask(taskInfo, context).then((res: number) = > {
  GlobalContext.getContext().setObject('taskListChange', true);
  // 成功的状态,成功后跳转首页
  router.back({
    url: 'pages/MainPage', 
    params: {
      editTask: this.backIndexParams(),
    }
  })
  Logger.info('addTaskFinished', JSON.stringify(res));
}).catch((error: Error) = > {
  // 失败的状态,失败后弹出提示,并打印错误日志
  prompt.showToast({
    message: Const.SETTING_FINISH_FAILED_MESSAGE
  })
  Logger.error('addTaskFailed', JSON.stringify(error));
})

任务编辑弹窗

弹窗由封装的自定义组件CustomDialogView注册事件,并在点击对应的编辑项时进行触发,从而打开弹窗。

CustomDialogView引入实例并注册事件:

// TaskDialogView.ets
targetSettingDialog: CustomDialogController = new CustomDialogController({ 
  builder: TargetSettingDialog(),
  autoCancel: true,
  alignment: DialogAlignment.Bottom,
  offset: { dx: Const.ZERO, dy: Const.MINUS_20 }
});
...

// 注册事件
this.broadCast.on(BroadCastType.SHOW_TARGET_SETTING_DIALOG, () = > {
  this.targetSettingDialog.open();
})

点击对应的编辑项进行触发:

// TaskDetailComponent.ets
.onClick(() = > {
  this.broadCast.emit(
    BroadCastType.SHOW_TARGET_SETTING_DIALOG);
})

自定义弹窗的实现:

任务目标设置的弹窗较为特殊,故单独拿出来说明。

因为任务目标设置有三种类型:

  • 早睡早起的时间
  • 喝水的量度
  • 吃苹果的个数

如下图所示:

故根据任务的ID进行区分,将同一弹窗复用:

// TaskSettingDialog.ets
if ([taskType.getup, taskType.sleepEarly].indexOf(this.settingParams?.taskID) > Const.HAS_NO_INDEX) {
  TimePicker({
    selected: new Date(`${new Date().toDateString()} 8:00:00`),
  })
    .height(Const.THOUSANDTH_800)
    .useMilitaryTime(true)
    .onChange((value: TimePickerResult) = > {
      this.currentTime = formatTime(value);
    })
} else {
  TextPicker({ range: this.settingParams?.taskID === taskType.drinkWater ? this.drinkRange : this.appleRange })
    .width(Const.THOUSANDTH_900,)
    .height(Const.THOUSANDTH_800,)
    .onChange((value) = > {
      this.currentValue = value?.split(' ')[0];
    })
}

弹窗确认的时候将修改好的值赋予该项设置,如不符合规则,将弹出提示:

// TaskSettingDialog.ets
// 校验规则
compareTime(startTime: string, endTime: string) {
  if (returnTimeStamp(this.currentTime) < returnTimeStamp(startTime) ||
    returnTimeStamp(this.currentTime) > returnTimeStamp(endTime)) {
    prompt.showToast({
      message: Const.CHOOSE_TIME_OUT_RANGE
    })
    return false;
  }
  return true;
}
// 设置修改项
setTargetValue() {
  if (this.settingParams?.taskID === taskType.getup) {
    if (!this.compareTime(Const.GET_UP_EARLY_TIME, Const.GET_UP_LATE_TIME)) {
      return;
    }
    this.settingParams.targetValue = this.currentTime;
    return;
  }
  if (this.settingParams?.taskID === taskType.sleepEarly) {
    if (!this.compareTime(Const.SLEEP_EARLY_TIME, Const.SLEEP_LATE_TIME)) {
      return;
    }
    this.settingParams.targetValue = this.currentTime;
    return;
  }
  this.settingParams.targetValue = this.currentValue;
}

其余弹窗实现基本类似,这里不再赘述。

后台代理提醒

健康生活App中提供了任务提醒功能,我们用系统提供的后台代理提醒reminderAgent接口完成相关的开发。

说明: 后台代理提醒接口需要在module.json5中申请ohos.permission.PUBLISH_AGENT_REMINDER权限,代码如下:

// module.json5
"requestPermissions": [
  {
    "name": "ohos.permission.PUBLISH_AGENT_REMINDER"
  }
]

后台代理提醒entrysrcmainetsserviceReminderAgent.ts文件中提供了发布提醒任务、查询提醒任务、删除提醒任务三个接口供任务编辑页面调用,跟随任务提醒的开关增加、更改、删除相关后台代理提醒,代码如下:开发前请熟悉鸿蒙开发指导文档:[gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md]

// ReminderAgent.ets
import reminderAgent from '@ohos.reminderAgentManager';
import notification from '@ohos.notificationManager';
import preferences from '@ohos.data.preferences';
import Logger from '../common/utils/Logger';
import { CommonConstants as Const } from '../common/constants/CommonConstants';
import ReminderInfo from '../viewmodel/ReminderInfo';
import PublishReminderInfo from '../viewmodel/PublishReminderInfo';

// 发布提醒
function publishReminder(params: PublishReminderInfo, context: Context) {
  if (!params) {
    Logger.error(Const.REMINDER_AGENT_TAG, 'publishReminder params is empty');
    return;
  }
  let notifyId: string = params.notificationId.toString();
  hasPreferencesValue(context, notifyId, (preferences: preferences.Preferences, hasValue: boolean) = > {
    if (hasValue) {
      preferences.get(notifyId, -1, (error: Error, value: preferences.ValueType) = > {
        if (typeof value !== 'number') {
          return;
        }
        if (value >= 0) {
          reminderAgent.cancelReminder(value).then(() = > {
            processReminderData(params, preferences, notifyId);
          }).catch((err: Error) = > {
            Logger.error(Const.REMINDER_AGENT_TAG, `cancelReminder err: ${err}`);
          });
        } else {
          Logger.error(Const.REMINDER_AGENT_TAG, 'preferences get value error ' + JSON.stringify(error));
        }
      });
    } else {
      processReminderData(params, preferences, notifyId);
    }
  });
}

// 取消提醒
function cancelReminder(reminderId: number, context: Context) {
  if (!reminderId) {
    Logger.error(Const.REMINDER_AGENT_TAG, 'cancelReminder reminderId is empty');
    return;
  }
  let reminder: string = reminderId.toString();
  hasPreferencesValue(context, reminder, (preferences: preferences.Preferences, hasValue: boolean) = > {
    if (!hasValue) {
      Logger.error(Const.REMINDER_AGENT_TAG, 'cancelReminder preferences value is empty');
      return;
    }
    getPreferencesValue(preferences, reminder);
  });
}

// 可通知ID
function hasNotificationId(params: number) {
  if (!params) {
    Logger.error(Const.REMINDER_AGENT_TAG, 'hasNotificationId params is undefined');
    return;
  }
  return reminderAgent.getValidReminders().then((reminders) = > {
    if (!reminders.length) {
      return false;
    }
    let notificationIdList: Array< number > = [];
    for (let i = 0; i < reminders.length; i++) {
      let notificationId = reminders[i].notificationId;
      if (notificationId) {
        notificationIdList.push(notificationId);
      }
    }
    const flag = notificationIdList.indexOf(params);
    return flag === -1 ? false : true;
  });
}

function hasPreferencesValue(context: Context, hasKey: string, callback: Function) {
  let preferencesPromise = preferences.getPreferences(context, Const.H_STORE);
  preferencesPromise.then((preferences: preferences.Preferences) = > {
    preferences.has(hasKey).then((hasValue: boolean) = > {
      callback(preferences, hasValue);
    });
  });
}

// 进程提醒数据
function processReminderData(params: PublishReminderInfo, preferences: preferences.Preferences, notifyId: string) {
  let timer = fetchData(params);
  reminderAgent.publishReminder(timer).then((reminderId: number) = > {
    putPreferencesValue(preferences, notifyId, reminderId);
  }).catch((err: Error) = > {
    Logger.error(Const.REMINDER_AGENT_TAG, `publishReminder err: ${err}`);
  });
}

// 获取数据
function fetchData(params: PublishReminderInfo): reminderAgent.ReminderRequestAlarm {
  return {
    reminderType: reminderAgent.ReminderType.REMINDER_TYPE_ALARM,
    hour: params.hour || 0,
    minute: params.minute || 0,
    daysOfWeek: params.daysOfWeek || [],
    wantAgent: {
      pkgName: Const.PACKAGE_NAME,
      abilityName: Const.ENTRY_ABILITY
    },
    title: params.title || '',
    content: params.content || '',
    notificationId: params.notificationId || -1,
    slotType: notification.SlotType.SOCIAL_COMMUNICATION
  }
}

function putPreferencesValue(preferences: preferences.Preferences, putKey: string, putValue: number) {
  preferences.put(putKey, putValue).then(() = > {
    preferences.flush();
  }).catch((error: Error) = > {
    Logger.error(Const.REMINDER_AGENT_TAG, 'preferences put value error ' + JSON.stringify(error));
  });
}

function getPreferencesValue(preferences: preferences.Preferences, getKey: string) {
  preferences.get(getKey, -1).then((value: preferences.ValueType) = > {
    if (typeof value !== 'number') {
      return;
    }
    if (value >= 0) {
      reminderAgent.cancelReminder(value).then(() = > {
        Logger.info(Const.REMINDER_AGENT_TAG, 'cancelReminder promise success');
      }).catch((err: Error) = > {
        Logger.error(Const.REMINDER_AGENT_TAG, `cancelReminder err: ${err}`);
      });
    }
  }).catch((error: Error) = > {
    Logger.error(Const.REMINDER_AGENT_TAG, 'preferences get value error ' + JSON.stringify(error));
  });
}

const reminder = {
  publishReminder: publishReminder,
  cancelReminder: cancelReminder,
  hasNotificationId: hasNotificationId
} as ReminderInfo

export default reminder;

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 鸿蒙
    +关注

    关注

    55

    文章

    1867

    浏览量

    42181
  • OpenHarmony
    +关注

    关注

    24

    文章

    3442

    浏览量

    15250
收藏 人收藏

    评论

    相关推荐

    HarmonyOS开发案例:【生活健康app之实现打卡功能】(2)

    首页会展示当前用户已经开启的任务列表,每条任务会显示对应的任务名称以及任务目标、当前任务完成情况。用户只可对当天任务进行打卡操作,用户可以根据需要对任务列表中相应的任务进行点击打卡。如果任务列表中的每个任务都在当天完成则为连续打卡一天,连续打卡多天会获得成就徽章。
    的头像 发表于 05-10 15:29 274次阅读
    <b class='flag-5'>HarmonyOS</b><b class='flag-5'>开发案</b>例:【<b class='flag-5'>生活</b><b class='flag-5'>健康</b><b class='flag-5'>app</b>之实现打卡功能】(2)

    HarmonyOS开发案例:【生活健康app之编写通用工具类】(5)

    本节将介绍日志打印、时间换算等通用工具类的编写和使用,工具类可以简化应用代码编写和业务流程处理。
    的头像 发表于 05-11 09:58 357次阅读
    <b class='flag-5'>HarmonyOS</b><b class='flag-5'>开发案</b>例:【<b class='flag-5'>生活</b><b class='flag-5'>健康</b><b class='flag-5'>app</b>之编写通用工具类】(5)

    HarmonyOS IoT 硬件开发案例分享

    ``许思维老师HiSpark Wi-Fi IoT 开发案例分享:案例一:AHT20温湿度传感器开发、调试;案例二:oled屏驱动库移植,调试;案例三:用OLED屏播放视频,Wi-Fi 和 TCP/IP 综合应用。 ``
    发表于 10-27 17:30

    HarmonyOS开发跨设备的鸿蒙(HarmonyOSApp

    是圆形(如智能手表),这就给开发App带来了麻烦。现在几乎每一个智能设备厂商,如Apple、华为都面临这个问题。这就要求我们开发App尽可能适合更多的智能设备。当然,最简单,最直接的
    发表于 11-02 15:18

    如何优雅地开发HarmonyOS APP应用

    ` 本帖最后由 软通动力HOS 于 2021-3-10 15:29 编辑 研究HarmonyOS有一段时间了,今天主要结合自己多年的项目开发经验和各种技术栈结合HarmonyOS APP
    发表于 03-10 15:13

    harmonyOS开发APP如何访问Webservice?

    我接到一个项目,需要用到HarmonyOS开发APP做为移动手机查询和收到报警数据,具体是这样的,在C/S加B/S的系统框架下我们有数据库服务器和Web服务器,有widows桌面应用和Web浏览器
    发表于 03-28 10:14

    鸿蒙智联生态产品《接入智慧生活App开发指导》(官方更新版)

    HarmonyOS Connect生态产品应用开发过程中,很多开发者对于如何接入智慧生活App还存在一些疑问,如:如何选择合适的
    发表于 04-26 15:00

    HarmonyOS APP打包运行和调试应用开发步骤

    在进行HarmonyOS应用开发前,您应该掌握HarmonyOS应用的逻辑结构。HarmonyOS应用发布形态为APP Pack(Appli
    发表于 05-24 14:27

    4天带你上手HarmonyOS ArkUI开发——《HarmonyOS ArkUI入门训练营之健康生活实战》

    HarmonyOS ArkUI入门训练营之健康饮食应用》是面向入门开发者打造的实战课程系列。特邀华为终端BG高级开发工程师作为本次训练营讲师,以
    发表于 01-05 11:49

    App开发案例教程PDF电子书免费下载

    App开发案例教程》通过一个实例,介绍App设计、开发直至上线的全过程,引导读者在较短时间内熟悉一个较大规模的App应用系统的
    发表于 08-05 08:00 38次下载
    <b class='flag-5'>App</b><b class='flag-5'>开发案</b>例教程PDF电子书免费下载

    许思维老师HarmonyOS IoT硬件开发案例分享

    许思维老师HiSpark Wi-Fi IoT 开发案例分享:案例一:AHT20温湿度传感器开发、调试;案例二:oled屏驱动库移植,调试;案例三:用OLED屏播放视频,Wi-Fi 和 TCP/IP 综合应用。
    发表于 10-29 10:39 39次下载
    许思维老师<b class='flag-5'>HarmonyOS</b> IoT硬件<b class='flag-5'>开发案</b>例分享

    华为开发者分论坛HarmonyOS学生公开课-OpenHarmony Codelabs开发案

    2021华为开发者分论坛HarmonyOS学生公开课-OpenHarmony Codelabs开发案
    的头像 发表于 10-24 11:25 1652次阅读
    华为<b class='flag-5'>开发</b>者分论坛<b class='flag-5'>HarmonyOS</b>学生公开课-OpenHarmony Codelabs<b class='flag-5'>开发案</b>例

    如何接入智慧生活App

     在HarmonyOS Connect生态产品应用开发过程中,很多开发者对于如何接入智慧生活App还存在一些疑问,如:如何选择合适的
    的头像 发表于 04-25 10:42 3180次阅读
    如何接入智慧<b class='flag-5'>生活</b><b class='flag-5'>App</b>

    HarmonyOS开发案例:【app初始启动

    基于自定义弹框、首选项和页面路由实现一个模拟应用首次启动的案例。
    的头像 发表于 04-18 22:04 127次阅读
    <b class='flag-5'>HarmonyOS</b><b class='flag-5'>开发案</b>例:【<b class='flag-5'>app</b>初始<b class='flag-5'>启动</b>】

    HarmonyOS开发案例:【生活健康app之获取成就】(3)

    成就页面展示用户可以获取的所有勋章,当用户满足一定的条件时,将点亮本页面对应的勋章,没有得到的成就勋章处于熄灭状态。共有六种勋章,当用户连续完成任务打卡3天、7天、30天、50天、73天、99天时,可以获得对应的“连续xx天达成”勋章。
    的头像 发表于 05-10 17:18 160次阅读
    <b class='flag-5'>HarmonyOS</b><b class='flag-5'>开发案</b>例:【<b class='flag-5'>生活</b><b class='flag-5'>健康</b><b class='flag-5'>app</b>之获取成就】(3)