本篇Codelab介绍了如何实现一个简单的健康生活应用,主要功能包括:
本应用的运行效果如下图所示:
相关概念完成本篇Codelab我们首先要完成开发环境的搭建,本示例以RK3568开发板为例,参照以下步骤进行:
2.搭建烧录环境。
3.搭建开发环境。
本篇Codelab只对核心代码进行讲解。
├─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 // 资源文件夹
应用架构分析
本应用的基本架构如下图所示,数据库为其他服务提供基础的用户数据,主要业务包括:用户可以查看和编辑自己的健康任务并进行打卡、查看成就。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))
首页
首页包含了任务信息的所有入口,包含任务列表的展示,任务的编辑和新增,上下滚动的过程中顶部导航栏的渐变,日期的切换以及随着日期切换界面任务列表跟着同步的功能,效果图如下:
具体代码实现我们将在下边分模块进行说明:
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;
}
由于首页右下角有一个悬浮按钮,所以首页整体我们用了一个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'))
4.界面跳转及传参
首页任务列表长按时需要跳转到对应的任务编辑界面,同时点击悬浮按钮时需要跳转到任务列表页面。
页面跳转需要在头部引入router。
// HomeComponent.ets import router from '@ohos.router';
// 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"
}
]
后台代理提醒entry\src\main\ets\service\ReminderAgent.ts文件中提供了发布提醒任务、查询提醒任务、删除提醒任务三个接口供任务编辑页面调用,跟随任务提醒的开关增加、更改、删除相关后台代理提醒,代码如下:
// 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;
实现打卡功能
首页会展示当前用户已经开启的任务列表,每条任务会显示对应的任务名称以及任务目标、当前任务完成情况。用户只可对当天任务进行打卡操作,用户可以根据需要对任务列表中相应的任务进行点击打卡。如果任务列表中的每个任务都在当天完成则为连续打卡一天,连续打卡多天会获得成就徽章。打卡效果如下图所示:
任务列表使用List组件展示用户当前已经开启的任务,每条任务对应一个TaskCard组件,clickAction包装了点击和长按事件,用户点击任务卡时会触发弹起打卡弹窗,从而进行打卡操作;长按任务卡时会跳转至任务编辑界面,对相应的任务进行编辑处理。代码如下:
// HomeComponent.ets
// 任务列表
ForEach(this.homeStore.getTaskListOfDay(), (item: TaskInfo) => {
TaskCard({
taskInfoStr: JSON.stringify(item),
clickAction: (isClick: boolean) => this.taskItemAction(item, isClick)
})
.margin({ bottom: Const.DEFAULT_12 })
.height($r('app.float.default_64'))
}, (item: TaskInfo) => JSON.stringify(item))
...
CustomDialogView() // 自定义弹窗中间件
自定义弹窗中间件CustomDialogView
在组件CustomDialogView的aboutToAppear生命周期中注册SHOW_TASK_DETAIL_DIALOG的事件回调方法 ,当通过emit触发此事件时即触发回调方法执行。代码如下:
// CustomDialogView.ets
export class CustomDialogCallback {
confirmCallback: Function = () => {};
cancelCallback: Function = () => {};
}
@Component
export struct CustomDialogView {
@State isShow: boolean = false;
@Provide achievementLevel: number = 0;
@Consume broadCast: BroadCast;
@Provide currentTask: TaskInfo = TaskItem;
@Provide dialogCallBack: CustomDialogCallback = new CustomDialogCallback();
// 成就对话框
achievementDialog: CustomDialogController = new CustomDialogController({
builder: AchievementDialog(),
autoCancel: true,
customStyle: true
});
// 任务时钟对话框
taskDialog: CustomDialogController = new CustomDialogController({
builder: TaskDetailDialog(),
autoCancel: true,
customStyle: true
});
aboutToAppear() {
Logger.debug('CustomDialogView', 'aboutToAppear');
// 成就对话框
this.broadCast.on(BroadCastType.SHOW_ACHIEVEMENT_DIALOG, (achievementLevel: number) => {
Logger.debug('CustomDialogView', 'SHOW_ACHIEVEMENT_DIALOG');
this.achievementLevel = achievementLevel;
this.achievementDialog.open();
});
// 任务时钟对话框
this.broadCast.on(BroadCastType.SHOW_TASK_DETAIL_DIALOG,
(currentTask: TaskInfo, dialogCallBack: CustomDialogCallback) => {
Logger.debug('CustomDialogView', 'SHOW_TASK_DETAIL_DIALOG');
this.currentTask = currentTask || TaskItem;
this.dialogCallBack = dialogCallBack;
this.taskDialog.open();
});
}
aboutToDisappear() {
Logger.debug('CustomDialogView', 'aboutToDisappear');
}
build() {
}
}
点击任务卡片
点击任务卡片会emit触发 “SHOW_TASK_DETAIL_DIALOG” 事件,同时把当前任务,以及确认打卡回调方法传递下去。代码如下:
// HomeComponent.ets
// 任务卡片事件
taskItemAction(item: TaskInfo, isClick: boolean): void {
...
if (isClick) {
// 点击任务打卡
let callback: CustomDialogCallback = { confirmCallback: (taskTemp: TaskInfo) => {
this.onConfirm(taskTemp)
}, cancelCallback: () => {
} };
// 触发弹出打卡弹窗事件 并透传当前任务参数(item) 以及确认打卡回调
this.broadCast.emit(BroadCastType.SHOW_TASK_DETAIL_DIALOG, [item, callback]);
} else {
// 长按编辑任务
...
}
}
// 确认打卡
onConfirm(task) {
this.homeStore.taskClock(task).then((res: AchievementInfo) => {
// 打卡成功后 根据连续打卡情况判断是否 弹出成就勋章 以及成就勋章级别
if (res.showAchievement) {
// 触发弹出成就勋章SHOW_ACHIEVEMENT_DIALOG 事件, 并透传勋章类型级别
let achievementLevel = res.achievementLevel;
if (achievementLevel) {
this.broadCast.emit(BroadCastType.SHOW_ACHIEVEMENT_DIALOG, achievementLevel);
} else {
this.broadCast.emit(BroadCastType.SHOW_ACHIEVEMENT_DIALOG);
}
}
})
}
打卡弹窗组件TaskDetailDialog
打卡弹窗组件根据当前任务的ID获取任务名称以及弹窗背景图片资源。
打卡弹窗组件由两个小组件构成,代码如下:
// TaskDetailDialog.ets
Column() {
// 展示任务的基本信息
TaskBaseInfo({
taskName: TaskMapById[this.currentTask?.taskID - 1].taskName // 根据当前任务ID获取任务名称
});
// 打卡功能组件 (任务打卡、关闭弹窗)
TaskClock({
confirm: () => {
this.dialogCallBack.confirmCallback(this.currentTask);
this.controller.close();
},
cancel: () => {
this.controller.close();
},
showButton: this.showButton
})
}
...
TaskBaseInfo组件代码如下:
// TaskDetailDialog.ets
@Component
struct TaskBaseInfo {
taskName: string | Resource = '';
build() {
Column({ space: Const.DEFAULT_8 }) {
Text(this.taskName)
.fontSize($r('app.float.default_22'))
.fontWeight(FontWeight.Bold)
.fontFamily($r('app.string.HarmonyHeiTi_Bold'))
.taskTextStyle()
.margin({left: $r('app.float.default_12')})
}
.position({ y: $r('app.float.default_267') })
}
}
TaskClock组件代码如下:
// 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'))
.height($r('app.float.default_42'))
.fontSize($r('app.float.default_20'))
.fontWeight(FontWeight.Normal)
.textStyle()
}
.width($r('app.float.default_220'))
.borderRadius($r('app.float.default_24'))
.backgroundColor('rgba(255,255,255,0.40)')
.onClick(() => {
GlobalContext.getContext().setObject('taskListChange', true);
this.confirm();
})
.visibility(!this.showButton ? Visibility.None : Visibility.Visible)
Text($r('app.string.got_it'))
.fontSize($r('app.float.default_14'))
.fontWeight(FontWeight.Regular)
.textStyle()
.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;
}
// HomeViewModel.ets
// 更新当天任务列表
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;
if (step === 0) { // 任务步长为0 打卡一次即完成该任务
updateTask.isDone = true; // 打卡一次即完成该任务
updateTask.finValue = targetValue;
} else {
let value = Number(finValue) step; // 任务步长非0 打卡一次 步长与上次打卡进度累加
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);
})
})
}
为了帮助大家更深入有效的学习到鸿蒙开发知识点,小编特意给大家准备了一份全套最新版的HarmonyOS NEXT学习资源,获取完整版方式请点击→《》
HarmonyOS教学视频:语法ArkTS、TypeScript、ArkUI等.....视频教程鸿蒙生态应用开发白皮书V2.0PDF:获取完整版白皮书请点击→《》
鸿蒙 (Harmony OS)开发学习手册一、入门必看更多了解更多鸿蒙开发的相关知识可以参考:《》
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved