赞
踩
本篇Codelab介绍了如何实现一个简单的健康生活应用,主要功能包括:
用户可通过长按添加2x2或2x4卡片查看任务完成情况,具体ArkTS卡片实现可以参考文档:健康生活卡片(ArkTS)。
本篇Codelab用到了任务提醒功能,需要在配置文件module.json5里添加后台代理提醒权限:ohos.permission.PUBLISH_AGENT_REMINDER。
源码下载
我们首先需要完成HarmonyOS开发环境搭建,可参照如图步骤进行。
本篇Codelab只对核心代码进行讲解,对于完整代码,我们会在源码下载或gitee中提供。
- ├──entry/src/main/ets // 代码区
- │ ├──agency // 2x4 ArkTS卡片目录
- │ │ └──pages
- │ │ └──AgencyCard.ets // 2x4 ArkTS卡片任务
- │ ├──common
- │ │ ├──constants
- │ │ │ └──CommonConstants.ets // 公共常量
- │ │ ├──database
- │ │ │ ├──rdb // 数据库封装类
- │ │ │ │ ├──RdbHelper.ets
- │ │ │ │ ├──RdbHelperImp.ets
- │ │ │ │ ├──RdbUtils.ets
- │ │ │ │ └──TableHelper.ets
- │ │ │ └──tables // 数据表
- │ │ │ ├──DayInfoApi.ets
- │ │ │ ├──FormInfoApi.ets
- │ │ │ ├──GlobalInfoApi.ets
- │ │ │ └──TaskInfoApi.ets
- │ │ └──utils
- │ │ ├──BroadCast.ets // 通知
- │ │ ├──FormUtils.ets // 卡片操作工具类
- │ │ ├──GlobalContext.ets
- │ │ ├──HealthDataSrcMgr.ets // 数据管理单例
- │ │ ├──Logger.ets // 日志类
- │ │ └──Utils.ets // 工具类
- │ ├──entryability
- │ │ └──EntryAbility.ets // 程序入口类
- │ ├──entryformability
- │ │ └──EntryFormAbility.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 // 任务列表页面
- │ ├──progress // 2x2 ArkTS卡片目录
- │ │ └──pages
- │ │ └──ProgressCard.ets // 2x2 ArkTS卡片任务进度
- │ ├──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 // 成就勋章信息接口
- │ ├──AchievementViewModel.ets // 成就相关模块
- │ ├──AgencyCardInfo.ets // 2x4卡片信息接口
- │ ├──BroadCastCallBackInfo.ets // 回调相关接口
- │ ├──CalendarViewModel.ets // 日历相关模块
- │ ├──CardInfo.ets // 卡片信息接口
- │ ├──ColumnInfo.ets // 数据表信息接口
- │ ├──CommonConstantsInfo.ets // 配置信息接口
- │ ├──DayInfo.ets // 每日信息接口
- │ ├──FormInfo.ets // 卡片信息接口
- │ ├──GlobalInfo.ets // 全局信息接口
- │ ├──HomeViewModel.ets // 首页相关模块
- │ ├──ProgressCardInfo.ets // 2x2卡片信息接口
- │ ├──PublishReminderInfo.ets // 提醒信息接口
- │ ├──ReminderInfo.ets // 提醒操作接口
- │ ├──TaskInfo.ets // 任务信息接口
- │ ├──TaskViewModel.ets // 任务设置相关模块
- │ ├──WeekCalendarInfo.ets // 日期信息接口
- │ └──WeekCalendarMethodInfo.ets // 日期操作接口
- └──entry/src/main/resources // 资源文件目录
本应用的基本架构如右图所示,数据库为其他服务提供基础的用户数据,主要业务包括:用户可以查看和编辑自己的健康任务并进行打卡、查看成就。UI层提供了承载上述业务的UI界面。
本节给应用添加一个启动页,启动页里需要用到定时器来实现启动页展示固定时间后跳转应用主页的功能。具体实现逻辑是:
- // EntryAbility.ets
- windowStage.loadContent('pages/SplashPage', (err, data) => {
- if (err.code) {...}
- Logger.info('windowStage', 'Succeeded in loading the content. Data: ' + JSON.stringify(data))
- });
-
- // SplashPage.ets
- import common from '@ohos.app.ability.common';
- import data_preferences from '@ohos.data.preferences';
-
- @Entry
- @Component
- struct SplashIndex {
- context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
-
- onConfirm() {
- let preferences = data_preferences.getPreferences(that.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();
- }
- });
- });
- }
-
- build() {
- ...
- }
- }
我们需要给APP添加底部菜单栏,用于切换不同的应用模块,由于各个模块之间属于完全独立的情况,并且不需要每次切换都进行界面的刷新,所以我们用到了Tabs,TabContent组件。
本应用一共有首页(HomeIndex),成就(AchievementIndex)和我的(MineIndex)三个模块,分别对应Tabs组件的三个子组件TabContent。
- // MainPage.ets
- Tabs({ barPosition: BarPosition.End, controller: this.tabController }) {
- 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()
- }
- .tabBar(this.TabBuilder(TabId.MINE))
- }
首页包含了任务信息的所有入口,包含任务列表的展示,任务的编辑和新增,上下滚动的过程中顶部导航栏的渐变,日期的切换以及随着日期切换界面任务列表跟着同步的功能。具体代码实现我们将在下边分模块进行说明。
Scroll滚动的过程中,在它的onScrollAction()方法里我们通过计算它Y轴的偏移量来改变当前界面的@State修饰的naviAlpha变量值,进而改变顶部标题的背景色。
- // HomeComponent.ets
- // 视图滚动的过程中处理导航栏的透明度
- @State naviAlpha: number = 0;
- ...
- onScrollAction() {
- this.yOffset = this.scroller.currentOffset().yOffset;
- if (this.yOffset > Const.DEFAULT_56) {
- this.naviAlpha = 1;
- } else {
- this.naviAlpha = this.yOffset / Const.DEFAULT_56;
- }
- }
日历组件主要用到的是一个横向滑动的Scroll组件。手动滑动页面时,通过在onScrollEndAction()方法里计算Scroll的偏移量来实现分页的效果,同时Scroll有提供scrollPage()方法可供我们点击左右按钮的时候来进行页面切换。需要在Scroll滑动到左边边缘的时候去请求更多的历史数据以便Scroll能一直滑动,通过Scroll的onScrollEdge方法可以判断它是否已滑到边缘位置。homeStore主要是请求数据库的数据并对数据进行处理进而渲染到界面上。同时还需要知道怎么根据当天的日期计算出本周内的所有日期数据。
- // 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))
- })
- }
- }
- .onScrollStop(() => this.onScrollEndAction())
- .onScrollEdge((event) => this.onScrollEdgeAction(event))
- }
- }
- }
- }
-
- // WeekCalendarComponent.ets
- // scroll滚动停止时通过判断偏移量进行分页处理
- onScrollEndAction() {
- // 区分是否是手动滑动,点击左右箭头按钮导致Scroll滑动时不作处理,不然会引起死循环
- 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;
- let dayModel: WeekDateModel = this.homeStore.dateArr[Const.WEEK_DAY_NUM * page + this.homeStore.selectedDay];
- this.homeStore!.setSelectedShowDate(dayModel!.date!.getTime());
- }
- this.isPageScroll = false;
- }
-
- // 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);
- that.homeStore.getPreWeekData(date, () => {});
- this.isLoadMore = true;
- }
- }
- }
-
- // 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 {
- let weekCalendarInfo: WeekCalendarInfo = new WeekCalendarInfo();
- let arr: Array<WeekDateModel> = [];
- let strArr: Array<string> = [];
-
- // 由于date的getDay()方法返回的是0-6代表周日到周六,我们界面上展示的周一-周日为一周,所以这里要将getDay()数据偏移一天
- let currentDay = showDate.getDay() - 1;
-
- // 将日期设置为当前周第一天的数据(周一)
- 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));
- }
- weekCalendarInfo.arr = arr;
- weekCalendarInfo.strArr = strArr;
- return weekCalendarInfo;
- }
由于首页右下角有一个悬浮按钮,所以首页整体我们用了一个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'))
- }
- .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'))
- }
首页任务列表长按时需要跳转到对应的任务编辑界面,同时点击悬浮按钮时需要跳转到任务列表页面。页面跳转需要在头部引入router。
- // HomeComponent.ets
- import router from '@ohos.router';
-
- 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);
- editTask.targetValue = item?.targetValue;
- editTask.isAlarm = item.isAlarm;
- editTask.startTime = item.startTime;
- editTask.frequency = item.frequency;
- editTask.isOpen = item.isOpen;
- 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
- List({ space: Const.LIST_ITEM_SPACE }) {
- ForEach(this.taskList, (item: ITaskItem) => {
- ListItem() {
- Row() {
- Row() {
- Image(item?.icon)
- Text(item?.taskName)
- ...
- }
- .width(Const.THOUSANDTH_500)
-
- Blank()
- .layoutWeight(1)
-
- // 状态显示
- if (item?.isOpen) {
- Text($r('app.string.already_open'))
- }
- Image($r('app.media.ic_right_grey'))
- .width(Const.DEFAULT_8)
- .height(Const.DEFAULT_16)
-
- }
- ...
- }
- ...
-
- // 路由跳转到任务编辑页
- .onClick(() => {
- router.pushUrl({
- url: 'pages/TaskEditPage',
- params: {
- params: formatParams(item)
- }
- })
- })
- ...
- })
- }
任务编辑页由上方的“编辑任务”标题以及返回按钮,主体内容的List配置项和下方的完成按钮组成。任务编辑页面,由Navigation和一个自定义组件TaskDetail构成。自定义组件由List以及其子组件ListItem构成,实现效果如图所示:
- // 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)
-
- // TaskDetailComponent.ets
- List({ space: Const.LIST_ITEM_SPACE }) {
- ListItem() {
- TaskChooseItem()
- }
- .listItemStyle()
-
- ListItem() {
- TargetSetItem()
- }
- .listItemStyle()
- // 一些特殊情况的禁用,如每日微笑、每日刷牙的目标设置不可编辑
- .enabled(
- this.settingParams?.isOpen
- && this.settingParams?.taskID !== taskType.smile
- && this.settingParams?.taskID !== taskType.brushTeeth
- )
- .onClick(() => {
- this.broadCast.emit(
- BroadCastType.SHOW_TARGET_SETTING_DIALOG);
- })
-
- ListItem() {
- OpenRemindItem()
- }
- .listItemStyle()
- // 其中做了禁用判断,需要任务打开才可以点击编辑
- .enabled(this.settingParams?.isOpen)
-
- ListItem() {
- RemindTimeItem()
- }
- .listItemStyle()
- // 提醒时间在开启提醒打开之后才可以编辑
- .enabled(this.settingParams?.isOpen && this.settingParams?.isAlarm)
- .onClick(() => {
- this.broadCast.emit(BroadCastType.SHOW_REMIND_TIME_DIALOG);
- })
-
- ListItem() {
- FrequencyItem()
- }
- .listItemStyle()
- .enabled(this.settingParams?.isOpen && this.settingParams?.isAlarm)
- .onClick(() => {
- this.broadCast.emit(BroadCastType.SHOW_FREQUENCY_DIALOG);
- })
- }
- .width(Const.THOUSANDTH_940)
-
- // TaskDetailComponent.ets
- addTask({
- // 相关参数
- ...
- })
- .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组件内注册打开弹窗的事件,当点击对应任务的编辑项时触发该事件,进而打开弹窗。
- // 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_TARGETSETTING_DIALOG, () => {
- this.targetSettingDialog.open();
- })
-
- // HomeComponent.ets
- taskItemAction(item: TaskInfo, isClick: boolean): void {
- ...
- if (isClick) {
- let callback: CustomDialogCallback ={ confirmCallback: (taskTemp: TaskInfo) => { this.onConfirm(taskTemp) }, cancelCallback: () => {} };
- this.broadCast.emit(BroadCastType.SHOW_TASK_DETAIL_DIALOG, [item, callback]);
- } else {
- ...
- }
- }
任务目标设置有三种类型,早睡早起的时间、喝水的量度、吃苹果的个数。故根据任务的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`),
- })
- ...
- } else {
- TextPicker({
- range: this.settingParams?.taskID === taskType.drinkWater ?
- this.drinkRange :
- this.appleRange
- })
- ...
- }
-
- // TaskSettingDialog.ets
- // 校验规则
- compareTime(startTime: string, endTime: string) {
- if (returnTimeStamp(this.currentTime) < returnTimeStamp(startTime) ||
- returnTimeStamp(this.currentTime) > returnTimeStamp(endTime)) {
- // 弹出提示
- prompt.showToast({
- message: commonConst.CHOOSE_TIME_OUT_RANGE
- })
- return false;
- }
- return true;
- }
-
- // 设置修改项
- setTargetValue() {
- ...
- if (this.settingParams?.taskID === taskType.sleepEarly) {
- if (!this.compareTime(commonConst.SLEEP_EARLY_TIME, commonConst.SLEEP_LATE_TIME)) {
- return;
- }
- this.settingParams.targetValue = this.currentTime;
- return;
- }
- this.settingParams.targetValue = this.currentValue;
- }
首页会展示当前用户已经开启的任务列表,每条任务会显示对应的任务名称以及任务目标、当前任务完成情况。用户只可对当天任务进行打卡操作,用户可以根据需要对任务列表中相应的任务进行点击打卡。如果任务列表中的每个任务都在当天完成则为连续打卡一天,连续打卡多天会获得成就徽章。打卡效果如图所示:
使用List组件展示用户当前已经开启的任务,每条任务对应一个TaskCard组件,clickAction包装了点击和长按事件,用户点击任务卡时会触发弹起打卡弹窗,从而进行打卡操作;长按任务卡时会跳转至任务编辑界面,对相应的任务进行编辑处理。
- // HomeComponent.ets
- Column({ space: Const.DEFAULT_8 }) {
- ForEach(this.homeStore.getTaskListOfDay(), (item: TaskInfo) => {
- TaskCard({
- taskInfoStr: JSON.stringify(item),
- clickAction: (isClick: boolean) => this.taskItemAction(item, isClick)
- })
- }, (item: TaskInfo) => JSON.stringify(item))
- }
- ...
- CustomDialogView() // 自定义弹窗中间件
在组件CustomDialogView的aboutToAppear生命周期中注册SHOW_TASK_DETAIL_DIALOG的事件回调方法 ,当通过emit触发此事件时即触发回调方法执行。
- // CustomDialogView.ets
- @Component
- export struct CustomDialogView {
- @Consume broadCast: BroadCast;
- @Provide currentTask: TaskInfo = TaskItem;
- @Provide dialogCallBack: CustomDialogCallback = new CustomDialogCallback();
-
- // 任务打卡弹窗
- taskDialog: CustomDialogController = new CustomDialogController({
- builder: TaskDetailDialog(),
- autoCancel: true,
- customStyle: true
- });
-
- aboutToAppear() {
- // 任务打卡弹窗 注册 “SHOW_TASK_DETAIL_DIALOG” 事件回调
- this.broadCast.on(BroadCastType.SHOW_TASK_DETAIL_DIALOG,
- (currentTask: TaskInfo, dialogCallBack: CustomDialogCallback) => {
- // 接收当前任务参数 以Provide Consume方式向子组件透传
- this.currentTask = currentTask || TaskItem;
- // 接收当前任务确认打卡回调 以Provide Consume方式向子组件透传
- this.dialogCallBack = dialogCallBack;
- this.taskDialog.open();
- });
- }
- ...
- }
点击任务卡片会emit触发 “SHOW_TASK_DETAIL_DIALOG” 事件,同时把当前任务,以及确认打卡回调方法传递下去。
- // 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 {
- // 长按编辑任务
- ...
- }
-
- // 确认打卡
- onConfirm(task: TaskInfo) {
- this.homeStore.taskClock(task).then((res: AchievementInfo) => {
- // 打卡成功后,根据连续打卡情况判断是否弹出成就勋章以及成就勋章级别
- if (res.showAchievement) {
- let achievementLevel = res.achievementLevel;
- // 触发弹出成就勋章SHOW_ACHIEVEMENT_DIALOG 事件, 并透传勋章类型级别
- if (achievementLevel) {
- this.broadCast.emit(BroadCastType.SHOW_ACHIEVEMENT_DIALOG, achievementLevel);
- } else {
- this.broadCast.emit(BroadCastType.SHOW_ACHIEVEMENT_DIALOG);
- }
- }
- })
- }
弹窗组件由两个组件构成(TaskBaseInfo、TaskClock)会根据当前任务的ID获取任务名称以及弹窗背景图片资源。
- // TaskDetailDialog.ets
- Column() {
- // 展示任务的基本信息
- TaskBaseInfo({
- // 根据当前任务ID获取任务名称
- taskName: TaskMapById[this.currentTask?.taskID - 1].taskName
- });
- // 打卡功能组件(任务打卡、关闭弹窗)
- TaskClock({
- confirm: () => {
- // 任务打卡确认回调执行
- this.dialogCallBack.confirmCallback(this.currentTask);
- this.controller.close();
- },
- cancel: () => {
- this.controller.close();
- },
- showButton: this.showButton
- })
- }
- ...
-
- // TaskDetailDialog.ets
- @Component
- struct TaskBaseInfo {
- taskName: string | Resource = '';
- build() {
- Column({ space: Const.DEFAULT_8 }) {
- Text(this.taskName)
- ...
- }
- ...
- }
- }
-
- // TaskDetailDialog.ets
- @Component
- struct TaskClock {
- confirm: Function = () => {};
- cancel: Function = () => {};
- showButton: boolean = false;
- build() {
- Column({ space: Const.DEFAULT_12 }) {
- Button() {
- Text($r('app.string.clock_in')) // 打卡
- ...
- }
- ...
- .onClick(() => {
- GlobalContext.getContext().setObject('taskListChange', true);
- this.confirm();
- })
- .visibility(!this.showButton ? Visibility.None : Visibility.Visible)
- Text($r('app.string.got_it')) // 知道了
- ...
- .onClick(() => {
- this.cancel();
- })
- }
- }
- }
打卡成功后同步更新当天任务完成情况数据,以及判断累计打卡天数是否满足获得勋章条件,满足条件会弹出对应勋章,以及在成就页面查看已获得的勋章。
- // HomeViewModel.ets
- public async taskClock(taskInfo: TaskInfo) {
- let taskItem = await this.updateTask(taskInfo);
- let dateStr = this.selectedDayInfo?.dateStr;
- // 更新任务失败
- if (!taskItem) {
- return {
- achievementLevel: 0,
- showAchievement: false
- } as AchievementInfo;
- }
- // 更新当前时间的任务列表
- this.selectedDayInfo.taskList = this.selectedDayInfo.taskList.map((item) => {
- return item.taskID === taskItem?.taskID ? taskItem : item;
- });
- let achievementLevel: number = 0;
- if(taskItem.isDone) {
- // 更新每日任务完成情况数据
- let dayInfo = await this.updateDayInfo();
- // 当日任务完成数量等于总任务数量时 累计连续打卡一天
- if (dayInfo && dayInfo?.finTaskNum === dayInfo?.targetTaskNum) {
- // 更新成就勋章数据 判断是否弹出获得勋章弹出及勋章类型
- achievementLevel = await this.updateAchievement(this.selectedDayInfo.dayInfo);
- }
- }
- ...
- return {
- achievementLevel: achievementLevel,
- showAchievement: ACHIEVEMENT_LEVEL_LIST.includes(achievementLevel)
- } as AchievementInfo;
- }
-
- // 更新当天任务列表
- updateTask(task: TaskInfo): Promise<TaskInfo> {
- return new Promise((resolve, reject) => {
- let taskID = task.taskID;
- let targetValue = task.targetValue;
- let finValue = task.finValue;
- let updateTask = new TaskInfo(task.id, task.date, taskID, targetValue, task.isAlarm, task.startTime,
- task.endTime, task.frequency, task.isDone, finValue, task.isOpen);
- // 任务步长
- let step = TaskMapById[taskID - 1].step;
- let hasExceed = updateTask.isDone;
- // 任务步长为0 打卡一次即完成该任务
- if (step === 0) {
- // 打卡一次即完成该任务
- updateTask.isDone = true;
- updateTask.finValue = targetValue;
- } else {
- // 任务步长非0 打卡一次 步长与上次打卡进度累加
- let value = Number(finValue) + step;
- // 判断任务是否完成
- updateTask.isDone = updateTask.isDone || value >= Number(targetValue);
- updateTask.finValue = updateTask.isDone ? targetValue : `${value}`;
- }
- // 更新数据库
- TaskInfoTableApi.updateDataByDate(updateTask, (res: number) => {
- if (!res || hasExceed) {
- Logger.error('taskClock-updateTask', JSON.stringify(res));
- reject(res);
- }
- resolve(updateTask);
- });
- })
- }
成就页面展示用户可以获取的所有勋章,当用户满足一定的条件时,将点亮本页面对应的勋章,没有得到的成就勋章处于熄灭状态。共有六种勋章,当用户连续完成任务打卡3天、7天、30天、50天、73天、99天时,可以获得对应的 “连续xx天达成”勋章。
标题部分TitleBar是一个横向容器Row里包含一个子组件Text。
- // TitleBarComponent.ets
- @Component
- export struct TitleBar {
- build() {
- Row() {
- Text($r('app.string.achievement'))
- ... // 省略属性设置
- }.width(Const.FULL_WIDTH)
- }
- }
每个勋章卡片BadgeCard是由一个竖向容器Column、一个图片子组件Image、和一个文字子组件Text组成。
- // BadgeCardComponent.ets
- @Component
- export struct BadgeCard {
- @Prop content: string = '';
- imgSrc: Resource = $r('app.string.empty');
-
- build() {
- Column({ space: Const.DEFAULT_18 }) {
- Image(this.imgSrc)
- ... // 省略属性设置
- Text($r('app.string.task_achievement_level', Number(this.content)))
- ... // 省略属性设置
- }
- }
- }
整体的勋章面板使用Flex一个组件即可以实现均分和换行的功能。
- // BadgePanelComponent.ets
- @Component
- export struct BadgePanel {
- @StorageProp(ACHIEVEMENT_LEVEL_KEY) successiveDays: number = 0;
-
- aboutToAppear() {
- getAchievementLevel();
- }
-
- build() {
- Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {
- ForEach(getBadgeCardItems(this.successiveDays), (item: CardInfo) => {
- BadgeCard({ content: item.titleContent, imgSrc: item.achievement })
- })
- }
- .width(Const.FULL_WIDTH)
- }
- }
进入界面第一次获取数据在aboutToAppear()声明周期中从数据库GlobalInfo表中获取存储的勋章数据, 通过@StorageProp装饰器刷新界面,其他的地方只要通过AppStorage更新勋章数据即可。
- // BadgePanelComponent.ets
- aboutToAppear() {
- getAchievementLevel()
- }
-
- // AchieveModel.ets
- export function getAchievementLevel() {
- GlobalInfoApi.query((res: GlobalInfo) => {
- ... // 省略数据验证
- if (achievements.length > 0) {
- AppStorage.Set<Number>(ACHIEVEMENT_LEVEL_KEY, Number(achievements[achievements.length - 1]));
- }
- })
- }
-
- // BadgePanelComponent.ets
- @StorageProp(ACHIEVEMENT_LEVEL_KEY) successiveDays: number = 0;
-
- ForEach(getBadgeCardItems(this.successiveDays), (item: CardInfo) => {
- BadgeCard({ content: item.titleContent, imgSrc: item.achievement } )
- })
-
- // AchievementViewModel.ets
- export function getBadgeCardItems(successiveDays: number): Array<CardInfo> {
- let badgeMileStones = ACHIEVEMENT_LEVEL_LIST;
- let cardItems: Array<CardInfo> = [];
- for (let i = 0; i < badgeMileStones.length; i++) {
- ... // 省略数据拼装细节
- cardItems.push(cardInfo);
- }
- return cardItems;
- }
本章节将介绍如何发布提醒任务和取消提醒任务,ReminderAgent文件提供了发布提醒任务、查询提醒任务、取消提醒任务三个接口供任务编辑页面调用。
在编辑任务页面中,开启提醒,选好提醒时间,点击保存,然后通过ReminderAgent.publishReminder方法发布提醒任务。在发布提醒任务之前,判断当前提醒任务Id是否存在,如不存在则通过reminderAgent.publishReminder发布提醒任务,否则先取消提醒任务再发布。
- // ReminderAgent.ets
- // 发布提醒任务
- 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 (haseValue) {
- ...
- } else {
- processReminderData(params, preferences, notifyId);
- }
- });
- }
-
- // 处理提醒任务数据
- 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}`);
- });
- }
在编辑任务页面中,关闭提醒,点击保存,然后通过ReminderAgent.cancelReminder方法取消当前提醒任务。在取消提醒任务之前,判断当前提醒任务Id是否存在,如果存在则通过reminderAgent.cancelReminder方法取消提醒任务,否则说明当前任务未开启提醒。
- // ReminderAgent.ets
- // 取消提醒任务
- 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);
- });
- }
-
- 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));
- });
- }
要使用关系型数据库存储用户数据,首先要进行数据库的创建,并提供基本的增、删、查、改接口。如图所示,关系型数据库提供两个基本功能:
首先要获取一个RdbStore来操作关系型数据库。
- // RdbHelperImp.ets
- getRdb(context: Context): Promise<RdbHelper> {
- this.storeConfig = {
- // 配置数据库文件名、安全级别
- name: this.mDatabaseName, securityLevel: dataRdb.SecurityLevel.S1
- };
- return new Promise<RdbHelper>((success, error) => {
- dataRdb.getRdbStore(context, this.storeConfig).then(dbStore => {
- this.rdbStore = dbStore; // 获取RdbStore
- success(this);
- }).catch((err: Error) => {
- Logger.error(`initRdb err : ${JSON.stringify(err)}`);
- error(err);
- })
- })
- }
关系型数据库接口提供的增、删、改、查操作均有callback和Promise两种异步回调方式,本Codelab使用了callback异步回调。
- // RdbHelperImp.ets
- insert(tableName: string, values: dataRdb.ValuesBucket | Array<dataRdb.ValuesBucket>): Promise<number> {
- return new Promise<number>((success, error) => {
- Logger.info(`insert tableName : ${tableName}, values : ${JSON.stringify(values)}`);
- ...
- if (values instanceof Array) { // 如果插入一组数据,则批量插入
- Logger.info(`insert values isArray = ${values.length}`);
- this.rdbStore.beginTransaction();
- this.saveArray(tableName, values).then(data => {
- Logger.info(`insert success, data : ${JSON.stringify(data)}`);
- success(data);
- this.rdbStore.commit();
- }).catch((err: Error) => {
- Logger.error(`insert failed, err : ${err}`);
- error(err);
- this.rdbStore.commit();
- })
- } else {
- this.rdbStore.insert(tableName, values).then(data => { // 调用insert()接口插入数据
- Logger.info(`insert success id : ${data}`);
- success(data);
- this.rdbStore.commit();
- }).catch((err: Error) => {
- Logger.error(`insert failed, err : ${JSON.stringify(err)}`);
- error(err);
- this.rdbStore.commit();
- })
- }
- })
- }
-
- // 删除数据使用了delete()接口,实现代码如下
- // RdbHelperImp.ets
- delete(rdbPredicates: dataRdb.RdbPredicates): Promise<number> {
- Logger.info(`delete rdbPredicates : ${JSON.stringify(rdbPredicates)}`);
- return this.rdbStore.delete(rdbPredicates);
- }
-
- // 更新数据使用了update()接口,实现代码如下
- // RdbHelperImp.ets
- update(values: dataRdb.ValuesBucket, rdbPredicates: dataRdb.RdbPredicates): Promise<number> {
- return this.rdbStore.update(values, rdbPredicates);
- }
-
- // 查找数据使用了query()接口,实现代码如下
- // RdbHelperImp.ets
- query(rdbPredicates: dataRdb.RdbPredicates, columns?: Array<string>): Promise<dataRdb.ResultSet> {
- Logger.info(`query rdbPredicates : ${JSON.stringify(rdbPredicates)}`);
- return this.rdbStore.query(rdbPredicates, columns);
- }
根据健康生活APP的使用场景和业务逻辑,定义了三个数据对象,并使用三张数据表来存储,分别是健康任务信息表、每日信息表和全局信息表。
目前健康生活应用提供了6个基本的健康任务,分别是早起、喝水、吃苹果、每日微笑、睡前刷牙和早睡。用户可以选择开启或关闭某个任务,开启的任务可以选择是否开启提醒,在指定的时间段内提醒用户进行打卡。任务也可以选择开启的频率,如只在周一到周五开启等。需要记录每项任务的目标值和实际完成值,在用户打卡后判断任务是否已经完成,并记录在数据库中。因此,需要创建一张存储每天的健康任务信息的表,表头如图所示。
在主页面,用户可以查看当天健康任务的完成进度,需要创建一张表记录当天开启的任务个数和已经完成的任务个数,表头如图所示。
用户连续多日打卡完成所有创建的任务可以获得相应的成就,因此,需要有一张表记录连续打卡天数和已达成的成就项。另外,考虑应用多日未打开的情况,需要记录应用第一次打开的日期和最后一次打开的日期以向数据库回填数据,表头如图所示。
根据上文设计的表结构,创建对应的数据表,实现对相应数据的读写操作。
在EntryAbility的onCreate方法中,通过RdbUtils.createTable方法创建相应的表结构和初始化数据。
- // EntryAbility.ets
- async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
- ...
- RdbUtils.createTable(Const.TASK_INFO.tableName ? Const.TASK_INFO.tableName : '', columnTaskInfoInfoList).then(() => {
- Logger.info(`RdbHelper createTable taskInfo success`);
- }).catch((err: Error) => {
- Logger.error(`RdbHelper taskInfo err : ${JSON.stringify(err)}`);
- });
- ...
- }
-
- // CommonConstants.ets
- static readonly TASK_INFO = {
- tableName: 'taskInfo',
- columns: [
- 'id',
- 'date',
- 'taskID',
- 'targetValue',
- 'isAlarm',
- 'startTime',
- 'endTime',
- 'frequency',
- 'isDone',
- 'finValue',
- 'isOpen'
- ]
- } as CommonConstantsInfo
-
- // RdbColumnModel.ets
- export const columnTaskInfoInfoList: Array<ColumnInfo> = [
- new ColumnInfo('id', 'integer', -1, false, true, true),
- new ColumnInfo('date', 'TEXT', -1, false, false, false),
- new ColumnInfo('taskID', 'integer', -1, false, false, false),
- new ColumnInfo('targetValue', 'text', -1, false, false, false),
- new ColumnInfo('isAlarm', 'boolean', -1, false, false, false),
- new ColumnInfo('startTime', 'text', -1, false, false, false),
- new ColumnInfo('endTime', 'text', -1, false, false, false),
- new ColumnInfo('frequency', 'text', -1, false, false, false),
- new ColumnInfo('isDone', 'boolean', -1, true, false, false),
- new ColumnInfo('finValue', 'text', -1, false, false, false),
- new ColumnInfo('isOpen', 'boolean', -1, true, false, false)
- ];
-
- // TableHelper.ets
- createTableSql(tableName: string, columns: Array<ColumnInfo>): string {
- let sql = `create table if not exists ${tableName}(`;
- for (let column of columns) {
- sql = sql.concat(`${column.name} ${column.type}`);
- sql = sql.concat(`${column.length && column.length > 0 ? `(${column.length})` : ''}`);
- sql = sql.concat(`${column.primary ? ' primary key' : ''}`);
- sql = sql.concat(`${column.autoincrement ? ' autoincrement' : ''}`);
- sql = sql.concat(`${column.nullable ? '' : ' not null'}`);
- sql = sql.concat(', ');
- }
- sql = `${sql.substring(0, sql.length - 2)})`;
- return sql;
- }
健康任务信息数据表需要提供插入数据的接口,以在用户当天第一次打开应用时创建当天的健康任务信息。
- // TaskInfoApi.ets
- insertData(taskInfo: TaskInfo, callback: Function): void {
- // 根据输入数据创建待插入的数据行
- const valueBucket = generateBucket(taskInfo);
- RdbUtils.insert('taskInfo', valueBucket).then((result: number) => {
- callback(result);
- });
- Logger.info('TaskInfoTable', `Insert taskInfo {${taskInfo.date}:${taskInfo.taskID}} finished.`);
- }
-
- function generateBucket(taskInfo: TaskInfo): dataRdb.ValuesBucket {
- let valueBucket = {} as dataRdb.ValuesBucket;
- Const.TASK_INFO.columns?.forEach((item: string) => {
- if (item !== 'id') {
- switch (item) {
- case 'date':
- valueBucket[item] = taskInfo.date;
- break;
- case 'taskID':
- valueBucket[item] = taskInfo.taskID;
- break;
- case 'targetValue':
- valueBucket[item] = taskInfo.targetValue;
- break;
- case 'isAlarm':
- valueBucket[item] = taskInfo.isAlarm;
- break;
- case 'startTime':
- valueBucket[item] = taskInfo.startTime;
- break;
- case 'endTime':
- valueBucket[item] = taskInfo.endTime;
- break;
- case 'frequency':
- valueBucket[item] = taskInfo.frequency;
- break;
- case 'isDone':
- valueBucket[item] = taskInfo.isDone;
- break;
- case 'finValue':
- valueBucket[item] = taskInfo.finValue;
- break;
- case 'isOpen':
- valueBucket[item] = taskInfo.isOpen;
- break;
- default:
- break;
- }
- }
- });
- return valueBucket;
- }
-
- // CommonConstants.ets
- static readonly TASK_INFO = {
- tableName: 'taskInfo',
- columns: [
- 'id',
- 'date',
- 'taskID',
- 'targetValue',
- 'isAlarm',
- 'startTime',
- 'endTime',
- 'frequency',
- 'isDone',
- 'finValue',
- 'isOpen'
- ]} as CommonConstantsInfo
用户开启和关闭任务,改变任务的目标值、提醒时间、频率等,用户打卡后修改任务的实际完成值都是通过更新数据接口来实现的。
- // TaskInfoApi.ets
- updateDataByDate(taskInfo: TaskInfo, callback: Function): void {
- const valueBucket = generateBucket(taskInfo);
- let tableName = Const.TASK_INFO.tableName;
- if (!tableName) {
- return;
- }
- let predicates = new dataRdb.RdbPredicates(tableName);
-
- // 根据date和taskID匹配要更新的数据行
- predicates.equalTo('date', taskInfo.date).and().equalTo('taskID', taskInfo.taskID);
- RdbUtils.update(valueBucket, predicates).then((result: number) => {
- callback(result);
- });
- Logger.info('TaskInfoTable', `Update data {${taskInfo.date}:${taskInfo.taskID}} finished.`);
- }
用户可以查看当天和以前某日的健康任务信息,需要提供查找数据接口。
- // TaskInfoApi.ets
- query(date: string, isOpen: boolean = true, callback: Function): void {
- let tableName = Const.TASK_INFO.tableName;
- if (!tableName) {
- return;
- }
- let predicates = new dataRdb.RdbPredicates(tableName);
- predicates.equalTo('date', date);
-
- // 如果isOpen为true,则只查找开启的任务
- if (isOpen) {
- predicates.equalTo('isOpen', true);
- }
- predicates.orderByAsc('taskID'); // 查找结果按taskID排序
- RdbUtils.query(predicates).then(resultSet => {
- let count = resultSet.rowCount;
- // 查找结果为空则返回空数组,否则返回查找结果数组
- if (count === 0 || typeof count === 'string') {
- Logger.error('TaskInfoTable', `${date} query no results!`);
- const result: TaskInfo[] = [];
- callback(result);
- } else {
- resultSet.goToFirstRow();
- const result: TaskInfo[] = [];
- for (let i = 0; i < count; i++) {
- let tmp = new TaskInfo(0, '', 0, '', false, '', '', '', false, '');
- tmp.id = resultSet.getDouble(resultSet.getColumnIndex('id'));
- ... // 省略赋值代码
- result[i] = tmp;
- resultSet.goToNextRow();
- }
- callback(result);
- }
- });
- }
在当天第一次打开应用时需要初始化每日信息数据,页面需要根据用户编辑任务和打卡的情况来更新当天目标任务个数和完成任务个数,所以需要提供插入数据和更新数据的接口,写法与上一条中相应接口类似,不再赘述。
- // EntryAbility.ets
- async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
- ...
- RdbUtils.createTable(Const.DAY_INFO.tableName ? Const.DAY_INFO.tableName : '', columnDayInfoList).then(() => {
- Logger.info(`RdbHelper createTable dayInfo success`);
- }).catch((err: Error) => {
- Logger.error(`RdbHelper dayInfo err : ${JSON.stringify(err)}`);
- });
- ...
- }
-
- // CommonConstants.ets
- static readonly DAY_INFO = {
- tableName: 'dayInfo',
- columns: ['date', 'targetTaskNum', 'finTaskNum']
- } as CommonConstantsInfo
-
- // RdbColumnModel.ets
- export const columnDayInfoList: Array<ColumnInfo> = [
- new ColumnInfo('date', 'text', -1, false, true, false),
- new ColumnInfo('targetTaskNum', 'integer', -1, true, false, false),
- new ColumnInfo('finTaskNum', 'integer', -1, true, false, false)
- ];
-
- // TableHelper.ets
- createTableSql(tableName: string, columns: Array<ColumnInfo>): string {
- let sql = `create table if not exists ${tableName}(`;
- for (let column of columns) {
- sql = sql.concat(`${column.name} ${column.type}`);
- sql = sql.concat(`${column.length && column.length > 0 ? `(${column.length})` : ''}`);
- sql = sql.concat(`${column.primary ? ' primary key' : ''}`);
- sql = sql.concat(`${column.autoincrement ? ' autoincrement' : ''}`);
- sql = sql.concat(`${column.nullable ? '' : ' not null'}`);
- sql = sql.concat(', ');
- }
- sql = `${sql.substring(0, sql.length - 2)})`;
- return sql;
- }
页面需要查找对应日期的目标任务个数和完成任务个数用以在页面显示任务进度,因此需要查找数据的接口。且页面在打开时需要显示当周每天任务的完成情况,因此需要允许一次调用查找一周的每日任务信息。
- // DayInfoApi.ets
- queryList(dates: string[], callback: Function): void {
- let predicates: dataRdb.RdbPredicates = new dataRdb.RdbPredicates(Const.DAY_INFO.tableName ? Const.DAY_INFO.tableName : '');
- predicates.in('date', dates); // 匹配日期数组内的所有日期
- RdbUtils.query(predicates).then(resultSet => {
- let count = resultSet.rowCount;
- if (count === 0) {
- Logger.info('DayInfoTable', 'query no results.');
- let result: DayInfo[] = [];
- callback(result);
- } else {
- resultSet.goToFirstRow();
- let result: DayInfo[] = [];
- for (let i = 0; i < count; i++) {
- let tmp = new DayInfo('', 0, 0);
- ... // 省略赋值代码
- result[i] = tmp;
- resultSet.goToNextRow();
- }
- callback(result);
- }
- });
- }
全局信息数据表同样需要提供插入数据、更新数据和查找数据的接口,写法与本节前两条中相应接口类似,不再赘述。
- // EntryAbility.ets
- async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
- ...
- RdbUtils.createTable(Const.GLOBAL_INFO.tableName ? Const.GLOBAL_INFO.tableName : '', columnGlobalInfoList).then(() => {
- Logger.info(`RdbHelper createTable globalInfo success`);
- }).catch((err: Error) => {
- Logger.error(`RdbHelper globalInfo err : ${JSON.stringify(err)}`);
- });
- ...
- }
-
- // CommonConstants.ets
- static readonly GLOBAL_INFO = {
- tableName: 'globalInfo',
- columns: ['id', 'firstDate', 'lastDate', 'checkInDays', 'achievements']
- } as CommonConstantsInfo
-
- // RdbColumnModel.ets
- export const columnGlobalInfoList: Array<ColumnInfo> = [
- new ColumnInfo('id', 'integer', -1, true, true, false),
- new ColumnInfo('firstDate', 'text', -1, false, false, false),
- new ColumnInfo('lastDate', 'text', -1, false, false, false),
- new ColumnInfo('checkInDays', 'integer', -1, true, false, false),
- new ColumnInfo('achievements', 'text', -1, false, false, false)
- ];
-
-
- // TableHelper.ets
- createTableSql(tableName: string, columns: Array<ColumnInfo>): string {
- let sql = `create table if not exists ${tableName}(`;
- for (let column of columns) {
- sql = sql.concat(`${column.name} ${column.type}`);
- sql = sql.concat(`${column.length && column.length > 0 ? `(${column.length})` : ''}`);
- sql = sql.concat(`${column.primary ? ' primary key' : ''}`);
- sql = sql.concat(`${column.autoincrement ? ' autoincrement' : ''}`);
- sql = sql.concat(`${column.nullable ? '' : ' not null'}`);
- sql = sql.concat(', ');
- }
- sql = `${sql.substring(0, sql.length - 2)})`;
- return sql;
- }
应用首次打开时,数据库中没有数据,要做数据库的初始化,写入一组空数据。另外,如果用户连续几天没有打开APP,再次打开时需要将数据回写至数据库。因此需要实现一个数据库接口,在应用打开时调用,进行上述操作。
- // DatabaseModel.ets
- query(date: string, callback: Function) {
- let result: TaskInfo[] = [];
- let self = this;
- GlobalInfoApi.query((globalResult: GlobalInfo) => {
- // 如果查不到全局信息,就写入全局信息
- if (!globalResult.firstDate) {
- ... // 插入健康任务信息、每日信息和全局信息
- callback(result, dayInfo);
- } else {
- // 如果查到全局信息,那么查询当日任务信息
- let newGlobalInfo = globalResult;
- let preDate = globalResult.lastDate;
- newGlobalInfo.lastDate = date;
- ... // 更新全局信息
-
- // 查询当日任务信息
- GlobalInfoApi.updateData(newGlobalInfo, (isDone: number) => {
- if (isDone) {
- Logger.info('AppStart', 'update globalInfo success: ' + JSON.stringify(newGlobalInfo));
- }
- });
- self.queryPreInfo(self, date, preDate, result, callback);
- }
- });
- }
日志类Logger旨在提供一个全局的日志打印、日志管理的地方,既可以规范整个应用的日志打印,也方便日后对日志工具类进行修改,而不需要去改动代码中每一个调用日志的地方,目前分info,debug,warn,error四个级别。
- // Logger.ets
- const LOGGER_PREFIX: string = 'Healthy_life';
-
- class Logger {
- private domain: number;
- private prefix: string;
-
- // format Indicates the log format string.
- private format: string = '%{public}s, %{public}s';
-
- constructor(prefix: string = '', domain: number = 0xFF00) {
- this.prefix = prefix;
- this.domain = domain;
- }
-
- debug(...args: string[]): void {
- hilog.debug(this.domain, this.prefix, this.format, args);
- }
-
- info(...args: string[]): void {
- hilog.info(this.domain, this.prefix, this.format, args);
- }
-
- warn(...args: string[]): void {
- hilog.warn(this.domain, this.prefix, this.format, args);
- }
-
- error(...args: string[]): void {
- hilog.error(this.domain, this.prefix, this.format, args);
- }
- }
-
- export default new Logger(LOGGER_PREFIX, 0xFF02);
为全局提供时间工具,避免重复定义。常用时间相关常量,时间函数示例(由时间常量衍生出星期一到星期日和数字 1-7 的字典映射)。
- // Utils.ets
- const CHINESE_OF_WEEK: string[] = ['一', '二', '三', '四', '五', '六', '日'];
- const YEAR: string = '年';
- const MONTH: string = '月';
- const DAY: string = '日';
- const WEEK: string = '星期';
- const DAYS_OF_WEEK: number = 7;
- const SUNDAY_FIRST_SHIFT: number = 6;
-
- // 时间函数示例
- export const oneWeekDictFunc = () => {
- const oneWeekDict: Array<string> = [];
- for (let index = 0; index < CHINESE_OF_WEEK.length; index++) {
- oneWeekDict[index] = `${WEEK}${CHINESE_OF_WEEK[index]}`;
- }
- return oneWeekDict;
- }
把比例等分浮点数转换为百分比字符串,例如成就页面,每一行平均分布三个徽章,可以先定义一个浮点数代表等分比例,再转换为百分比字符串。
- // Utils.ets
- export function ratio2percent(ratio: number): string {
- return `${ ratio * 100 }%`;
- }
-
- // import Utils工具方法:
- import { ratio2percent } from '../common/utils/Utils';
-
- // 引用工具方法( 例如成就页面,每个徽章占据屏幕宽度的三分之一 ) :
- Column({ space: Const.DEFAULT_18 }) {
- ... // 省略徽章卡片的 UI 布局细节
- }
- .width(ratio2percent(achieveConst.ACHIEVE_SPLIT_RATIO))
事件分发类提供应用全局的事件注册,分发,接受,可以实现组件之间的解耦。全局共享一个实例, 将事件处理统一管理(HealthDataSrcMgr是单例)。注册事件、取消事件注册、发送事件代码示例如右侧所示。
- // HomeComponent.ets
- @Provide broadCast: BroadCast = HealthDataSrcMgr.getInstance().getBroadCast();
-
- // HealthDataSrcMgr.ets
- public getBroadCast(): BroadCast {
- return this.broadCast;
- }
-
- // 事件注册
- // CustomDialogView.ets
- aboutToAppear() {
- ...
- this.broadCast.on(BroadCastType.SHOW_ACHIEVEMENT_DIALOG, (achievementLevel: number) => {
- ... // 省略回调细节
- })
- ...
- }
-
- // BroadCast.ets
- public on(event: string, callback: Function) {
- switch (event) {
- case BroadCastType.SHOW_ACHIEVEMENT_DIALOG:
- this.callBackArray.showAchievementDialog = callback;
- break;
- case BroadCastType.SHOW_TASK_DETAIL_DIALOG:
- this.callBackArray.showTaskDetailDialog = callback;
- break;
- case BroadCastType.SHOW_TARGET_SETTING_DIALOG:
- this.callBackArray.showTargetSettingDialog = callback;
- break;
- case BroadCastType.SHOW_REMIND_TIME_DIALOG:
- this.callBackArray.showRemindTimeDialog = callback;
- break;
- case BroadCastType.SHOW_FREQUENCY_DIALOG:
- this.callBackArray.showFrequencyDialog = callback;
- break;
- default:
- break;
- }
- }
-
-
- // BroadCast.ets
- public off(event: string, callback: Function) {
- if (event === null) {
- Logger.info(FILE_TAG, 'cancel all broadcast');
- this.callBackArray = callBackArrayTemp;
- }
- Logger.info(FILE_TAG, 'cancel broadcast with type '+ event);
- const cbs = this.callBackArray;
- if (!cbs) {
- return;
- }
- if (callback === null) {
- switch (event) {
- case BroadCastType.SHOW_ACHIEVEMENT_DIALOG:
- this.callBackArray.showAchievementDialog = () => {};
- break;
- case BroadCastType.SHOW_TASK_DETAIL_DIALOG:
- this.callBackArray.showTaskDetailDialog = () => {};
- break;
- case BroadCastType.SHOW_TARGET_SETTING_DIALOG:
- this.callBackArray.showTargetSettingDialog = () => {};
- break;
- case BroadCastType.SHOW_REMIND_TIME_DIALOG:
- this.callBackArray.showRemindTimeDialog = () => {};
- break;
- case BroadCastType.SHOW_FREQUENCY_DIALOG:
- this.callBackArray.showFrequencyDialog = () => {};
- break;
- default:
- break;
- }
- }
- }
-
- // 发送事件
- // HomeComponent.ets
- taskItemAction(item: TaskInfo, isClick: boolean): void {
- ...
- if (isClick) {
- // 点击任务打卡
- ...
- this.broadCast.emit(BroadCastType.SHOW_TASK_DETAIL_DIALOG, [item, callback]);
- } else {
- ...
- }
- }
-
- // BroadCast.ets
- public emit(event: string, args?: (number | number[] | (TaskInfo | CustomDialogCallback)[])) {
- if (!this.callBackArray) {
- Logger.info(FILE_TAG, 'emit broadcast failed for no callback');
- return;
- }
- Logger.info(FILE_TAG, 'emit broadcast with type '+ event);
- let cbs: Array<Function> = [];
- switch (event) {
- case BroadCastType.SHOW_ACHIEVEMENT_DIALOG:
- cbs = [this.callBackArray.showAchievementDialog];
- break;
- case BroadCastType.SHOW_TASK_DETAIL_DIALOG:
- cbs = [this.callBackArray.showTaskDetailDialog];
- break;
- case BroadCastType.SHOW_TARGET_SETTING_DIALOG:
- cbs = [this.callBackArray.showTargetSettingDialog];
- break;
- case BroadCastType.SHOW_REMIND_TIME_DIALOG:
- cbs = [this.callBackArray.showRemindTimeDialog];
- break;
- case BroadCastType.SHOW_FREQUENCY_DIALOG:
- cbs = [this.callBackArray.showFrequencyDialog];
- break;
- default:
- break;
- }
- if (cbs) {
- let len = cbs.length;
- for (let i = 0; i < len; i++) {
- try {
- if (args instanceof Array) {
- cbs[i](args[0], args[1]);
- } else {
- cbs[i](args);
- }
- } catch (error) {
- new Error(error);
- }
- }
- }
- }
您已经完成了本次Codelab的学习,并了解到以下知识点:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。