ArkTS实现闹钟
一、需求说明
基于ArkTS的声明式开发范式、HarmonyOS的后台代理提醒能力和用户首选项,实现简单的闹钟提醒功能。
-
-
-
用户首选项(Preferences):提供了轻量级配置数据的持久化能力,并支持订阅数据变化的通知能力。通常用于保存应用的配置信息。数据通过文本的形式保存在设备中,应用使用过程中会将文本中的数据全量加载到内存中,所以访问速度快、效率高,但不适合需要存储大量数据的场景。
-
-
publishReminder
发布一个后台代理提醒,使用callback方式实现异步调用,该方法需要申请通知弹窗Notification.requestEnableNotification后才能调用。
-
cancelReminder
取消指定id的提醒,使用callback方式实现异步调用。
-
1.2.相关权限
本篇Codelab需要在module.json5中配置如下权限:
"requestPermissions": [ { "name": "ohos.permission.PUBLISH_AGENT_REMINDER", "reason": "$string:reason", "usedScene": { "abilities": [ "EntryAbility" ], "when": "inuse" } } ]
如下图所示:
1.3.使用说明
-
打开应用,添加一个闹钟,闹钟到设定的时间后弹出提醒。
-
修改闹钟的时间,闹钟到设定的时间后弹出提醒。
-
删除闹钟。
-
展示闹钟列表,并可打开和关闭单个闹钟。
二、创建通用类
在common包下分别创建:constants和utils包,
2.1.constants包
在constants包中,通常会保存项目中使用的各种常量
①.AlarmSettingType.ets
用于定义闹钟设置的类型或枚举,包括不同类型的闹钟设置,例如单次闹钟、重复闹钟、定时器等。代码如下:
/** * 闹钟类型枚举 */ export enum AlarmSettingType { /** * 设置类型重复 */ REPEAT, /** * 设置类型名称 */ ALARM_NAME, /** * 设置铃声持续时间 */ RING_DURATION, /** * 闹铃间隔时间 */ INTERVAL }
②.CommonConstants.ets:
用于存储项目中的通用常量,这些常量被整个项目的多个模块或组件使用。例如全局配置参数、错误码、通用的配置项等。代码如下:
export class CommonConstants { /** * 数据库首选项 ID。 */ static readonly PREFERENCE_ID = 'storageId'; /** * 数据库 闹钟 数据的 key. */ static readonly ALARM_KEY = 'alarmData'; /** * 全部长度100% */ static readonly FULL_LENGTH: string = '100%'; /** * 默认字符串空格 */ static readonly DEFAULT_STRING_SPACE: string = ' '; /** * 默认字符串逗号 */ static readonly DEFAULT_STRING_COMMA: string = ','; /** * 闹钟是否重复,常量不重复。 */ static readonly DEFAULT_STRING_NO_REPEAT: string = '不重复'; /** * 默认值为负数 */ static readonly DEFAULT_NUMBER_NEGATIVE: number = -1; /** * 默认布局权重 */ static readonly DEFAULT_LAYOUT_WEIGHT: number = 1; /** * 单 */ static readonly DEFAULT_SINGLE: number = 1; /** * 双 */ static readonly DEFAULT_DOUBLE: number = 2; /** * 默认的数据选择器小时选择。 */ static readonly DEFAULT_DATA_PICKER_HOUR_SELECTION: number = 2; /** * 默认总分钟数 */ static readonly DEFAULT_TOTAL_MINUTE: number = 60; /** * 默认字符串周一 */ static readonly DEFAULT_STRING_MONDAY: string = '周一'; /** * 默认字符串周二 */ static readonly DEFAULT_STRING_TUESDAY: string = '周二'; /** * 默认字符串周三 */ static readonly DEFAULT_STRING_WEDNESDAY: string = '周三'; /** * 默认字符串周四 */ static readonly DEFAULT_STRING_THURSDAY: string = '周四'; /** * 默认字符串周五 */ static readonly DEFAULT_STRING_FRIDAY: string = '周五'; /** * 默认字符串周六 */ static readonly DEFAULT_STRING_SATURDAY: string = '周六'; /** * 默认字符串周七 */ static readonly DEFAULT_STRING_SUNDAY: string = '周日'; /** * 默认数字时刻 */ static readonly DEFAULT_NUMBER_MOMENT: number = 3; /** * 默认间隔步长 */ static readonly DEFAULT_INTERVAL_STEP: number = 5; /** * 通用程度 */ static readonly DEFAULT_COMMON_DEGREE: number = 6; /** * 默认指针宽度。 */ static readonly DEFAULT_POINTER_WIDTH: number = 10; /** * 默认总小时数 */ static readonly DEFAULT_TOTAL_HOUR: number = 12; /** * 默认间隔时间最大值 */ static readonly DEFAULT_INTERVAL_TIME_MAX: number = 10; /** * 默认间隔 分钟 */ static readonly DEFAULT_INTERVAL_MINUTE_MAX: number = 30; /** * bundle name. */ static readonly BUNDLE_NAME: string = 'com.augus.myapplication'; /** * ability name. */ static readonly ABILITY_NAME: string = 'EntryAbility'; } /** * 一周的默认数字 */ export enum WeekDays { DEFAULT_NUMBER_MONDAY = 1, DEFAULT_NUMBER_TUESDAY = 2, DEFAULT_NUMBER_WEDNESDAY = 3, DEFAULT_NUMBER_THURSDAY = 4, DEFAULT_NUMBER_FRIDAY = 5, DEFAULT_NUMBER_SATURDAY = 6, DEFAULT_NUMBER_SUNDAY = 7 }
③.DetailConstant.ets:
用于存储闹钟详细信息相关的常量,例如闹钟时间选择信息、星期数据、铃声持续时间等等,代码如下:
import DayDataItem from '../../viewmodel/DayDataItem' /** * 页面常量描述 */ export class DetailConstant { /** * 详细页面时间数据。 */ static readonly DAY_DATA: DayDataItem[] = [ { timeType: 0, delSelect: 0, data: ['上午', '下午'] } as DayDataItem, { timeType: 1, delSelect: 0, data: [ '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12' ] } as DayDataItem, { timeType: 2, delSelect: 0, data: [ '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '00', ] } as DayDataItem ]; /** * 星期数据 */ static readonly WEEKDAY_DATA: number[] = [1, 2, 3, 4, 5, 6, 7]; /** * 铃声持续时间列表数据。 */ static readonly RING_DURATION: number[] = [1, 5, 10, 15, 20, 30]; /** * 默认 分钟。 */ static readonly DEFAULT_STRING_MINUTE: string = '分钟'; /** * 组名 */ static readonly DEFAULT_STRING_GROUP_NAME: string = 'radioGroup'; /** * default string provider key. */ static readonly DEFAULT_PROVIDER_KEY: string = 'alarmItemProvide'; /** * 创建闹钟选项名:重复 */ static readonly DEFAULT_STRING_REPEAT: string = '重复'; /** * 创建闹钟选项名:闹钟名 */ static readonly DEFAULT_STRING_ALARM_NAME: string = '闹钟名'; /** * 创建闹钟选项名:再响间隔 */ static readonly DEFAULT_STRING_INTERVAL: string = '再响间隔'; /** * 创建闹钟选项名:闹铃时长 */ static readonly DEFAULT_STRING_DURATION: string = '闹铃时长'; /** * 字符串单位:次 */ static readonly DEFAULT_STRING_TIMES: string = '次'; }
④.MainConstant.ets:
用于存储项目中的主要常量或枚举,包含各种与项目主要功能或模块相关的常量。如下:
/** * Main page constant description. */ export class MainConstant { /** * Main page times list. */ static readonly TIMES: number[] = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2]; /** * Default single digit max. */ static readonly DEFAULT_SINGLE_DIGIT_MAX: number = 9; /** * Default horizontal angle. */ static readonly DEFAULT_HORIZONTAL_ANGLE: number = 180; /** * Default one second ms. */ static readonly DEFAULT_ONE_SECOND_MS: number = 1000; /** * Default zeroing. */ static readonly DEFAULT_ZEROING: string = '0'; /** * Default string morning. */ static readonly DEFAULT_STRING_MORNING: string = '上午'; /** * Default string afternoon. */ static readonly DEFAULT_STRING_AFTERNOON: string = '下午'; /** * Default string alarm name. */ static readonly DEFAULT_STRING_ALARM: string = '闹钟'; /** * Default string quotation. */ static readonly DEFAULT_STRING_NULL: string = ''; /** * Default string colon. */ static readonly DEFAULT_STRING_COLON: string = ':'; /** * Default clock time font size unit. */ static readonly CLOCK_TIME_FONT_SIZE_UNIT: string = 'px'; /** * Hour pointer image url. */ static readonly HOUR_POINTER_IMAGE_URL: string = '../../../resources/base/media/ic_hour_pointer.png'; /** * Minute pointer image url. */ static readonly MINUTE_POINTER_IMAGE_URL: string = '../../../resources/base/media/ic_minute_pointer.png'; /** * Second pointer image url. */ static readonly SECOND_POINTER_IMAGE_URL: string = '../../../resources/base/media/ic_second_pointer.png'; /** * Clock pan image url. */ static readonly CLOCK_PAN_IMAGE_URL: string = '../../../resources/base/media/ic_clock_pan.png'; }
2.2.utils包:
在utils包中,通常会保存各种通用的工具函数,用于项目中的各种操作和处理
①.DataTypeUtils.ets:
数据类型转换、数据类型验证、数据类型处理等,代码如下:
/** * 数据类型工具类 */ export default class DataTypeUtils { /** * 判断obj是否为null或undefined或空字符串。 * * @return 返回一个布尔值. */ static isNull(obj: Object): boolean { return (typeof obj === 'undefined' || obj == null || obj === ''); } /** * 它接收一个参数obj,类型为数字数组,并返回一个新的深拷贝对象(创建obj的一个副本)。以避免直接修改原始对象 * * @return type in obj. */ static deepCopy(obj: number[]) { //声明了一个名为newObj的局部变量,它是一个空的数字数组 let newObj: number[] = []; //使用for循环遍历obj数组的每个元素。 for (let i = 0; i < obj.length; i++) { //对每个元素执行深拷贝操作,将其添加到newObj数组中 newObj[i] = JSON.parse(JSON.stringify(obj[i])); } return newObj; } }
②DimensionUtil.ets:
尺寸适配的工具类,用于根据设备屏幕的宽度和高度,将设计稿中的尺寸进行适配,代码如下:
import display from '@ohos.display'; // 导入显示相关的模块 import { GlobalContext } from './GlobalContext'; // 导入全局上下文模块 let context = getContext(this); // 获取当前上下文 /** * 定义了一个设计稿的宽度和高度。设计稿是在软件开发中用于设计界面的原始图稿,通常由设计师根据产品需求和用户体验设计制作。设计稿可以是静态的图像文件,也可以是交互式的原型,其目的是为了呈现出软件界面的外观和交互效果。 * 为什么要定义设计稿?主要有以下几个原因: * 基准尺寸: 设计稿的宽度和高度作为基准尺寸,用于开发人员进行界面布局和尺寸适配。在开发过程中,需要将设计稿中的各种元素和布局转化为代码,而设计稿的尺寸就是开发人员在开发过程中的参考标准。 * 视觉一致性: 定义设计稿可以确保不同开发人员或团队在开发过程中都能够参考同样的界面设计,从而保持视觉上的一致性。这对于确保最终产品的外观和用户体验是非常重要的。 * 尺寸适配: 在不同的设备上,尤其是移动设备上,屏幕尺寸和分辨率可能各不相同。通过定义设计稿,开发人员可以根据设计稿的尺寸进行适配,确保界面在不同设备上显示效果一致。 * 开发效率: 设计稿可以作为开发的指导,提高开发效率。开发人员可以根据设计稿的布局和样式进行开发,而无需进行过多的猜测或调整。 * 因此,定义设计稿可以帮助开发团队更好地理解产品设计,保持视觉一致性,并提高开发效率。 */ const DESIGN_WIDTH = 360; const DESIGN_HEIGHT = 780; /** * 尺寸适配的工具类,用于根据设备屏幕的宽度和高度,将设计稿中的尺寸进行适配,以确保在不同设备上显示效果一致。 */ export default class DimensionUtil { // 静态方法,用于根据设备显示屏幕的宽度和高度,对设计稿中的尺寸进行适配 static adaptDimension(value: number): number { // 从全局上下文中获取了名为 'globalDisplay' 的对象,并将其转换为 display.Display 类型的对象,表示设备的显示信息。 let deviceDisplay = GlobalContext.getContext().getObject('globalDisplay') as display.Display; // 计算了设备显示屏宽度与设计稿宽度的比例,用于后续的尺寸适配计算。 let widthScale = deviceDisplay.width / DESIGN_WIDTH; // 根据宽度比例计算了适配后的虚拟高度,保持了设计稿的宽高比。 let virtualHeight = widthScale * DESIGN_HEIGHT; // 计算了设计稿的对角线长度,用于后续的尺寸换算。 let designDim = Math.sqrt(DESIGN_WIDTH * DESIGN_WIDTH + DESIGN_HEIGHT * DESIGN_HEIGHT); // 计算了适配后的虚拟对角线长度,用于后续的尺寸换算 let virtualDim = Math.sqrt(deviceDisplay.width * deviceDisplay.width + virtualHeight * virtualHeight); // 根据设计稿和设备的对角线长度比例,对输入的尺寸值进行适配计算,并返回适配后的尺寸值。 return virtualDim * value / designDim; } /** * 获取适配后的像素值 * @param value * @returns 返回一个数字,表示适配后的像素值。 */ static getPx(value: Resource): number { //getNumber:用户获取指定资源ID对应的integer数值或者float数值,使用同步方式返回资源对应的数值。 let beforeVp = context.resourceManager.getNumber(value.id); return DimensionUtil.adaptDimension(beforeVp); // 返回适配后的像素值 } // 静态方法,用于获取适配后的vp值 static getVp(value: Resource): number { //getNumber:用户获取指定资源ID对应的integer数值或者float数值,使用同步方式返回资源对应的数值。 let beforeVp = context.resourceManager.getNumber(value.id); return px2vp(DimensionUtil.adaptDimension(beforeVp)); // 调用adaptDimension方法进行适配,并将像素值转换为vp值 } // 静态方法,用于获取适配后的fp值 static getFp(value: Resource): number { //getNumber:用户获取指定资源ID对应的integer数值或者float数值,使用同步方式返回资源对应的数值。 let beforeFp = context.resourceManager.getNumber(value.id); return px2fp(DimensionUtil.adaptDimension(beforeFp)); // 调用adaptDimension方法进行适配,并将像素值转换为fp值 } }
③GlobalContext.ets
用于存储全局上下文或全局状态的文件,用于在整个项目中共享和访问全局的状态或上下文信息。代码如下:
export class GlobalContext { private constructor() { // 私有构造函数,防止外部实例化 } private static instance: GlobalContext; // 静态实例 private _objects = new Map<string, Object>(); // 存储对象的Map // 获取全局上下文的静态方法 public static getContext(): GlobalContext { if (!GlobalContext.instance) {// 如果实例不存在,则创建新实例 GlobalContext.instance = new GlobalContext(); } return GlobalContext.instance; // 返回全局上下文实例 } // 根据键获取存储的对象 getObject(value: string): Object | undefined { return this._objects.get(value); // 从Map中获取对应键的对象 } // 设置存储对象 setObject(key: string, objectClass: Object): void { this._objects.set(key, objectClass); // 将对象存储到Map中 } }
④Logger.ets:
用于存储日志记录相关的功能,如下:
import hilog from '@ohos.hilog'; class Logger { private domain: number; private prefix: string; private format: string = '%{public}s, %{public}s'; /** * constructor. * * @param Prefix identifies the log tag. * @param domain Domain Indicates the service domain, which is a hexadecimal integer ranging from 0x0 to 0xFFFFF. */ constructor(prefix: string = 'AlarmClock', 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('AlarmClock', 0xFF00);
三、完成用户首选项初始化和获取设备的屏幕对象
EntryAbility.ets中完成用户首选项初始化和获取设备的屏幕对象,如下:
四、model目录
"model" 目录通常用于存放与数据模型相关的代码,包括数据结构、数据访问对象、业务逻辑等。
4.1.创建ReminderService.ets
用于实现提醒服务相关的功能,包括提醒的创建、管理、触发等。代码如下:
import reminderAgent from '@ohos.reminderAgentManager'; import notification from '@ohos.notificationManager'; import { CommonConstants } from '../common/constants/CommonConstants'; import ReminderItem from '../viewmodel/ReminderItem'; import Logger from '../common/utils/Logger'; /** * 基于ohos提醒代理服务 */ export default class ReminderService { /** * 用于打开通知权限 */ public openNotificationPermission() { //通过调用 notification.requestEnableNotification() 方法请求用户授予通知权限。这是一个异步操作,当用户授予权限时会执行 then 方法中的代码。 notification.requestEnableNotification().then(() => { //如果通知权限请求成功,记录一条信息,说明通知权限已启用。 Logger.info('Enable notification success'); }).catch((err: Error) => { //记录一条错误信息,说明通知权限请求失败,并将错误信息转换为字符串进行记录。 Logger.error('Enable notification failed because ' + JSON.stringify(err)); }); } /** * 添加闹钟提醒 * * @param alarmItem ReminderItem 一个表示提醒事项的对象,可能包含闹钟的详细信息。 * @param callback callback 一个可选的回调函数,接受一个 reminderId 参数并不返回任何值 */ public addReminder(alarmItem: ReminderItem, callback?: (reminderId: number) => void) { //初始化 闹钟提醒设置 let reminder = this.initReminder(alarmItem); //发布一个后台代理提醒, reminder指的是需要发布的提醒实例,reminderId提醒 Id reminderAgent.publishReminder(reminder, (err, reminderId: number) => { if (callback != null) {//如果不为null则执行 callback(reminderId);//通过该回调获取认证结果信息或认证过程中的提示信息 } }); } /** * 添加和修改闹钟提醒 * * @param reminderId number */ public deleteReminder(reminderId: number) { //取消指定id的提醒,使用Promise方式实现异步调用。 reminderAgent.cancelReminder(reminderId); } /** * 初始化一个提醒对象 * @param item 表示提醒事项的对象,包含闹钟的详细信息。 * @returns 返回值类型为 reminderAgent.ReminderRequestAlarm。 */ private initReminder(item: ReminderItem): reminderAgent.ReminderRequestAlarm { return { reminderType: item.remindType, //设置提醒的类型,使用了 item 对象中的 remindType 属性。 hour: item.hour, //设置提醒的小时数,使用了 item 对象中的 hour 属性。 minute: item.minute, //设置提醒的分钟数,使用了 item 对象中的 minute 属性。 daysOfWeek: item.repeatDays, //设置提醒的重复日期,使用了 item 对象中的 repeatDays 属性。 title: item.name, //设置提醒的标题,使用了 item 对象中的 name 属性。 ringDuration: item.duration * CommonConstants.DEFAULT_TOTAL_MINUTE, //设置提醒的铃声持续时间,使用了 item 对象中的 duration 属性,并乘以一个常量60。 snoozeTimes: item.intervalTimes,//设置提醒的贪睡次数,使用了 item 对象中的 intervalTimes 属性。 timeInterval: item.intervalMinute * CommonConstants.DEFAULT_TOTAL_MINUTE,//设置提醒的时间间隔,使用了 item 对象中的 intervalMinute 属性,并乘以一个常量60。 actionButton: [ //设置提醒的操作按钮,是一个数组。 { title: '关闭', type: reminderAgent.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE }, { title: '稍后提醒', type: reminderAgent.ActionButtonType.ACTION_BUTTON_TYPE_SNOOZE }, ], wantAgent: { //设置提醒的代理信息 pkgName: CommonConstants.BUNDLE_NAME, //设置包名,使用了一个常量。 abilityName: CommonConstants.ABILITY_NAME //设置能力名,使用了一个常量EntryAbility。 }, notificationId: item.notificationId,//设置提醒的通知 ID,使用了 item 对象中的 notificationId 属性。 expiredContent: '此提醒已过期',//设置提醒过期时的内容。 snoozeContent: '待会儿提醒', //提醒暂停时要显示的内容 slotType: notification.SlotType.SOCIAL_COMMUNICATION //社交类型的 NotificationSlot。 } } }
4.2.创建子包database
①.PreferencesHandler.ets:
用户首选项(Preferences)相关的功能,包括数据的读取、创建、修改等。代码如下:
import data_preferences from '@ohos.data.preferences'; import { CommonConstants } from '../../common/constants/CommonConstants'; import PreferencesListener from './PreferencesListener'; /** * 首选项设置 */ export default class PreferencesHandler { static instance: PreferencesHandler = new PreferencesHandler(); // 静态实例,用于全局访问 private preferences: data_preferences.Preferences | null = null; // 存储首选项设置的引用 private defaultValue = ''; // 默认值 private listeners: PreferencesListener[]; // 首选项设置监听器数组 private constructor() { this.listeners = new Array(); // 初始化偏好设置监听器数组 } /** * 配置 PreferencesHandler。 * @param context Context 指示应用程序或功能的上下文,就是UIAbility */ public async configure(context: Context) { /** * 获取Preferences实例,赋值给this.preferences * context - 指示应用程序或功能的上下文,就是UIAbility * CommonConstants.PREFERENCE_ID - 表示首选项文件名。这个自定义即可 */ this.preferences = await data_preferences.getPreferences(context, CommonConstants.PREFERENCE_ID); /** * this.preferences.on 订阅数据变更,订阅的Key的值发生变更后,在执行flush方法后,触发callback回调。 * change: 事件类型,固定值'change',表示数据变更。 * (data: Record<string, Object>) 回调对象实例。 * * 通用的类型Record,用于构造具有一组指定属性名和相同属性值类型的新类型。这在 TypeScript 中可以用于创建通用的数据结构类型。 */ this.preferences.on('change', (data: Record<string, Object>) => {// 监听首选项设置变化 for (let preferencesListener of this.listeners) { preferencesListener.onDataChanged(data.key as string);// 通知首选项设置监听器数据变化 } }); } /** * 在 首选项 中添加数据。 * * @param key 新建的首选项key * @param value key对应的值 */ public async set(key: string, value: string) { //判断,如果首选项文件存在,则创建 if (this.preferences != null) { await this.preferences.put(key, value); // 添加首选项数据 await this.preferences.flush(); // 将当前Preferences实例的数据异步存储到用户首选项的持久化文件中,写入后需要调用 } } /** * 在 首选项 根据key获取数据。 * * @param key 首选项的key * @param defValue 获取键对应的值,如果值为null或者非默认值类型,返回默认数据defValue * @return data 返回 */ public async get(key: string) { //定义变量 let data: string = ''; //判断,如果首选项文件存在,则进行查询 if (this.preferences != null) { //在首选项中查询出来数据,赋值给变量data data = await this.preferences.get(key, this.defaultValue) as string; } return data; } /** * 清除此Preferences实例中存储的所有数据 */ public clear() { //判断,如果首选项文件存在,则进行清除 if (this.preferences != null) { this.preferences.clear(); // 清除首选项中所有数据 } } /** * 添加首选项侦听器 * @param listener PreferencesListener接口 */ public addPreferencesListener(listener: PreferencesListener) { // 添加首选项监听器到数组中 this.listeners.push(listener); } }
②.PreferencesListener.ets:
用于首选项数据变化的功能,当首选项发生变化时进行相应的处理。
/** * 首选项监听器 */ export default interface PreferencesListener { /** * 接口中的一个属性,它是一个函数类型的属性。定义了一个名为onDataChanged的方法,该方法接受一个字符串类型的参数key,并且没有返回值(void)。 * @param key string */ onDataChanged: (key: string) => void }
五、view
"view" 目录通常用于存放与用户界面相关的代码,包括界面布局、UI组件、样式等。
5.1.BackContainer.ets
用于实现新建闹钟界面返回按钮和标题的组件:
import router from '@ohos.router'; import { CommonConstants } from '../common/constants/CommonConstants'; import DimensionUtil from '../common/utils/DimensionUtil'; /** * 创建一个包含返回按钮和标题的组件 */ @Component export default struct BackContainer { // 定义了一个私有属性 header,它可以是字符串或资源。默认值为从资源文件中获取的字符串 "new_alarm"。 private header: string | Resource = $r('app.string.new_alarm'); //定义了一个私有属性 backImgRes,它可以是字符串或资源。默认值为从资源文件中获取的图像资源 "ic_cancel"。 private backImgRes: string | Resource = $r('app.media.ic_cancel'); // 定义了一个私有属性 backFunc,它是一个可选的函数类型,用于处理返回操作 private backFunc?: () => void; /** * 当开发者创建了自定义组件,并想对该组件添加特定功能时,例如在自定义组件中添加一个点击跳转操作。 * 若直接在组件内嵌入事件方法,将会导致所有引入该自定义组件的地方均增加了该功能。 * 为解决此问题,ArkUI引入了@BuilderParam装饰器,@BuilderParam用来装饰指向@Builder方法的变量, * 开发者可在初始化自定义组件时对此属性进行赋值,为自定义组件增加特定的功能。该装饰器用于声明任意UI描述的一个元素,类似slot占位符。 */ @BuilderParam closer?: () => void; build() { Row() { //如果this.backImgRes为null 则显示<--否则显示×, ImageFit.Fill并设置了图像的填充方式为 Fill Button() { Image(this.backImgRes == null ? $r('app.media.ic_public_back') : this.backImgRes).objectFit(ImageFit.Fill) } .backgroundColor($r('app.color.trans_parent')) //背景颜色 .width(DimensionUtil.getVp($r('app.float.title_button_size'))) //设置宽 .height(DimensionUtil.getVp($r('app.float.title_button_size'))) //设置高 .onClick(() => { //如果 backFunc 存在,则调用 backFunc(),否则执行 router.back()。 this.backFunc ? this.backFunc() : router.back(); }) //新建闹钟 Text(this.header) .fontSize(DimensionUtil.getFp($r('app.float.detail_title_font_size'))) //设置字体大小 .lineHeight(DimensionUtil.getVp($r('app.float.title_line_height'))) //设置文本的文本行高 .margin({ left: DimensionUtil.getVp($r('app.float.title_margin')) }) //外边距 .fontColor($r('app.color.grey_divider')) //字体颜色 .fontWeight(FontWeight.Bold) //字体加粗 Blank() //空白填充 //如果 closer 存在,则执行 closer() if (this.closer) { this.closer(); } } .padding({ left: DimensionUtil.getVp($r('app.float.title_horizon_margin')), right: DimensionUtil.getVp($r('app.float.title_horizon_margin')) }) .height(DimensionUtil.getVp($r('app.float.page_title_height'))) .width(CommonConstants.FULL_LENGTH) } }
5.2.子包Main
①.AlarmListItem.ets
用于定义创建的单个闹钟项的在主页的展示,代码如下:
import MainModel from './../../viewmodel/MainViewModel'; import { CommonConstants } from '../../common/constants/CommonConstants'; import AlarmItem from '../../viewmodel/AlarmItem'; import DimensionUtil from '../../common/utils/DimensionUtil'; @Component export default struct AlarmListItem { private mainModel: MainModel = MainModel.instant; //接收传递过来的闹钟信息 private alarmItem: AlarmItem = new AlarmItem(); build() { Row() { Column() { Row() { // 显示上午或者下午 Text(this.mainModel.getNoonContent(this.alarmItem.hour)) .CommonTextAttr(DimensionUtil.getFp($r('app.float.alarms_item_noon_font_size')), FontWeight.Regular) //03:46 Text(this.mainModel.getTaskTimeContent(this.alarmItem.hour, this.alarmItem.minute)) .CommonTextAttr(DimensionUtil.getFp($r('app.float.alarms_item_time_font_size')), FontWeight.Regular, { left: DimensionUtil.getVp($r('app.float.alarms_item_time_margin_left')) }) } // 闹钟、不重复 Text(this.mainModel.getDescContent(this.alarmItem)) .CommonTextAttr(DimensionUtil.getFp($r('app.float.alarms_item_dec_font_size')), FontWeight.Normal, { top: DimensionUtil.getVp($r('app.float.alarms_item_dec_margin_top')) }, $r('app.float.alarms_item_desc_text_opacity')) } .width(CommonConstants.FULL_LENGTH) //宽度 .alignItems(HorizontalAlign.Start) //在水平方向上设置子组件的对齐格式 .layoutWeight(CommonConstants.DEFAULT_LAYOUT_WEIGHT)//权重占比 //设置开关,默认创建开关是打开的 Toggle({ type: ToggleType.Switch, isOn: this.alarmItem.isOpen }) .onChange((isOn: boolean) => {//关闭或者打开闹钟 // this.mainModel.openAlarm(this.alarmItem.id, isOn); }) .width(DimensionUtil.getVp($r('app.float.alarms_item_toggle_width'))) //宽度 .height(DimensionUtil.getVp($r('app.float.alarms_item_toggle_height'))) //高度 } .padding({ left: DimensionUtil.getVp($r('app.float.alarm_list_content_distance')), right: DimensionUtil.getVp($r('app.float.alarm_list_content_distance')) }) .width(CommonConstants.FULL_LENGTH) .height(DimensionUtil.getVp($r('app.float.alarms_item_height'))) .backgroundColor(Color.White) .borderRadius(DimensionUtil.getVp($r('app.float.alarms_item_radius'))) } } @Extend(Text) function CommonTextAttr(fontSize: number, fontWeight: number, margin?: Margin, opacity?: Resource) { .fontColor($r('app.color.grey_divider')) .fontSize(fontSize) .fontWeight(fontWeight) .margin(margin != undefined ? margin : 0) .opacity(opacity != undefined ? opacity : 1) }
②AlarmList.ets
显示创建的多个闹钟信息,代码如下:
import router from '@ohos.router'; import { CommonConstants } from '../../common/constants/CommonConstants'; import AlarmItem from '../../viewmodel/AlarmItem'; import AlarmListItem from '../Main/AlarmListItem'; import DimensionUtil from '../../common/utils/DimensionUtil'; @Component export default struct AlarmList { // 添加的多个闹钟信息 @Link alarmItems: Array<AlarmItem>; build() { //使用list容器,显示添加的多个闹钟信息 List({ space: DimensionUtil.getVp($r('app.float.alarm_list_space')) }) { //循环遍历渲染多个闹钟信息 ForEach(this.alarmItems, (item: AlarmItem) => { ListItem() { //渲染闹钟信息 AlarmListItem({ alarmItem: item }) }.onClick(() => { //点击闹钟卡片,进入到具体的闹钟信息页面,需要将本次循环的闹钟信息item作为参数传递过去 router.pushUrl({ url: 'pages/DetailIndex', params: { alarmItem: item } }); }) }, (item: AlarmItem) => JSON.stringify(item)) //ForEach 方法的第二个参数是一个函数,用于将每个元素转换为字符串形式。 } .padding({//设置左右内边距 left: DimensionUtil.getVp($r('app.float.alarm_list_content_distance')), right: DimensionUtil.getVp($r('app.float.alarm_list_content_distance')) }) .listDirection(Axis.Vertical) //调用了 listDirection 方法,设置列表的方向为垂直方向。 .layoutWeight(CommonConstants.DEFAULT_LAYOUT_WEIGHT) //设置布局权重为默认的布局权重。 .margin({ top: DimensionUtil.getVp($r('app.float.alarm_list_content_distance')) }) //设置上外边距 } }
③.ClockArea.ets
用于显示时钟表盘区域的界面绘制布局,代码如下:
import { CommonConstants } from '../../common/constants/CommonConstants'; import MainModel from '../../viewmodel/MainViewModel'; import { MainConstant } from '../../common/constants/MainConstant'; import DimensionUtil from '../../common/utils/DimensionUtil'; @Component // 声明组件 export default struct ClockArea { // 定义名为 ClockArea 的结构体 private mainModel: MainModel = MainModel.instant; // 实例化 MainModel 对象,用于处理时钟逻辑 private drawInterval: number = CommonConstants.DEFAULT_NUMBER_NEGATIVE; // 初始化绘制间隔时间为默认负数 @State showClock: boolean = true; // 定义状态变量,控制时钟显示与隐藏 private canvasSize: number = DimensionUtil.getVp($r('app.float.clock_size')); // 获取画布尺寸,根据设计稿中定义的时钟大小进行适配 private clockRadius: number = this.canvasSize / CommonConstants.DEFAULT_DOUBLE - CommonConstants.DEFAULT_DOUBLE; // 计算时钟半径 private settings: RenderingContextSettings = new RenderingContextSettings(true); // 创建渲染上下文设置对象,用于绘制时钟 private renderContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); // 创建 Canvas 渲染上下文对象,用于绘制时钟图形 aboutToDisappear() { /** * clearInterval(this.drawInterval);: 这行代码清除了之前设置的定时器,即停止了之前启动的周期性执行任务。 * this.drawInterval 是之前设置的定时器的标识符。 * clearInterval 是 JavaScript 中用于清除定时器的函数。 */ clearInterval(this.drawInterval); } build() { // 定义 build 方法,用于构建组件 Canvas(this.renderContext) // 创建 Canvas 对象,传入渲染上下文 .width(this.canvasSize) // 设置画布宽度 .aspectRatio(CommonConstants.DEFAULT_LAYOUT_WEIGHT) // 设置画布宽高比 .onReady(() => { // 在画布准备就绪时执行回调函数 // 如果绘制间隔时间为-1,则开始绘制时钟 if (this.drawInterval === CommonConstants.DEFAULT_NUMBER_NEGATIVE) { this.startDrawTask(); // 开始绘制时钟任务 } }) .onClick(() => { // 点击画布时执行的回调函数 this.showClock = !this.showClock; // 切换时钟显示状态 }) } /** * 启动绘画任务 */ private startDrawTask() { this.renderContext.translate( //将原点移到画布中心 this.canvasSize / CommonConstants.DEFAULT_DOUBLE, //取画布尺寸的一半作为x坐标 this.canvasSize / CommonConstants.DEFAULT_DOUBLE); //取画布尺寸的一半作为y坐标 this.drawClockArea(); // 绘制时钟区域 this.drawInterval = setInterval(() => { // 设置定时器,每隔一秒重新绘制时钟区域 this.drawClockArea(); }, MainConstant.DEFAULT_ONE_SECOND_MS); } /** * 开始绘制时钟区域 */ private drawClockArea(): void { this.renderContext.clearRect( // 清空画布,删除指定区域内的绘制内容。 -this.canvasSize, //指定矩形上的左上角x坐标 -this.canvasSize / CommonConstants.DEFAULT_DOUBLE, //指定矩形上的左上角y坐标 this.canvasSize * CommonConstants.DEFAULT_DOUBLE, //指定矩形的宽度, 取画布尺寸的一半 this.canvasSize); //指定矩形的高度 取画布尺寸 let date = new Date();//创建一个新的 Date 对象以获取当前日期和时间 let hours = date.getHours(); //获取时钟数 let minutes = date.getMinutes();//获取分钟数 let seconds = date.getSeconds();//获取秒数 if (this.showClock) { // 如果为true则时钟显示出来 this.drawPan(); // 绘制表盘 /** * 计算方法如下: * 一个小时是60分钟,所以钟面上是60小格,分针每走一格是1分钟。 * 时针走5小格就是60分钟,所以走1小格是12分钟。 */ /** * 根据当前时间绘制时针, 时针的角度计算如下:30 *(hours>12? hours-12:hours)+ minutes / 12 * 6 * 如果当前小时数大于 12,则使用 hours - 12,否则使用当前小时数。 * 乘以 CommonConstants.DEFAULT_INTERVAL_MINUTE_MAX(12),这是每小时的角度。 * 加上分钟数的一部分,以考虑分钟对时针的影响。 * 最后,使用 MainConstant.HOUR_POINTER_IMAGE_URL 来绘制时针的图像。 */ this.drawPointer( CommonConstants.DEFAULT_INTERVAL_MINUTE_MAX * (hours > CommonConstants.DEFAULT_TOTAL_HOUR ? hours - CommonConstants.DEFAULT_TOTAL_HOUR : hours) + minutes / CommonConstants.DEFAULT_TOTAL_HOUR * CommonConstants.DEFAULT_COMMON_DEGREE , MainConstant.HOUR_POINTER_IMAGE_URL); /** * 绘制分针: * 分针的角度直接使用分钟数乘以 CommonConstants.DEFAULT_COMMON_DEGREE,这是每分钟的角度。 * 使用 MainConstant.MINUTE_POINTER_IMAGE_URL(6) 来绘制分针的图像。 */ this.drawPointer(CommonConstants.DEFAULT_COMMON_DEGREE * minutes, MainConstant.MINUTE_POINTER_IMAGE_URL); /** * 绘制秒针。 * 秒针的角度直接使用秒数乘以 CommonConstants.DEFAULT_COMMON_DEGREE(6),这是每秒的角度。 * 使用 MainConstant.SECOND_POINTER_IMAGE_URL 来绘制秒针的图像。 */ this.drawPointer(CommonConstants.DEFAULT_COMMON_DEGREE * seconds, MainConstant.SECOND_POINTER_IMAGE_URL); } else { // 如果false则时钟隐藏 this.drawTime(hours, minutes, seconds); //以文本形式显示完整时间(小时、分钟和秒钟) } } // 绘制表盘 private drawPan(): void { //创建一个新的绘制路径 this.renderContext.beginPath(); // 加载表盘图片,ImageBitmap对象可以存储canvas渲染的像素数据。 let secondImg = new ImageBitmap(MainConstant.CLOCK_PAN_IMAGE_URL); //使用 时钟半径*2 为表盘宽度 let imgWidth = this.clockRadius * 2; this.renderContext.drawImage(secondImg, -this.clockRadius, -this.clockRadius, imgWidth, imgWidth); // 绘制表盘图片 this.renderContext.restore(); //对保存的绘图上下文进行恢复。 } /** * 封装绘制时针、分针、秒针的方法 * @param degree * @param pointerImgRes */ private drawPointer(degree: number, pointerImgRes: string) { //保存当前绘制状态,以便稍后恢复 this.renderContext.save(); //计算旋转角度,将角度转换为弧度 let theta = (degree + MainConstant.DEFAULT_HORIZONTAL_ANGLE) * Math.PI / MainConstant.DEFAULT_HORIZONTAL_ANGLE; //旋转坐标系,使指针按照计算的角度旋转。 this.renderContext.rotate(theta); //开始新的路径绘制 this.renderContext.beginPath(); //加载指针图片 let secondImg = new ImageBitmap(pointerImgRes); // 获取指针图片的宽度 let imgWidth = CommonConstants.DEFAULT_POINTER_WIDTH; /** * this.renderContext.drawImage(...): 绘制指针图片。 * 指定图片的左上角 x 坐标为 -imgWidth / CommonConstants.DEFAULT_DOUBLE。 * 指定图片的左上角 y 坐标为 -this.clockRadius。 * 指定图片的宽度为 imgWidth。 * 指定图片的高度为 this.clockRadius * CommonConstants.DEFAULT_DOUBLE。 */ this.renderContext.drawImage( // 绘制指针图片 secondImg, -imgWidth / CommonConstants.DEFAULT_DOUBLE, -this.clockRadius, imgWidth, this.clockRadius * CommonConstants.DEFAULT_DOUBLE); //恢复之前保存的绘制状态。 this.renderContext.restore(); } /** * 画布上绘制格式化的时间字符串,通常用于显示完整的时间(小时、分钟和秒钟) * @param hour 小时 * @param minute 分钟 * @param second 秒钟 */ private drawTime(hour: number, minute: number, second: number): void { //根据传入的小时、分钟和秒钟,格式化成字符串形式的时间,例如 "12:34:56"。 let time = this.mainModel.fillZero(hour) // 格式化小时,补零 + MainConstant.DEFAULT_STRING_COLON //':' + this.mainModel.fillZero(minute) // 格式化分钟,补零 + MainConstant.DEFAULT_STRING_COLON //':' + this.mainModel.fillZero(second); // 格式化秒钟,补零 //保存当前绘制状态,以便稍后恢复 this.renderContext.save(); //设置绘制文本的字体大小,使用从配置文件中获取的字体大小 MainConstant.CLOCK_TIME_FONT_SIZE_UNIT为px this.renderContext.font = DimensionUtil.getPx($r('app.float.clock_time_font_size')) + MainConstant.CLOCK_TIME_FONT_SIZE_UNIT; //开始新的路径绘制 this.renderContext.beginPath(); //设置文本对齐方式为居中 this.renderContext.textAlign = 'center'; // 绘制时间字符串,将其居中绘制在坐标 (0, 0) 处 this.renderContext.fillText(time, 0, 0); //恢复之前保存的绘制状态 this.renderContext.restore(); } }
5.3.Detail
①.DatePickArea.ets
用于选择日期的界面逻辑。包含日期选择器的UI布局、日期选择的交互逻辑等。代码如下:
import { DetailConstant } from '../../common/constants/DetailConstant'; import { CommonConstants } from '../../common/constants/CommonConstants'; import DimensionUtil from '../../common/utils/DimensionUtil'; import DayDataItem from '../../viewmodel/DayDataItem'; /** * 构建一个日期选择区域 */ @Component export default struct DatePickArea { build() { //创建一个堆叠布局,其中的内容将在垂直方向上居中对齐 Stack({ alignContent: Alignment.Center }) { Row() { //遍历DetailConstant.DAY_DATA数组中的每个元素,执行后面的箭头函数。第二个箭头函数用于将每个元素转换为JSON字符串。 ForEach(DetailConstant.DAY_DATA, (item: DayDataItem) => { //创建一个文本选择器,其中range属性指定了可选的文本范围,selected属性指定了当前选择的文本索引。 TextPicker({ range: item.data, selected: item.delSelect }) .layoutWeight(CommonConstants.DEFAULT_LAYOUT_WEIGHT) //子元素与兄弟元素占主轴尺寸按照权重进行分配 .backgroundColor($r('app.color.grey_light')) //设置背景 .onChange((value: string, index: number) => { /** * 滑动选中TextPicker文本内容后,触发该回调。 * - value: 当前选中项的文本。 * - index: 当前选中项的索引值。 */ item.delSelect = index;//修改 }) }, (item: DayDataItem) => JSON.stringify(item)) } } .height(DimensionUtil.getVp($r('app.float.date_picker_height'))) .padding({ left: DimensionUtil.getVp($r('app.float.date_picker_padding_horizon')), right: DimensionUtil.getVp($r('app.float.date_picker_padding_horizon')) }) } }
②.SettingItem.ets:
用于实现重复、闹钟名、闹铃时长、再响间隔功能的组件,
代码如下:
import { CommonConstants } from '../../common/constants/CommonConstants'; import { AlarmSettingType } from '../../common/constants/AlarmSettingType'; import AlarmSettingItem from '../../viewmodel/AlarmSettingItem'; import DimensionUtil from '../../common/utils/DimensionUtil'; import IntervalDialog from './dialog/IntervalDialog'; import DurationDialog from './dialog/DurationDialog'; import RenameDialog from './dialog/RenameDialog'; import RepeatDialog from './dialog/RepeatDialog'; @Component export default struct SettingItem { @Link settingInfo: Array<AlarmSettingItem>; /** * 重复日期选择组件 * CustomDialogController:自定义对话框控制器 */ repeatDialogController: CustomDialogController = new CustomDialogController({ builder: RepeatDialog(), //重复日期选择组件 autoCancel: true //定义在单击对话框外部时是否使用自动取消 }); /** * 闹钟名组件 * CustomDialogController:自定义对话框控制器 */ reNameDialogController: CustomDialogController = new CustomDialogController({ builder: RenameDialog(),//自定义闹钟名弹窗 autoCancel: true //定义在单击对话框外部时是否使用自动取消 }); /** * 闹钟时长设置组件 * CustomDialogController:自定义对话框控制器 */ durationDialogController: CustomDialogController = new CustomDialogController({ builder: DurationDialog(), autoCancel: true }); /** * 再响间隔组件 * CustomDialogController:自定义对话框控制器 */ intervalDialogController: CustomDialogController = new CustomDialogController({ builder: IntervalDialog(), autoCancel: true }); //判断显示的是:重复、闹钟名、闹钟时长、再响间隔 private showSettingDialog(sType: AlarmSettingType) { switch (sType) { case AlarmSettingType.REPEAT: //重复 this.repeatDialogController.open(); //open:显示自定义弹窗内容,若已显示,则不生效。 break; case AlarmSettingType.ALARM_NAME://闹钟名 this.reNameDialogController.open();//open:显示自定义弹窗内容,若已显示,则不生效。 break; case AlarmSettingType.RING_DURATION://闹钟时长 this.durationDialogController.open();//open:显示自定义弹窗内容,若已显示,则不生效。 break; case AlarmSettingType.INTERVAL://再响间隔 this.intervalDialogController.open();//open:显示自定义弹窗内容,若已显示,则不生效。 break; default: break } } build() { Column() { ForEach(this.settingInfo, (item: AlarmSettingItem, index: number | undefined) => { Divider() //提供分隔器组件,分隔不同内容块/内容元素。 .visibility(index === 0 ? Visibility.Hidden : Visibility.Visible)//根据索引设置分隔器的可见性。如果索引为 0,则分隔器隐藏;否则可见。 .opacity($r('app.float.divider_opacity')) //使用从配置变量中获取的值设置分隔器的不透明度(透明度) .color($r('app.color.grey_divider')) //背景颜色 .lineCap(LineCapStyle.Round) //将线帽(分隔器线的末端)的样式设置为圆形。 .margin({ left: DimensionUtil.getVp($r('app.float.setting_item_divider_margin_horizon')), right: DimensionUtil.getVp($r('app.float.setting_item_divider_margin_horizon')) }) Row() { Text(item.title) //显示设置项的标题 .fontSize(DimensionUtil.getFp($r('app.float.setting_item_name_font_size'))) //设置字体大小 .fontWeight(FontWeight.Regular) //粗细 .fontColor($r('app.color.grey_divider')) //颜色 .layoutWeight(CommonConstants.DEFAULT_LAYOUT_WEIGHT) //权重比 Text(item.content) //显示设置项的内容 .fontSize(DimensionUtil.getFp($r('app.float.setting_item_content_font_size')))//设置字体大小 .fontWeight(FontWeight.Normal)//粗细 .fontColor($r('app.color.grey_divider'))//颜色 .opacity($r('app.float.setting_item_content_font_opacity')) //通过设置透明比率来让PixelMap达到对应的透明效果 Image($r('app.media.ic_right')) //显示图像(箭头图标),表示有更多详细信息或导航。点击可以查看 .width(DimensionUtil.getVp($r('app.float.setting_item_image_width'))) //宽 .height(DimensionUtil.getVp($r('app.float.setting_item_image_height')))//高 .objectFit(ImageFit.Fill) //填充 .margin({ left: DimensionUtil.getVp($r('app.float.setting_item_image_margin_horizon')) }) } .height(DimensionUtil.getVp($r('app.float.setting_item_height'))) //高度 .alignItems(VerticalAlign.Center) //纵向居中 .padding({ //设置内边距 left: DimensionUtil.getVp($r('app.float.setting_item_interval')), right: DimensionUtil.getVp($r('app.float.setting_item_interval')) }) .onClick(() => { //点击行时要执行的操作 this.showSettingDialog(item.sType); }) }, (item: AlarmSettingItem, index: number | undefined) => JSON.stringify(item) + index) } .margin({ bottom: DimensionUtil.getVp($r('app.float.setting_item_interval')), left: DimensionUtil.getVp($r('app.float.setting_item_interval')), right: DimensionUtil.getVp($r('app.float.setting_item_interval')) }) .borderRadius(DimensionUtil.getVp($r('app.float.setting_item_border_radius'))) .backgroundColor(Color.White) } }
③.dialog包
1)CommonDialog.ets:实现弹窗,有名称、取消和确定按钮
代码实现如下:
import { CommonConstants } from '../../../common/constants/CommonConstants'; import DimensionUtil from '../../../common/utils/DimensionUtil'; @Component export default struct CommonDialog { //标题 private title?: string | Resource; //自定义对话框控制器 private controller?: CustomDialogController; //声明了一个私有属性onConfirm,它是一个函数,初始值为空函数。 private onConfirm: () => void = () => { }; //声明了一个名为closer的构建器参数,它是一个函数,初始值为空函数 @BuilderParam closer: () => void = () => { }; build() { Column() { //显示对话框的标题文本 Text(this.title) .fontSize(DimensionUtil.getFp($r('app.float.common_dialog_title_font_size'))) //字体 .width(CommonConstants.FULL_LENGTH) //宽度 .fontColor($r('app.color.grey_divider')) //字体颜色 .margin({ //外边距 bottom: DimensionUtil.getVp($r('app.float.common_dialog_title_margin_vertical')) }) //调用closer函数 this.closer() Row() {//取消、确定按钮 //设置 取消 按钮 Button($r('app.string.cancel')).actionBtnStyle().onClick(() => { if (!this.controller) { //如果 自定义对话框控制器 不存在,则之间返回 return; } this.controller.close();//关闭自定义弹窗 }) //如果存在onConfirm函数,创建一个确认按钮,设置了样式和点击事件 if (this.onConfirm) { //确定按钮 Button($r('app.string.confirm')).actionBtnStyle().onClick(() => { this.onConfirm(); //调用函数 if (!this.controller) {// 如果 自定义对话框控制器 不存在,则之间返回 return; } this.controller.close();//点击确定,关闭自定义弹窗 }) } } .margin({ top: DimensionUtil.getVp($r('app.float.common_dialog_margin_vertical')) }) //外边距 } .width(CommonConstants.FULL_LENGTH) //宽度 .padding(DimensionUtil.getVp($r('app.float.common_dialog_padding'))) .justifyContent(FlexAlign.Center) //水平方向 居中显示 } }
2)RepeatDialog.ets:
用于设置重复的对话框界面逻辑。如下图:
实现代码如下:
import { CommonConstants } from '../../../common/constants/CommonConstants'; import { DetailConstant } from '../../../common/constants/DetailConstant'; import AlarmItem from '../../../viewmodel/AlarmItem'; import DetailModel from '../../../viewmodel/DetailViewModel'; import CommonDialog from './CommonDialog'; import DataTypeUtils from '../../../common/utils/DataTypeUtils'; import DimensionUtil from '../../../common/utils/DimensionUtil'; @CustomDialog //使用@CustomDialog装饰器装饰自定义弹窗 export default struct RepeatDialog { //使用了@Consume装饰器,将名为alarmItem的AlarmItem(闹钟)对象注入到组件中 @Consume(DetailConstant.DEFAULT_PROVIDER_KEY) alarmItem: AlarmItem; //DetailModel对象,初始值为DetailModel.instant private viewModel: DetailModel = DetailModel.instant; //数字数组,用于存储选中的重复日期 private selects: number[] = []; //自定义对话框控制器,通过CustomDialogController的构造函数进行初始化。 controller: CustomDialogController = new CustomDialogController({ builder: RepeatDialog(), autoCancel: true }); //在对话框即将显示时执行 aboutToAppear() { //将重复的天数加载赋值给selects this.selects = DataTypeUtils.deepCopy(this.alarmItem.repeatDays); } build() { Flex() { CommonDialog({ title: $r('app.string.repeat_name'),//标题 controller: this.controller, //自定义对话框控制器 onConfirm: () => { this.selects.sort();//对包含在此对象中的所有键值对进行排序 this.alarmItem.repeatDays = this.selects; //闹钟是否重复,如果重复天数大于0,则表示重复,给this.alarmItem.isRepeat赋值true this.alarmItem.isRepeat = this.selects.length > 0; } }) { //遍历星期天数 ForEach(DetailConstant.WEEKDAY_DATA, (item: number) => { Row() { //显示周一、周二、周三等 Text(this.viewModel.transAlarmRepeatDayContent(item)) .layoutWeight(CommonConstants.DEFAULT_LAYOUT_WEIGHT) .fontColor($r('app.color.grey_divider')) //字体颜色 .fontSize(DimensionUtil.getFp($r('app.float.repeat_dialog_check_font_size'))) //字体大小 //勾选按钮 Checkbox({ name: item.toString() }) //设置多选框是否选中, 传入一个布尔值 //indexOf:查找当前对象中第一次出现item的索引,如果不包含value,则为-1 , CommonConstants.DEFAULT_NUMBER_NEGATIVE-1 .select(this.alarmItem.repeatDays.indexOf(item) !== CommonConstants.DEFAULT_NUMBER_NEGATIVE) .width(DimensionUtil.getVp($r('app.float.repeat_dialog_check_box_size'))) //宽度 .height(DimensionUtil.getVp($r('app.float.repeat_dialog_check_box_size'))) //高度 .onChange((value: boolean) => { if (value) {//value为true时,表示已选中。 this.selects.push(item);//如果已经选中,将选中的日期添加到selects数组中 } else { //如果没有选中,查找selects中第一次出现item的索引,如果不包含value,则为-1 , let index = this.selects.indexOf(item); //从数组中删除元素,必要时插入新元素,并返回删除的元素 this.selects.splice(index, CommonConstants.DEFAULT_SINGLE); } }) } .width(CommonConstants.FULL_LENGTH) //分割线 Divider() .opacity($r('app.float.divider_opacity')) // 通过设置透明比率来让PixelMap达到对应的透明效果 .color($r('app.color.grey_divider')) //分割线的颜色 .lineCap(LineCapStyle.Round) //设置分割线的结尾样式。默认值为 Butt。LineCapStyle.Round分界线的两端是半圆形 }) } } } }
3)RenameDialog.ets:
用于重命名的对话框的实现:
实现代码如下:
import { CommonConstants } from '../../../common/constants/CommonConstants'; import { DetailConstant } from '../../../common/constants/DetailConstant'; import AlarmItem from '../../../viewmodel/AlarmItem'; import CommonDialog from './CommonDialog'; import DimensionUtil from '../../../common/utils/DimensionUtil'; @CustomDialog //使用@CustomDialog装饰器装饰自定义弹窗 export default struct RenameDialog { // 从默认提供程序中闹钟设置信息 @Consume(DetailConstant.DEFAULT_PROVIDER_KEY) alarmItem: AlarmItem; // 初始化 闹钟名称属性 private name: string = ''; // 创建自定义对话框控制器 controller: CustomDialogController = new CustomDialogController({ builder: RenameDialog(), // 指定对话框构建器 autoCancel: true // 在需要时自动取消对话框 }); // 构建对话框 UI build() { Flex() { CommonDialog({ title: $r('app.string.alarm_name'), // 设置标题:闹钟名 controller: this.controller, // 关联控制器 onConfirm: () => { // 在确认时更新闹钟名称 this.alarmItem.name = this.name; } }) { TextArea({ text: this.alarmItem.name }) // 显示闹钟名称 .width(CommonConstants.FULL_LENGTH) // 设置宽度 .margin({ bottom: DimensionUtil.getVp($r('app.float.rename_dialog_text_margin_bottom')) }) // 设置底部边距 .onChange((value: string) => {//value:当前输入的文本内容。 // 将输入的内容,赋值给name this.name = value; }); Divider() .opacity($r('app.float.divider_opacity')) // 设置分隔线透明度 .color($r('app.color.grey_divider')) // 设置分隔线颜色 .lineCap(LineCapStyle.Round); // 设置分隔线线帽样式 } } } }
3)IntervalDialog.ets:
用于选择再响间隔的对话框界面,如下:
实现代码如下:
import { CommonConstants } from '../../../common/constants/CommonConstants'; import { DetailConstant } from '../../../common/constants/DetailConstant'; import AlarmItem from '../../../viewmodel/AlarmItem'; import CommonDialog from './CommonDialog'; import DimensionUtil from '../../../common/utils/DimensionUtil'; // 导入自定义弹窗装饰器 @CustomDialog export default struct IntervalDialog { // 使用Consume装饰器注入一个名为alarmItem的AlarmItem(闹钟)实例 @Consume(DetailConstant.DEFAULT_PROVIDER_KEY) alarmItem: AlarmItem; // 用于存储选择的时间间隔的分钟数,默认为0 @State intervalMinuteSelect: number = 0; // 重复响应次数,默认为0 @State intervalTimesSelect: number = 0; // 弹窗控制器 controller?: CustomDialogController; // 弹窗即将出现时的回调函数,用于初始化选择的时间间隔,这是为了点击进入后,加载已有的设置 aboutToAppear(): void{ //获取闹钟时间,赋值给响铃间隔时间 this.intervalMinuteSelect = this.alarmItem.intervalMinute; //从闹钟信息获取获取重复响应次数,赋值给响铃间隔次数 this.intervalTimesSelect = this.alarmItem.intervalTimes; } // 构建弹窗内容的方法 build() { // 使用Flex布局 Flex() { // 如果时间间隔选择大于0 if ((this.intervalTimesSelect | this.intervalMinuteSelect) > 0) { // 创建通用弹窗,设置标题和确认按钮回调 CommonDialog({ title: $r('app.string.ring_interval'), //显示 "再响间隔" controller: this.controller,//弹窗控制器 onConfirm: () => { // 更新alarmItem的时间间隔 this.alarmItem.intervalMinute = Number(this.intervalMinuteSelect.toFixed(0)) this.alarmItem.intervalTimes = Number(this.intervalTimesSelect.toFixed(0)) } }) { // 构建弹窗内容 // 显示闹钟时间间隔的标题和滑动条,app.string.ring_interval_content 响铃间隔时间(分钟) this.IntervalItem($r('app.string.ring_interval_content'), 0) // 添加分隔线 Divider() .opacity($r('app.float.divider_opacity')) //// 设置分隔线透明度 .color($r('app.color.grey_divider')) //颜色 .lineCap(LineCapStyle.Round) // 设置分隔线线帽样式 // 显示时间间隔次数的标题和滑动条 this.IntervalItem($r('app.string.ring_times_content'), CommonConstants.DEFAULT_SINGLE) } } } } // 构建时间间隔条目的方法 @Builder IntervalItem(title: string | Resource, intervalType: number) { // 显示标题 Text(title) //响铃间隔时间(分钟) .fontSize(DimensionUtil.getFp($r('app.float.interval_dialog_title_font_size'))) //字体大小 .width(CommonConstants.FULL_LENGTH)//宽度 .textAlign(TextAlign.Start)//设置字体水平居中 .margin({ top: DimensionUtil.getVp($r('app.float.interval_dialog_title_margin_top')) }) // 使用Row布局,放置滑动条和值显示 Row() { // 显示滑动条 Slider({ //如果为0,则当前进度值为响铃间隔时间, 否则为重复响应次数 value: (intervalType === 0 ? this.intervalMinuteSelect : this.intervalTimesSelect), //如果为0,设置最小值为5, 否则为0 min: (intervalType === 0 ? CommonConstants.DEFAULT_INTERVAL_STEP : 0), //如果为0,设置最大值为30, 否则为10 max: (intervalType === 0 ? CommonConstants.DEFAULT_INTERVAL_MINUTE_MAX : CommonConstants.DEFAULT_INTERVAL_TIME_MAX), //如果0,则步长设置5,否则为2 step: (intervalType === 0 ? CommonConstants.DEFAULT_INTERVAL_STEP : CommonConstants.DEFAULT_DOUBLE), style: SliderStyle.OutSet //设置Slider的滑块与滑轨显示样式, SliderStyle.OutSet滑块在滑轨上。 }) .blockColor(Color.Blue) //设置滑块的颜色。 .trackColor(Color.Gray) //设置滑轨的背景颜色 .selectedColor(Color.Blue) //设置滑轨的已滑动部分颜色。 .showSteps(true) //设置当前是否显示步长刻度值。 .showTips(false) //设置滑动时是否显示百分比气泡提示。 .onChange((value: number) => {//Slider滑动时触发事件回调,value:当前滑动进度值 // 根据滑动条值更新选择的时间间隔 if (intervalType === 0) { //如果intervalType === 0,则表示是设置的响铃间隔时间(分钟),进行赋值 this.intervalMinuteSelect = value; } else { //如果intervalType 不等于 0,则表示是设置的重复响铃次数,进行赋值 this.intervalTimesSelect = value; } }) // 显示当前滑动条选择的值, 如果为0,表示为响铃间隔时间(分钟),否则显示的重复响铃次数 Text((intervalType === 0 ? this.intervalMinuteSelect : this.intervalTimesSelect).toFixed(0)) // toFixed设置的是小数部分保留位数 .fontSize(DimensionUtil.getFp($r('app.float.interval_dialog_result_font_size')))//字体大小 } // 设置整行宽度 .width(CommonConstants.FULL_LENGTH) } }
4)DurationDialog.ets:
用于选择闹铃时长的对话框设置如下:
实现代码如下:
import { CommonConstants } from '../../../common/constants/CommonConstants'; import { DetailConstant } from '../../../common/constants/DetailConstant'; import AlarmItem from '../../../viewmodel/AlarmItem'; import CommonDialog from './CommonDialog'; import DimensionUtil from '../../../common/utils/DimensionUtil'; @CustomDialog export default struct DurationDialog { // 从默认提供程序中获取闹钟信息 @Consume(DetailConstant.DEFAULT_PROVIDER_KEY) alarmItem: AlarmItem; // 初始化闹钟响铃时长数组(以分钟为单位) private durations: Array<number> = DetailConstant.RING_DURATION; // 自定义对话框控制器(可选) controller?: CustomDialogController; // 构建对话框 UI build() { Flex() { CommonDialog({ title: $r('app.string.ring_duration'), // 设置对话框标题:闹铃时长 controller: this.controller // 关联控制器 }) { // 遍历响铃时长选项,数组内容:[1,5,10,15钟,20,30] ForEach(this.durations, (item: number) => { Row() { // 显示响铃时长文本 例如: 1 分钟 Text(item + CommonConstants.DEFAULT_STRING_SPACE + DetailConstant.DEFAULT_STRING_MINUTE) .layoutWeight(CommonConstants.DEFAULT_LAYOUT_WEIGHT) // 设置布局权重 .fontColor($r('app.color.grey_divider')) // 设置字体颜色 .fontSize(DimensionUtil.getFp($r('app.float.duration_dialog_content_font_size'))) // 设置字体大小 /** * 响铃时长单选按钮 * value:响铃时长单选按钮 * group:当前单选框的所属群组名称,相同group的Radio只能有一个被选中。 * DetailConstant.DEFAULT_STRING_GROUP_NAME=radioGroup */ Radio({ value: item.toString(), group: DetailConstant.DEFAULT_STRING_GROUP_NAME }) .checked(item === this.alarmItem.duration ? true : false) // 根据当前选项是否匹配警报项的时长来设置选中状态 .height(DimensionUtil.getVp($r('app.float.duration_dialog_radio_size'))) // 设置高度 .width(DimensionUtil.getVp($r('app.float.duration_dialog_radio_size'))) // 设置宽度 .onChange(() => {//单选框选中状态改变时触发回调。 // 处理单选按钮变化事件 if (!this.controller) {//如果不存在,则直接返回 return; } this.controller.close(); // 关闭自定义弹出窗口 this.alarmItem.duration = item; // 修改闹钟的时长 }) } .width(CommonConstants.FULL_LENGTH) // 设置行的宽度 // 添加分隔线 Divider() .opacity($r('app.float.divider_opacity')) // 设置分隔线透明度 .color($r('app.color.grey_divider')) // 设置分隔线颜色 .lineCap(LineCapStyle.Round); // 设置分隔线线帽样式 }) } } } }
六、viewmodel
"viewmodel" 则是一种设计模式,用于将视图(View)与数据模型(Model)分离,提供一个用于交互的中间层。
6.1.AlarmItem.ets
创建闹钟实体类如下:
import ReminderItem from './ReminderItem'; /** * 闹钟实体类 */ @Observed export default class AlarmItem extends ReminderItem { /** * Custom alarm name. */ name = '闹钟'; /** * 自定义闹钟已经开启 */ isOpen: boolean = true; /** * 自定义闹钟重复 */ isRepeat: boolean = false; /** * 持续时间 */ duration: number = 5; /** * 闹钟间隔分钟。 */ intervalMinute: number = 10; /** * 闹钟在此响起间隔时间 */ intervalTimes: number = 3; }
6.2.AlarmSettingItem.ets
表示闹钟的设置属性,例如铃声、振动等。如下:
import { AlarmSettingType } from '../common/constants/AlarmSettingType'; /** * 闹钟设置实体类 */ export default class AlarmSettingItem { /** * 闹钟设置标题 */ public title: string; /** * 闹钟设置内容 */ public content: string; /** * 闹钟类型 */ public sType: AlarmSettingType; constructor(title: string, content: string, sType: AlarmSettingType) { this.title = title; this.content = content; this.sType = sType; } }
6.3.DayDataItem.ets
用于表示时间日期的实体类:
/** * 时间数据实体类 */ export default class DayDataItem { timeType: number = 0; //表示时间类型 delSelect: number = 0;//表示删除选择 data: Array<string> = []; //数据保存到字符串数组中 }
6.4.DetailViewModel.ets
设置闹钟时间、添加、修改和删除闹钟,以及展示闹钟提醒相关操作等。
import { DetailConstant } from '../common/constants/DetailConstant'; import { CommonConstants, WeekDays } from '../common/constants/CommonConstants'; import AlarmItem from './AlarmItem'; import ReminderService from '../model/ReminderService'; import DataTypeUtils from '../common/utils/DataTypeUtils'; import { GlobalContext } from '../common/utils/GlobalContext'; import PreferencesHandler from '../model/database/PreferencesHandler'; import DayDataItem from './DayDataItem'; /** * 详细页面视图模式说明 */ export default class DetailViewModel { //创建了一个静态属性 instant,它是 DetailViewModel 类的一个实例。这个实例在整个应用程序中只会被创建一次。 static instant: DetailViewModel = new DetailViewModel(); //私有属性 reminderService,类型是 ReminderService。ReminderService 是一个服务类,用于处理闹钟功能的业务逻辑。 private reminderService: ReminderService; //私有属性 alarms,它是一个数组,存储着 AlarmItem 对象。AlarmItem 表示一个闹钟项,包含了提醒的详细信息。 private alarms: Array<AlarmItem>; private constructor() { this.reminderService = new ReminderService(); this.alarms = new Array<AlarmItem>(); } /** * 对于闹钟重复日期进行转换 * * @param repeatDay number 重复天数 * @return repeatContent string 字符串类型 */ public transAlarmRepeatDayContent(repeatDay: number): string { // 初始化一个字符串变量repeatContent,并将其赋值为CommonConstants.DEFAULT_STRING_MONDAY='周一' let repeatContent: string = CommonConstants.DEFAULT_STRING_MONDAY; switch (repeatDay) { // 开始一个switch语句,根据repeatDay的值进行分支判断。 case WeekDays.DEFAULT_NUMBER_MONDAY://repeatDay==1 // 将repeatContent的值设置为周一 repeatContent = CommonConstants.DEFAULT_STRING_MONDAY; break; case WeekDays.DEFAULT_NUMBER_TUESDAY://repeatDay==2 //将repeatContent的值设置为周二 repeatContent = CommonConstants.DEFAULT_STRING_TUESDAY; break; case WeekDays.DEFAULT_NUMBER_WEDNESDAY://repeatDay==3 //将repeatContent的值设置为周三 repeatContent = CommonConstants.DEFAULT_STRING_WEDNESDAY; break; case WeekDays.DEFAULT_NUMBER_THURSDAY://repeatDay==4 //将repeatContent的值设置为周四 repeatContent = CommonConstants.DEFAULT_STRING_THURSDAY; break; case WeekDays.DEFAULT_NUMBER_FRIDAY://repeatDay==5 //将repeatContent的值设置为周五 repeatContent = CommonConstants.DEFAULT_STRING_FRIDAY; break; case WeekDays.DEFAULT_NUMBER_SATURDAY://repeatDay==6 //将repeatContent的值设置为周六 repeatContent = CommonConstants.DEFAULT_STRING_SATURDAY; break; case WeekDays.DEFAULT_NUMBER_SUNDAY://repeatDay==7 //将repeatContent的值设置为周日 repeatContent = CommonConstants.DEFAULT_STRING_SUNDAY; break; default: // 如果repeatDay的值不在以上的任何一个情况中,结束switch语句。 break; } // 返回repeatContent的值作为方法的输出结果。例如:周一: return repeatContent; } /** * 设置闹钟的初始时间 * * @param alarmItem AlarmItem */ public setAlarmDefaultTime(alarmItem?: AlarmItem) { // 声明变量用于存储小时和分钟 let hour: number; let minute: number; // 如果传入的闹钟项为空 if (alarmItem == null) { // 获取当前时间的小时和分钟 let date = new Date(); hour = date.getHours(); minute = date.getMinutes(); } else { // 如果传入的闹钟项不为空,则使用传入的小时和分钟 hour = alarmItem.hour; minute = alarmItem.minute; } // 根据小时设置默认选中的日期数据, ['上午', '下午'] (hour>=12 ? 1:0) DetailConstant.DAY_DATA[0].delSelect = (hour >= CommonConstants.DEFAULT_TOTAL_HOUR ? CommonConstants.DEFAULT_SINGLE : 0); /** * 下面两个需要减去CommonConstants.DEFAULT_SINGLE=1 是因为数组索引从 0 开始,为了保持一致性,所以需要将小时数减去 1。 * DetailConstant.DAY_DATA 是一个数组,存储了一天中每个时刻的数据。 * CommonConstants.DEFAULT_SINGLE 的值为 1,表示单个单位。 * CommonConstants.DEFAULT_TOTAL_HOUR 的值为 12,表示一天中的小时数。(12小时记数) * CommonConstants.DEFAULT_TOTAL_MINUTE 的值为 60,表示一小时中的分钟数。 */ // 根据小时设置默认选中的日期数据 DetailConstant.DAY_DATA[1]表示DetailConstant中索引为1的数据 DetailConstant.DAY_DATA[1] =(hour === 0 ? 12: (hour>12? hour-12:hour))- 1 DetailConstant.DAY_DATA[CommonConstants.DEFAULT_SINGLE].delSelect = (hour === 0 ? CommonConstants.DEFAULT_TOTAL_HOUR : (hour > CommonConstants.DEFAULT_TOTAL_HOUR ? hour - CommonConstants.DEFAULT_TOTAL_HOUR : hour)) - CommonConstants.DEFAULT_SINGLE; // 设置默认选中的分钟数据 DetailConstant.DAY_DATA[2].delSelect = (minute === 0 ? 60 : minute) - 1; DetailConstant.DAY_DATA[CommonConstants.DEFAULT_DATA_PICKER_HOUR_SELECTION].delSelect = (minute === 0 ? CommonConstants.DEFAULT_TOTAL_MINUTE : minute) - CommonConstants.DEFAULT_SINGLE; } /** * 添加创建闹钟提醒 * * @param alarmItem AlarmItem 添加的闹钟参数 */ public async setAlarmRemind(alarmItem: AlarmItem) { //设置 alarmItem 的小时数属性,使用了 getAlarmTime 方法并传入一个常量1。 alarmItem.hour = this.getAlarmTime(CommonConstants.DEFAULT_SINGLE); //设置 alarmItem 的分钟数属性,使用了 getAlarmTime 方法并传入另一个常量2。 alarmItem.minute = this.getAlarmTime(CommonConstants.DEFAULT_DATA_PICKER_HOUR_SELECTION); //查看闹钟是否存在,如果存在则返回id let index = await this.findAlarmWithId(alarmItem.id); //index 如果不等于-1,如果找到了相同 id 的闹钟,则删除之前的提醒。否则,将 alarmItem 添加到闹钟数组中。 if (index !== CommonConstants.DEFAULT_NUMBER_NEGATIVE) { //删除提醒 this.reminderService.deleteReminder(alarmItem.id); } else { //将 alarmItem 添加到闹钟数组中。 index = this.alarms.length; //获取闹钟数组的长度 alarmItem.notificationId = index;//使用长度,作为闹钟提醒的id this.alarms.push(alarmItem);//将闹钟信息添加到闹钟数组中 } /** * 通过提醒服务 (reminderService) 添加新的提醒。在提醒添加成功后,设置 alarmItem 的 id 和 isOpen 属性,并将其存储在本地偏好设置中。 */ this.reminderService.addReminder(alarmItem, (newId: number) => { alarmItem.id = newId;//修改闹钟alarmItem.id属性的值为newId alarmItem.isOpen = true;//修改闹钟状态为true,表示闹钟已经打开 this.alarms[index] = alarmItem;//将本地的闹钟数组中之前添加的闹钟信息,进行替换 let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler; //在 首选项 中添加数据。'alarmData'作为key, this.alarms闹钟数组转换为JSON,保存到首选项中 preference.set(CommonConstants.ALARM_KEY, JSON.stringify(this.alarms)); }) } /** * 删除闹钟提醒。 * * @param id number */ public async removeAlarmRemind(id: number) { //调用了 reminderService 的 deleteReminder 方法,并传入 id 参数。取消闹钟提醒。 this.reminderService.deleteReminder(id); //初始化了变量 index,通过调用 this.findAlarmWithId 方法来查找指定 id 的闹钟,并使用 await 等待异步操作完成。 let index = await this.findAlarmWithId(id); //开启了一个条件判断语句,判断 index 是否不等于 -1。表示 if (index !== CommonConstants.DEFAULT_NUMBER_NEGATIVE) { // 如果条件满足,调用 splice 方法来删除 alarms 数组中指定下标的元素。 this.alarms.splice(index, CommonConstants.DEFAULT_SINGLE); } //声明并初始化了一个变量 preference,通过 GlobalContext 获取了一个对象,并将其转换为 PreferencesHandler 类型。 let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler; //调用了 preference 的 set 方法,将 this.alarms 数组转换为 JSON 字符串并存储在指定的 'alarmData'作为的key 中。 preference.set(CommonConstants.ALARM_KEY, JSON.stringify(this.alarms)); } /** * 获取闹钟时间 * @param aType 指定时间的类型(上下午、小时、分钟) * @returns */ private getAlarmTime(aType: number): number { //从一个名为 DetailConstant.DAY_DATA 的常量对象中获取对应 aType 的 DayDataItem 数据,表示一天中的特定类型数据。 let times:DayDataItem = DetailConstant.DAY_DATA[aType]; //获取 times 对象中的 delSelect 属性,表示选定的索引。 let selectedIndex = times.delSelect; //根据选定的索引,从 times.data 数组中获取对应的时间值,并转换为数字类型。 let time = Number(times.data[selectedIndex]); //如果aType===1 if (aType === CommonConstants.DEFAULT_SINGLE) { /** * (time === 12?0:time)+(DetailConstant.DAY_DATA[0].delSelect === 1 ? 12:0) */ time = (time === CommonConstants.DEFAULT_TOTAL_HOUR ? 0 : time) + (DetailConstant.DAY_DATA[0].delSelect === CommonConstants.DEFAULT_SINGLE ? CommonConstants.DEFAULT_TOTAL_HOUR : 0); } return time; } /** * 从首选项中获取存储的闹钟数据,并在数组中查找与指定 id 匹配的闹钟,如果找到则返回对应的索引,否则返回一个默认的负数值。 * @param id * @returns */ private async findAlarmWithId(id: number) { //通过 GlobalContext 获取了一个对象,并将其转换为 PreferencesHandler 类型。 let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler; //通过 GlobalContext 获取'alarmData'对象,并将其转换为 PreferencesHandler 类型。 let data = await preference.get(CommonConstants.ALARM_KEY); //如果获取的数据不为null或undefined或空字符串。则执行 if (!DataTypeUtils.isNull(data)) { //获取的 JSON 字符串 data 转换为对象,并赋值给 this.alarms 数组。 this.alarms = JSON.parse(data); //遍历 this.alarms 数组 for (let i = 0;i < this.alarms.length; i++) { //判断当前元素的 id 是否等于传入的 id if (this.alarms[i].id === id) { //如果条件满足,返回当前的索引 i,表示找到了对应的闹钟。 return i; } } } //如果循环结束仍未找到对应的闹钟,返回一个默认的负数值 -1。 return CommonConstants.DEFAULT_NUMBER_NEGATIVE; } }
6.5.MainViewModel.ets
包括展示主页的闹钟列表、打开和关闭单个闹钟等。
import { MainConstant } from '../common/constants/MainConstant'; import ReminderService from '../model/ReminderService'; import {CommonConstants, WeekDays } from '../common/constants/CommonConstants'; import AlarmItem from './AlarmItem'; import DataTypeUtils from '../common/utils/DataTypeUtils'; import { GlobalContext } from '../common/utils/GlobalContext'; import PreferencesHandler from '../model/database/PreferencesHandler'; import PreferencesListener from '../model/database/PreferencesListener'; /** * MainViewModel 类用于管理主界面相关的逻辑和数据。 */ export default class MainViewModel { // 单例实例 static instant: MainViewModel = new MainViewModel(); // 用于处理提醒服务的实例 private reminderService: ReminderService; // 存储闹钟项的数组 private alarms: Array<AlarmItem>; /** * 构造函数,在实例化时初始化闹钟数组和提醒服务,并请求通知权限。 */ private constructor() { this.alarms = new Array(); // 初始化闹钟数组 this.reminderService = new ReminderService(); // 初始化提醒服务 this.reminderService.openNotificationPermission(); // 请求通知权限 } /** * 查询本地数据库中存储的闹钟数据。 * @param callback 获取数据后的回调函数,参数为闹钟项数组。 */ private queryDatabaseAlarms(callback: (alarms: Array<AlarmItem>) => void) { // 获取首选项实例 let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler; // 从偏好设置中获取闹钟数据 ,'alarmData' preference.get(CommonConstants.ALARM_KEY).then((data: string) => { // 如果数据不为空 if (!DataTypeUtils.isNull(data)) { // 解析数据为闹钟首选项数组,赋值给this.alarms this.alarms = JSON.parse(data); /** * 当从数据库中成功获取到闹钟数据后,通过回调函数将这些数据传递给调用者。通常情况下,调用者会提供一个回调函数, * 在这个函数中处理从数据库获取到的闹钟数据,比如更新界面上的闹钟列表。 */ callback(this.alarms); } }) } /** * 查询闹钟任务数据库,并通过回调函数返回查询结果,同时还通过首选项监听器实时更新闹钟数据。当首选项数据发生变化时,会重新查询数据库中的闹钟数据并调用回调函数进行更新。 * @param callback 获取数据后的回调函数,参数为闹钟项数组。 */ public queryAlarmsTasker(callback: (alarms: Array<AlarmItem>) => void) { /** * 使用 let that = this; 的目的是在回调函数中保存对外部 this 的引用,以便在回调函数中能够访问到类的实例。这是因为在 JavaScript/TypeScript 中, * 回调函数内部的 this 上下文可能会改变,所以需要在外部把对当前对象的引用保存在一个变量中,以便在回调函数中使用。 */ let that = this; // 查询首选项数据库中的闹钟数据,并调用回调函数 that.queryDatabaseAlarms(callback); // 获取偏好设置实例 let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler; // 添加偏好设置监听器,用于实时更新闹钟数据 preference.addPreferencesListener({ onDataChanged() { // 数据发生变化时重新查询数据库中的闹钟数据,并调用回调函数 that.queryDatabaseAlarms(callback); } } as PreferencesListener) } /** * 将数字填充为两位数,不足时在前面补零。 * @param val 需要填充的数字 * @return 填充后的字符串 */ public fillZero(val: number): string { // 如果数字大于两位数,则直接转换为字符串返回 // 否则,在数字前补零后再转换为字符串返回 return (val > MainConstant.DEFAULT_SINGLE_DIGIT_MAX ? val.toString() : (MainConstant.DEFAULT_ZEROING + val)); } /** * 用于获取时间上午还是下午 * * @param hour number * @return content string */ public getNoonContent(hour: number): string { // hour < 12 ?'上午':'下午' return (hour < CommonConstants.DEFAULT_TOTAL_HOUR ? MainConstant.DEFAULT_STRING_MORNING : MainConstant.DEFAULT_STRING_AFTERNOON); } /** * 在 MainViewModel 中获取任务时间内容。 * 该方法用于将给定的小时数和分钟数转换为任务时间内容字符串。 * * @param hour 小时数 * @param minute 分钟数 * @return 表示任务时间的字符串,格式为 "HH:MM" */ public getTaskTimeContent(hour: number, minute: number): string { // 如果小时数大于默认的总小时数12,计算实际小时数;否则使用原始小时数 let adjustedHour = hour > CommonConstants.DEFAULT_TOTAL_HOUR ? hour - CommonConstants.DEFAULT_TOTAL_HOUR : hour; // 获取填充零后的小时数 let paddedHour = this.fillZero(adjustedHour); // 获取填充零后的分钟数 let paddedMinute = this.fillZero(minute); // 拼接小时数和分钟数,中间用冒号分隔,形成任务时间内容字符串 let taskTimeContent = paddedHour + MainConstant.DEFAULT_STRING_COLON + paddedMinute; // 返回任务时间内容字符串 return taskTimeContent; } /** * 获取闹钟重复的日期 * * @param alarmItem AlarmItem 闹钟信息 * @return content string 例如: 闹钟,周六 周日 闹钟,不重复 */ public getDescContent(alarmItem: AlarmItem): string { // ('闹钟' + ','+ (alarmItem.isRepeat? "显示具体重复的天数,例如:周六 周日":'不重复')) return (alarmItem.name + CommonConstants.DEFAULT_STRING_COMMA + (alarmItem.isRepeat ? this.getAlarmRepeatDayContent(alarmItem.repeatDays) : CommonConstants.DEFAULT_STRING_NO_REPEAT)); } /** * 根据一周中的重复日期数组,生成相应的字符串内容 * * @param repeatDays Array<number> 闹钟重复日期数组 * @return content string */ public getAlarmRepeatDayContent(repeatDays: Array<number>): string { //给content赋值'' let content = MainConstant.DEFAULT_STRING_NULL; //遍历重复天数 repeatDays 数组中的每个元素。 for (let i = 0; i < repeatDays.length; i++) { //创建一个名为 repeatDay 的变量,用于存储当前循环中的日期。 let repeatDay = repeatDays[i]; //创建一个名为 repeatContent 的变量,并将其初始化为: '周一' let repeatContent: string = CommonConstants.DEFAULT_STRING_MONDAY; switch (repeatDay) { case WeekDays.DEFAULT_NUMBER_MONDAY://判断repeatDay=1 //repeatContent赋值给周一 repeatContent = CommonConstants.DEFAULT_STRING_MONDAY; break; case WeekDays.DEFAULT_NUMBER_TUESDAY://判断repeatDay=2 //repeatContent赋值给周二 repeatContent = CommonConstants.DEFAULT_STRING_TUESDAY; break; case WeekDays.DEFAULT_NUMBER_WEDNESDAY://判断repeatDay=3 //repeatContent赋值给周三 repeatContent = CommonConstants.DEFAULT_STRING_WEDNESDAY; break; case WeekDays.DEFAULT_NUMBER_THURSDAY://判断repeatDay=4 //repeatContent赋值给周四 repeatContent = CommonConstants.DEFAULT_STRING_THURSDAY; break; case WeekDays.DEFAULT_NUMBER_FRIDAY://判断repeatDay=5 //repeatContent赋值给周五 repeatContent = CommonConstants.DEFAULT_STRING_FRIDAY; break; case WeekDays.DEFAULT_NUMBER_SATURDAY://判断repeatDay=6 //repeatContent赋值给周六 repeatContent = CommonConstants.DEFAULT_STRING_SATURDAY; break; case WeekDays.DEFAULT_NUMBER_SUNDAY://判断repeatDay=7 //repeatContent赋值给周七 repeatContent = CommonConstants.DEFAULT_STRING_SUNDAY; break; default: break; } // '' + "周一" + ' ' content += (repeatContent + CommonConstants.DEFAULT_STRING_SPACE); } return content; } /** * 启用/关闭 闹钟。 * * @param id number 表示闹钟的 ID * @return isOpen boolean 一个布尔值,表示是否打开(启用)或关闭(禁用)闹钟。 */ public openAlarm(id: number, isOpen: boolean) { //遍历创建的闹钟信息 for (let i = 0; i < this.alarms.length; i++) { //检查当前循环中的闹钟的 id 是否与传入的 id 参数匹配 if (this.alarms[i].id === id) { //将当前遍历闹钟的 isOpen 属性修改赋值,使用传入 isOpen 参数的值。这实际上是启用或禁用了该闹钟。 this.alarms[i].isOpen = isOpen; //如果isOpen为true,则表示开启了闹钟 if (isOpen) { //如果闹钟开启,调用 reminderService 对象上的 addReminder 方法,将当前闹钟作为参数传递。目的是将该闹钟添加到提醒列表中。 this.reminderService.addReminder(this.alarms[i]); } else { //调用 reminderService 对象上的 deleteReminder 方法,将当前闹钟的 id 作为参数传递。目的是从提醒列表中删除取消该闹钟提醒。 this.reminderService.deleteReminder(this.alarms[i].id); } //创建一个名为 preference 的局部变量。从全局上下文中获取首选项preference,并将其转换为 PreferencesHandler 类型。 let preference = GlobalContext.getContext().getObject('preference') as PreferencesHandler; //在 preference 对象上调用 set 方法。将 this.alarms 数组的序列化成 JSON ,存储在由alarmData指定的key下。 preference.set(CommonConstants.ALARM_KEY, JSON.stringify(this.alarms)); break; } } } }
6.6.ReminderItem.ets
用于表示后台提醒的属性,例如提醒的时间、类型等
import reminderAgent from '@ohos.reminderAgentManager'; /** * 闹钟提醒实体类 */ export default class ReminderItem { id: number = 0; //声明了一个属性 remindType,它是一个 reminderAgent.ReminderType 类型,初始值为 REMINDER_TYPE_ALARM。 remindType: reminderAgent.ReminderType = reminderAgent.ReminderType.REMINDER_TYPE_ALARM; name: string = '';//名称 hour: number = 0;//小时 minute: number = 0;//分钟 duration: number = 0;//持续时间 intervalMinute: number = 0;//间隔分钟 intervalTimes: number = 0;//闹钟在此响起间隔时间 repeatDays: Array<number> = [];//重复天数 notificationId: number = 0; }
七、创建主页面
MainIndex.ets,实现闹钟主要,如下:
代码如下:
import router from '@ohos.router'; import { CommonConstants } from '../common/constants/CommonConstants'; import AlarmItem from '../viewmodel/AlarmItem'; import { MainConstant } from '../common/constants/MainConstant'; import MainModel from '../viewmodel/MainViewModel'; import ClockArea from './../view/Main/ClockArea'; import AlarmList from './../view/Main/AlarmList'; import DimensionUtil from '../common/utils/DimensionUtil'; @Entry @Component struct MainIndex { // 主模型实例,用于处理主页面相关的逻辑和数据 private mainModel: MainModel = MainModel.instant; // 存储添加的多个闹钟信息的数组 @State alarmItems: Array<AlarmItem> = new Array(); // 表示用户是否已经授权的状态变量,初始值为 false @State isAuth: boolean = false; /** * 在页面即将显示时调用的方法。 * 该方法会调用主模型的查询闹钟任务方法,以获取闹钟数据,并将数据存储到 alarmItems 中。 */ aboutToAppear() { let that = this; that.mainModel.queryAlarmsTasker((alarms: Array<AlarmItem>) => { that.alarmItems = alarms; }) } build() { Column() { //闹钟 Text(MainConstant.DEFAULT_STRING_ALARM) .width(CommonConstants.FULL_LENGTH) //宽度 .height(DimensionUtil.getVp($r('app.float.page_title_height'))) //高度 .textAlign(TextAlign.Start) //设置文本绘制中的文本对齐方式 .fontSize(DimensionUtil.getFp($r('app.float.title_font_size'))) //字体大小 .fontColor($r('app.color.grey_divider')) //字体颜色 .fontWeight(FontWeight.Bold) //字体加粗 .margin({ bottom: DimensionUtil.getVp($r('app.float.content_margin_body')) }) .padding({ left: DimensionUtil.getVp($r('app.float.title_horizon_margin')) }) //表盘绘制 ClockArea() //显示添加的闹钟信息 AlarmList({ alarmItems: $alarmItems }) //控制填充 Blank() //+号,点击后跳转到添加闹钟页面 Button() { Image($r('app.media.ic_add')) //设置图片的缩放类型。ImageFit.Fill指的是在不保持宽高比的情况下放大或缩小图像,使图像填满显示边界。 .objectFit(ImageFit.Fill) } .backgroundColor($r('app.color.trans_parent')) //背景颜色 .width(DimensionUtil.getVp($r('app.float.new_alarm_button_size'))) //宽 .height(DimensionUtil.getVp($r('app.float.new_alarm_button_size'))) //高 .margin({ bottom: DimensionUtil.getVp($r('app.float.new_alarm_button_margin_vertical')), top: DimensionUtil.getVp($r('app.float.new_alarm_button_margin_vertical')) }) .onClick(() => { router.pushUrl({ url: 'pages/DetailIndex' }); //跳转的页面 }) } .width(CommonConstants.FULL_LENGTH) //100% .height(CommonConstants.FULL_LENGTH) //100% .backgroundColor($r('app.color.grey_light')) //背景颜色 } }
创建DetailIndex.ets,实现新建闹钟页面:
代码如下:
import router from '@ohos.router'; import { CommonConstants } from '../common/constants/CommonConstants'; import AlarmItem from '../viewmodel/AlarmItem'; import AlarmSettingItem from '../viewmodel/AlarmSettingItem'; import { AlarmSettingType } from '../common/constants/AlarmSettingType'; import { DetailConstant } from '../common/constants/DetailConstant'; import BackContainer from '../view/BackContainer'; import DetailModel from '../viewmodel/DetailViewModel'; import DatePickArea from '../view/detail/DatePickArea'; import SettingItem from '../view/Detail/SettingItem'; import DimensionUtil from '../common/utils/DimensionUtil'; @Entry @Component struct DetailIndex { //使用 Watch 装饰器监听 onAlarmItemChange 事件,当 alarmItem 发生变化时执行对应的方法。 //@Watch用于监听状态变量的变化,当状态变量变化时,@Watch的回调方法将被调用 //@Provide和@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景 //使用 Provide 装饰器将 alarmItem 提供给其他组件使用,并初始化为一个新的 AlarmItem 实例。 @Watch('onAlarmItemChange') @Provide(DetailConstant.DEFAULT_PROVIDER_KEY) alarmItem: AlarmItem = new AlarmItem(); //存储重复设置项的数组 @State repeatSettingArr: Array<AlarmSettingItem> = []; // 存储闹钟设置信息项的数组 @State alarmSettingInfoArr: Array<AlarmSettingItem> = []; private isNow: boolean = true; private viewModel: DetailModel = DetailModel.instant; // 当弹窗即将出现时的回调函数 aboutToAppear() { // 从路由参数中获取参数 let params = router.getParams() as Record<string, Object|undefined>; if (params !== undefined) { // 从参数中获取alarmItem 闹钟信息 let alarmItem: AlarmItem = params.alarmItem as AlarmItem; if (alarmItem !== undefined) { // 若获取到alarmItem,则更新isNow标志为false,并更新alarmItem,并根据新的alarmItem设置默认时间 this.isNow = false; //将传递过来的alarmItem进行赋值,由于this.alarmItem前面使用 @Watch('onAlarmItemChange'),所以会执行onAlarmItemChange this.alarmItem = alarmItem; this.viewModel.setAlarmDefaultTime(this.alarmItem); } else { // 若未获取到alarmItem,则使用viewModel设置默认时间 this.viewModel.setAlarmDefaultTime(); } } else { // 若参数为空,则使用viewModel设置默认时间 this.viewModel.setAlarmDefaultTime(); } // 初始化数据 this.initData(); } // 当alarmItem发生变化时执行的方法 onAlarmItemChange() { // 初始化数据 this.initData(); } // 初始化数据(创建闹钟时候的选项:重复、闹钟名、闹钟铃声、再想间隔)的方法 initData() { // 设置重复设置项数组 // new AlarmSettingItem('重复',this.alarmItem.isRepeat ?'重复':'不重复',重复类型) this.repeatSettingArr = [ new AlarmSettingItem(DetailConstant.DEFAULT_STRING_REPEAT, // 根据alarmItem的isRepeat属性确定重复设置的显示内容 this.alarmItem.isRepeat ? DetailConstant.DEFAULT_STRING_REPEAT : CommonConstants.DEFAULT_STRING_NO_REPEAT, AlarmSettingType.REPEAT) ]; // 设置闹钟信息项数组 this.alarmSettingInfoArr = [ // new AlarmSettingItem('闹钟名','闹钟名字',重复类型) new AlarmSettingItem(DetailConstant.DEFAULT_STRING_ALARM_NAME, this.alarmItem.name, AlarmSettingType.ALARM_NAME), // new AlarmSettingItem('闹铃时长','5分钟',设置铃声持续时间) new AlarmSettingItem(DetailConstant.DEFAULT_STRING_DURATION, // 设置闹钟响铃时长的显示内容,包括时长和单位 this.alarmItem.duration + DetailConstant.DEFAULT_STRING_MINUTE, AlarmSettingType.RING_DURATION), // new AlarmSettingItem('再响间隔','10分钟,3次',闹铃间隔时间) new AlarmSettingItem(DetailConstant.DEFAULT_STRING_INTERVAL, // 设置闹钟响铃间隔的显示内容,包括分钟数、单位、逗号和次数 this.alarmItem.intervalMinute + DetailConstant.DEFAULT_STRING_MINUTE + CommonConstants.DEFAULT_STRING_COMMA + this.alarmItem.intervalTimes + DetailConstant.DEFAULT_STRING_TIMES, AlarmSettingType.INTERVAL) ] } build() { Column() { BackContainer({ header: this.isNow ? $r('app.string.new_alarm') : $r('app.string.update_alarm'),//判断是新建闹钟还是修改闹钟 backImgRes: $r('app.media.ic_cancel'), //图标 × }) { Button() { //保存按钮 Image($r('app.media.ic_confirm')).objectFit(ImageFit.Fill) } .backgroundColor($r('app.color.trans_parent')) //背景颜色 .width(DimensionUtil.getVp($r('app.float.title_button_size'))) .height(DimensionUtil.getVp($r('app.float.title_button_size'))) .onClick(() => { this.viewModel.setAlarmRemind(this.alarmItem);//保存闹钟数据到首选项 router.back(); //返回页面 }) } //日期选择组件 DatePickArea() //重复 SettingItem({ settingInfo: $repeatSettingArr }) //闹钟名、闹铃时长、再响间隔 SettingItem({ settingInfo: $alarmSettingInfoArr }) Blank() Button($r('app.string.delete')) //删除闹钟按钮 .visibility(this.isNow ? Visibility.None : Visibility.Visible) //根据条件this.isNow来设置按钮的可见性。如果this.isNow为真,则按钮不可见;否则按钮可见。 .width(DimensionUtil.getVp($r('app.float.delete_button_width'))) //设置按钮的宽度 .height(DimensionUtil.getVp($r('app.float.delete_button_height'))) //设置按钮的高度 .fontSize(DimensionUtil.getFp($r('app.float.delete_button_font_size'))) //按钮文本的字体大小 .fontColor($r('app.color.red_light')) //字体颜色 .backgroundColor($r('app.color.grey_with_opacity')) //设置背景颜色 .borderRadius(DimensionUtil.getVp($r('app.float.delete_button_radius'))) //设置圆角半径 .margin({ bottom: DimensionUtil.getVp($r('app.float.delete_button_margin_bottom')) }) .onClick(() => { this.viewModel.removeAlarmRemind(this.alarmItem.id);//根据闹钟id,删除闹钟信息 router.back();//返回上一页 }) } .backgroundColor($r('app.color.grey_light')) .width(CommonConstants.FULL_LENGTH) .height(CommonConstants.FULL_LENGTH) } }