鸿蒙-6. 华为闹钟

阶段案例-华为闹钟

首页 详情页

1. 页面结构-首页结构

pages/Index.ets
1import { CanvasComp } from '../components/CanvasComp'
2import { ClockItemComp } from '../components/ClockItemComp'
3
4@Entry
5@Component
6struct Index {
7  build() {
8    Stack({ alignContent: Alignment.Bottom }){
9      Column({ space: 15 }) {
10        Text('闹钟')
11          .fontSize('24')
12          .width('100%')
13
14        CanvasComp()
15
16        ForEach([1, 2, 3], () => {
17          ClockItemComp()
18            .onClick(() => {
19              router.pushUrl({
20                url: 'pages/DetailPage',
21              })
22            })
23        })
24      }
25      .padding(15)
26      .height('100%')
27      .width('100%')
28      .backgroundColor('#f5f5f5')
29      // 添加
30      Text('+')
31        .width(50)
32        .aspectRatio(1)
33        .backgroundColor('#06f')
34        .fontSize(40)
35        .fontWeight(100)
36        .fontColor('#fff')
37        .borderRadius(25)
38        .textAlign(TextAlign.Center)
39        .margin({ bottom: 50 })
40        .onClick(() => {
41          router.pushUrl({
42            url: 'pages/DetailPage',
43          })
44        })
45    }
46  }
47}
components/CanvasComp.ets
1@Component
2export struct CanvasComp {
3  build() {
4    
5  }
6}
components/ClockItemComp.ets
1@Component
2export struct ClockItemComp {
3  build() {
4    
5  }
6}

2. 页面结构-绘制闹钟

  1. 绘制表盘
  2. 绘制 时分秒 指针
  3. 走起来
1@Component
2export struct CanvasComp {
3  private settings = new RenderingContextSettings(true)
4  private context = new CanvasRenderingContext2D(this.settings)
5  private panImg = new ImageBitmap('/images/ic_clock_pan.png')
6  private hourImg = new ImageBitmap('/images/ic_hour_pointer.png')
7  private minuteImg = new ImageBitmap('/images/ic_minute_pointer.png')
8  private secondImg = new ImageBitmap('/images/ic_second_pointer.png')
9
10  // 画布尺寸
11  canvasSize = 252
12  // 指针尺寸
13  pointerWidth = 8
14
15  startDraw() {
16    this.drawClock()
17    setInterval(() => {
18      this.drawClock()
19    }, 1000)
20  }
21
22  /**
23   * 绘制闹钟
24   */
25  drawClock() {
26    this.context.clearRect(0, 0, this.canvasSize, this.canvasSize)
27    this.context.drawImage(this.panImg, 0, 0, this.canvasSize, this.canvasSize)
28    // 根据时间绘制指针
29    const date = new Date()
30    const hour = date.getHours()
31    const minute = date.getMinutes()
32    const second = date.getSeconds()
33    this.drawPointer(this.hourImg, hour % 12 / 12 * 360)
34    this.drawPointer(this.minuteImg, minute / 60 * 360)
35    this.drawPointer(this.secondImg, second / 60 * 360)
36  }
37
38  /**
39   * 绘制指针
40   * @param img - 指针图片
41   * @param angle - 旋转角度,起点是 Y 轴上方向
42   */
43  drawPointer(img: ImageBitmap, angle: number) {
44    this.context.save()
45    this.context.translate(this.canvasSize / 2, this.canvasSize / 2)
46    this.context.rotate((angle + 180) * Math.PI / 180)
47    this.context.translate(-this.canvasSize / 2, -this.canvasSize / 2)
48    this.context.drawImage(img, this.canvasSize / 2 - this.pointerWidth / 2, 0, this.pointerWidth, this.canvasSize)
49    this.context.restore()
50  }
51
52  build() {
53    Canvas(this.context)
54      .width(this.canvasSize)
55      .aspectRatio(1)
56      .onReady(() => {
57        this.startDraw()
58      })
59  }
60}

3. 页面结构-绘制闹钟任务列表

1@Component
2export struct ClockItemComp {
3  build() {
4    Row(){
5      Column({ space: 5 }) {
6        Row({ space: 5 }){
7          Text('下午')
8            .fontColor('#666')
9          Text('04:30')
10            .fontWeight(600)
11            .fontSize(18)
12        }
13        Row({ space: 15 }){
14          Text('闹钟')
15            .fontColor('#999')
16            .fontSize(14)
17          Text('不重复')
18            .fontColor('#999')
19            .fontSize(14)
20        }
21      }
22      .alignItems(HorizontalAlign.Start)
23
24      // 开关
25      Toggle({ type: ToggleType.Switch , isOn: true })
26    }
27    .height(64)
28    .padding({ left: 20, right: 15 })
29    .width('100%')
30    .backgroundColor('#fff')
31    .borderRadius(30)
32    .justifyContent(FlexAlign.SpaceBetween)
33  }
34}

4. 页面结构-详情页

1@Entry
2@Component
3struct DetailPage {
4
5  submit () {
6    // TODO
7  }
8
9  @Builder
10  CellBuilder (title: string, value: string) {
11    Row(){
12      Text(title)
13        .layoutWeight(1)
14      Text(value)
15        .fontColor('#999')
16      Image('/images/ic_public_arrow_right.svg')
17        .width(18)
18        .aspectRatio(1)
19        .fillColor('#999')
20    }
21    .height(60)
22    .padding({ left: 15, right: 15 })
23    .backgroundColor('#fff')
24  }
25
26  build() {
27    Navigation(){
28      Column({ space: 15 }){
29        TimePicker()
30          .borderRadius(16)
31          .clip(true)
32
33        Column({ space: 1 }){
34          this.CellBuilder('闹铃名称', '闹铃')
35          this.CellBuilder('重复', '不重复')
36        }
37        .borderRadius(16)
38        .clip(true)
39
40        Text('删除闹铃')
41          .width(100)
42          .height(40)
43          .borderRadius(20)
44          .backgroundColor('#c3c4c5')
45          .textAlign(TextAlign.Center)
46          .fontColor('#f00')
47          .opacity(0.3)
48      }
49      .padding(15)
50    }
51    .title('新建闹钟')
52    .titleMode(NavigationTitleMode.Mini)
53    .mode(NavigationMode.Stack)
54    .backgroundColor('#f5f5f5')
55    .menus([
56      { value: '', icon: '/images/ic_confirm.png', action: () => this.submit() }
57    ])
58  }
59}

5. 业务逻辑-首选项存储工具

  • 创建首选项存储工具类
  • 获取首选项实例
  • 设置和修改闹钟信息
  • 删除闹钟信息
  • 获取所有的闹钟信息
1import preferences from '@ohos.data.preferences'
2
3export class ClockItem {
4  hour?: number
5  minute?: number
6  id?: string
7  reminderId?: number
8  enabled?: boolean
9}
10
11export class ClockStore {
12
13  getStore() {
14    return preferences.getPreferences(getContext(this), 'clockStore')
15  }
16
17  async addClock(clockItem: ClockItem) {
18    const store = await this.getStore()
19    await store.put(clockItem.id, JSON.stringify(clockItem))
20    await store.flush()
21  }
22
23  async delClock(id: string) {
24    const store = await this.getStore()
25    await store.delete(id)
26    await store.flush()
27  }
28
29  async getAllClock() {
30    const store = await this.getStore()
31    const data = await store.getAll()
32    const list = Object.keys(data).map<ClockItem>(key => {
33      return JSON.parse(data[key])
34    })
35    return list
36  }
37}

6. 业务逻辑-添加闹钟任务

  • 绑定时间选择组件,回收选中后的时间数据
  • 开启提醒
  • 保存到首选项
1@State
2  clockItem: ClockItem = {}
3
4  // Date 不能直接使用 @State 装饰器
5  @State
6  state: { now: Date } = { now: new Date() }
7  clockStore = new ClockStore()
8
9  async submit() {
10
11    // 1. 开启提醒
12    const hour = this.state.now.getHours()
13    const minute = this.state.now.getMinutes()
14    const request: reminderAgentManager.ReminderRequestAlarm = {
15      reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_ALARM,
16      hour,
17      minute,
18      title: '闹钟',
19      ringDuration: 60
20    }
21    const reminderId = await reminderAgentManager.publishReminder(request)
22    // 2. 记录数据
23    this.clockItem = {
24      hour,
25      minute,
26      id: util.generateRandomUUID(),
27      reminderId,
28      enabled: true
29    }
30    await this.clockStore.addClock(this.clockItem)
31    promptAction.showToast({ message: '添加闹钟成功' })
32    router.back()
33  }
1TimePicker({
2          selected: this.state.now
3        })
4          .borderRadius(16)
5          .clip(true)

7. 业务逻辑-渲染闹钟任务列表

  • 获取首选项闹钟数据
  • 遍历数组
  • 去闹钟项组件渲染

1) 获取首选项闹钟数据

1@State
2  clockList: ClockItem[] = []
3
4  clockStore = new ClockStore()
5
6  aboutToAppear() {
7    this.initData()
8  }
9  onPageShow() {
10    this.initData()
11  }
12  async initData() {
13    this.clockList = await this.clockStore.getAllClock()
14  }

2) 遍历数组

1ForEach(this.clockList, (item: ClockItem) => {
2          ClockItemComp({ item })
3            .onClick(() => {
4              router.pushUrl({
5                url: 'pages/DetailPage',
6                params: item
7              })
8            })
9        })

3) 去闹钟项组件渲染

1@State
2  item: ClockItem = {}
3
4  padZero(val: number = 0) {
5    return val.toString().padStart(2, '0')
6  }
1Row({ space: 5 }) {
2          Text(this.item.hour > 12 ? '下午' : '上午')
3            .fontColor('#666')
4          Text(`${this.padZero(this.item.hour > 12 ? this.item.hour - 12 : this.item.hour)}:${this.padZero(this.item.minute)}`)
5            .fontWeight(600)
6            .fontSize(18)
7        }

8. 业务逻辑-修改闹铃任务

  • 传递闹铃数据到详情页,需要接收下

  • 页面上的标题需要判断显示

  • 在 submit 函数中加入编辑逻辑

1)传递闹铃数据到详情页,需要接收下

DetailPage.ets
1aboutToAppear() {
2    const params = router.getParams() as ClockItem
3    if (params && params.id) {
4      this.clockItem = { ...params }
5      this.state.now.setHours(this.clockItem.hour)
6      this.state.now.setMinutes(this.clockItem.minute)
7    }
8  }

2)页面上的标题需要判断显示

DetailPage.ets
1+    .title((this.clockItem.id ? '编辑' : '添加') + '闹钟')
2    .titleMode(NavigationTitleMode.Mini)
3    .mode(NavigationMode.Stack)

3)在 submit 函数中加入编辑逻辑

DetailPage.ets
1// 2. 记录数据
2      if (this.clockItem.id) {
3        this.clockItem.hour = hour
4        this.clockItem.minute = minute
5        this.clockItem.enabled = true
6        // 不加 await 让它异步执行,因为可能存在失效的代理提醒,这样不会阻碍后续逻辑
7        reminderAgentManager.cancelReminder(this.clockItem.reminderId)
8        this.clockItem.reminderId = reminderId
9        await this.clockStore.addClock(this.clockItem)
10        promptAction.showToast({ message:'编辑闹钟成功' })
11      } else {
12        this.clockItem = {
13          hour,
14          minute,
15          id: util.generateRandomUUID(),
16          reminderId,
17          enabled: true
18        }
19        await this.clockStore.addClock(this.clockItem)
20        promptAction.showToast({ message:'添加闹钟成功' })
21      }

9. 业务逻辑-删除闹钟任务

  • 删除按钮在编辑条件下展示,绑定事件函数

  • 实现删除功能

1)删除按钮在编辑条件下展示,绑定事件函数

DetailPage.ets
1if ( this.clockItem.id ) {
2          Text('删除闹铃')
3            .width(100)
4            .height(40)
5            .borderRadius(20)
6            .backgroundColor('#c3c4c5')
7            .textAlign(TextAlign.Center)
8            .fontColor('#f00')
9            .opacity(0.3)
10            .onClick(() => {
11              this.remove()
12            })
13        }

2)实现删除功能

1async remove () {
2    try {
3      await this.clockStore.delClock(this.clockItem.id)
4      promptAction.showToast({ message: '删除闹钟成功' })
5      // 不加 await 让它异步执行,因为可能存在失效的代理提醒,这样不会阻碍跳转
6      reminderAgentManager.cancelReminder(this.clockItem.reminderId)
7      router.back()
8    } catch (e) {
9      console.error('CLOCK-REMOVE', e.message)
10    }
11  }

10. 业务逻辑-通知唤起应用和关闭延时

  • 抽取创建后台代理工具类
  • 加入唤起配置,加入延时和关闭配置
  • 进行复用

1)加入唤起配置,加入延时和关闭配置

utils/alarmManager.ets
1// 跳转
2      wantAgent: {
3        pkgName: 'com.itcast.myapplication',
4        abilityName: 'EntryAbility'
5      },
6      // 操作按钮
7      actionButton: [
8        { title: '关闭', type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE },
9        { title: '延时提醒', type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_SNOOZE }
10      ],
11      snoozeTimes: 1,
12      timeInterval: 10 * 60

2)抽取创建后台代理工具类

utils/alarmManager.ets
1import reminderAgentManager from '@ohos.reminderAgentManager'
2export class AlarmManager {
3
4  // 添加代理提醒
5  static async addReminder (hour: number, minute: number) {
6    // 1. 添加后台代理提醒-闹钟
7    const reminderRequest: reminderAgentManager.ReminderRequestAlarm = {
8      reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_ALARM,
9      hour,
10      minute,
11      ringDuration: 60,
12      title: '闹铃',
13      // 跳转
14      wantAgent: {
15        pkgName: 'com.itcast.hmday05',
16        abilityName: 'EntryAbility'
17      },
18      // 延时和关闭
19      actionButton: [
20        { title: '关闭', type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE },
21        { title: '延时提醒', type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_SNOOZE }
22      ],
23      snoozeTimes: 2,
24      timeInterval: 10 * 60
25    }
26
27    const reminderId = await reminderAgentManager.publishReminder(reminderRequest)
28    return reminderId
29  }
30}

3)进行复用

pages/DetailPage.ets
1// 1. 开启提醒
2      const hour = this.state.now.getHours()
3      const minute = this.state.now.getMinutes()
4+      const reminderId = await AlarmManager.addReminder(hour, minute)
5      // 2. 记录数据

11. 业务逻辑-停止和开启闹钟任务

ClockItemComp.ets
1clockStore = new ClockStore()
2
3  async toggle(isOn: boolean) {
4    reminderAgentManager.cancelReminder(this.item.reminderId)
5
6    this.item.enabled = isOn
7    if (this.item.enabled) {
8      const reminderId = await AlarmManager.createAlarmTask(this.item.hour, this.item.minute)
9      this.item.reminderId = reminderId
10    }
11    this.clockStore.addClock(this.item)
12    promptAction.showToast({ message: isOn ? '已开启' : '已关闭' })
13  }
ClockItemComp.ets
1// 开关
2      Toggle({ type: ToggleType.Switch, isOn: this.item.enabled })
3        .onChange(isOn => this.toggle(isOn))