鸿蒙-5. 应用程序开发

路由与生命周期

1. 路由-常用API

如何新建页面,如何跳转和回退操作

1)新建页面

pages/DetailPage.ets
1@Entry
2@Component
3struct DetailPage {
4  build() {
5    Column({ space: 15 }) {
6      Text('Detail Page')
7        .fontSize(40)
8      Button('Back')
9    }
10    .height('100%')
11    .width('100%')
12    .justifyContent(FlexAlign.Center)
13  }
14}
src/main/resources/base/profile/main_pages.json
1{
2  "src": [
3    "pages/Index",
4+    "pages/DetailPage"
5  ]
6}
TIP
  • 手动新建一个页面(ets)文件,需要在 main_pages.json 中手动配置
  • 可以自动创建

2)跳转与后退 API

  • 跳转 router.pushUrl()
  • 后退 router.back()
  • 替换跳转 router.replaceUrl()
1import router from '@ohos.router'
2@Entry
3@Component
4struct Index {
5  build() {
6    Column({ space: 15 }) {
7      Text('Index Page')
8        .fontSize(40)
9      Button('Jump To Detail Page')
10        .onClick(() => {
11          // 1. 跳转,压入页面栈顶部
12          // router.pushUrl({
13          //   url: 'pages/DetailPage'
14          // })
15
16          // 2. 跳转,替换当前页面栈
17          // router.replaceUrl({
18          //   url: 'pages/DetailPage'
19          // })
20
21          // 3. 返回
22          // router.back()
23        })
24    }
25    .height('100%')
26    .width('100%')
27    .justifyContent(FlexAlign.Center)
28  }
29}
TIP
  • 页面栈的最大容量为32个页面。如果超过这个限制,可以调用 router.clear() 方法清空历史页面栈,释放内存空间。

2. 路由-参数传递

页面参数传递和获取

1)传参

pages/Index.ets
1import router from '@ohos.router'
2
3class User {
4  name: string
5  age: number
6}
7
8@Entry
9@Component
10struct Index {
11
12  @State
13  user: User = {
14    name: 'jack',
15    age: 18
16  }
17
18  build() {
19    Column({ space: 15 }) {
20      Text('Index Page')
21        .fontSize(40)
22      Button('Jump To Detail Page')
23        .onClick(() => {
24          router.pushUrl({
25            url: 'pages/DetailPage',
26            params: this.user
27          })
28        })
29    }
30    .height('100%')
31    .width('100%')
32    .justifyContent(FlexAlign.Center)
33  }
34}

2)获取

pages/DetailPage.ets
1import router from '@ohos.router'
2import promptAction from '@ohos.promptAction'
3@Entry
4@Component
5struct DetailPage {
6
7  aboutToAppear() {
8    const params = router.getParams()
9    promptAction.showToast({ message: params['name'] + params['age'] })
10  }
11
12  build() {
13    Column({ space: 15 }) {
14      Text('Detail Page')
15        .fontSize(40)
16      Button('Back')
17        .onClick(() => {
18          router.back()
19        })
20    }
21    .height('100%')
22    .width('100%')
23    .justifyContent(FlexAlign.Center)
24  }
25}

3. UIAbility-生命周期

当用户打开、切换和返回到对应应用时,应用中的UIAbility实例会在其生命周期的不同状态之间转换。

  1. UIAbility实例创建完成时触发,系统会调用 onCreate() 回调。

    • 可以在该回调中进行应用初始化操作,例如变量定义资源加载等,用于后续的UI界面展示。
  2. onForeground() 回调,在 UIAbility 的UI界面可见之前,如 UIAbility 切换至前台时触发。

    • 可以在 onForeground() 回调中申请系统需要的资源,或者重新申请在 onBackground() 中释放的资源。
  3. onBackground() 回调,在 UIAbility 的UI界面完全不可见之后,如 UIAbility 切换至后台时候触发。

    • 可以在 onBackground() 回调中释放UI界面不可见时无用的资源,或者在此回调中执行较为耗时的操作,例如状态保存等
  4. Destroy 状态在 UIAbility 实例销毁时触发,系统会调用 onDestroy() 回调。

    • 可以在该回调中进行系统资源的释放、数据的保存等操作。

4. 组件-生命周期

1)任何组件

aboutToAppear

  • aboutToAppear 函数在创建自定义组件的新实例后,在执行其 build 函数之前执行。
  • 允许在 aboutToAppear 函数中改变状态变量,更改将在后续执行 build 函数中生效。

aboutToDisappear

  • aboutToDisappear 函数在自定义组件析构销毁之前执行。

  • 不允许在 aboutToDisappear 函数中改变状态变量,特别是 @Link 变量的修改可能会导致应用程序行为不稳定。

2)仅页面 @Entry 组件

onPageShow

  • 页面每次显示时触发一次,包括路由过程、应用进入前后台等场景,仅 @Entry 修饰的自定义组件生效。

onPageHide

  • 页面每次隐藏时触发一次,包括路由过程、应用进入前后台等场景,仅 @Entry 修饰的自定义组件生效。

onBackPress

  • 当用户点击返回按钮时触发,仅 @Entry 修饰的自定义组件生效。

5. UIAbility跳转

UIAbility组件是一种包含UI界面的应用组件,主要用于和用户交互

  • UIAbility组件是系统调度的基本单元,为应用提供绘制界面的窗口;
  • 一个UIAbility组件中可以通过多个页面来实现一个功能模块;
  • 每一个UIAbility组件实例,都对应于一个最近任务列表中的任务。
PayAbility
1Button('Jump To PayAbility Page')
2  .onClick(() => {
3    const context = getContext(this) as common.UIAbilityContext
4    const want: Want = {
5      bundleName: 'com.itcast.myapplication',
6      abilityName: 'PayAbility'
7    }
8    context.startAbility(want)
9  })
PayAbility
1Button('Back')
2  .onClick(() => {
3    router.back()
4  })
TIP
  • 后续消息通知跳转到应用也需要跳转到对应的 UIAbility 方式类似

使用动画

1. 属性动画

组件的某些通用属性变化时,可以通过属性动画实现渐变过渡效果,提升用户体验。

  • 支持的属性包括width、height、backgroundColor、opacity、scale、rotate、translate等。
1@Entry
2@Component
3struct Index {
4  @State
5  widthSize: number = 100
6  @State
7  heightSize: number = 40
8
9  build() {
10    Column({ space: 15 }) {
11      Button('元素动画')
12        .width(this.widthSize)
13        .height(this.heightSize)
14        .onClick(() => {
15          this.widthSize = 200
16          this.heightSize = 100
17        })
18        .animation({
19          // 动画时间
20          duration: 1000,
21          // 执行次数
22          iterations: -1,
23          // 动画曲线
24          curve: Curve.Ease,
25          // 延时时间
26          delay: 1000,
27          // 播放模式
28          playMode: PlayMode.Alternate
29        })
30    }
31    .height('100%')
32    .width('100%')
33    .justifyContent(FlexAlign.Center)
34  }
35}

2. 显示动画

提供全局animateTo显式动画接口来指定由于闭包代码导致的状态变化插入过渡动效。

1@Entry
2@Component
3struct Index {
4  @State
5  heightSize: number = 40
6
7  build() {
8    Column() {
9      Text('一级菜单')
10        .height(40)
11        .onClick(() => {
12          animateTo({
13            duration: 200
14          }, () => {
15            this.heightSize = this.heightSize === 40 ? 0 : 40
16          })
17        })
18      Column() {
19        Text('一级菜单')
20          .height(this.heightSize)
21        Text('一级菜单')
22          .height(this.heightSize)
23      }
24    }
25    .backgroundColor('#069')
26  }
27}

3. 元素共享转场

当路由进行切换时,可以通过设置组件的 sharedTransition 属性将该元素标记为共享元素并设置对应的共享元素转场动效。

Index.ets
1import router from '@ohos.router'
2@Entry
3@Component
4struct Index {
5  build() {
6    Row({ space: 15 }) {
7      Column({ space: 10 }){
8        Image($rawfile('apple.png'))
9          .width('100%')
10          .aspectRatio(1)
11          .sharedTransition('apple', { duration: 500 })
12        Text('鸣春谷 正宗甘肃特产花牛水果苹果 【天水直发】 4.5-5斤中果A(约13-16个)')
13          .sharedTransition('text', { duration: 500 })
14      }
15      .padding(15)
16      .width('50%')
17      .onClick(() => {
18        router.pushUrl({
19          url: 'pages/DetailPage'
20        })
21      })
22    }
23    .width('100%')
24  }
25}
DetailPage.ets
1@Entry
2@Component
3struct DetailPage {
4  build() {
5    Column({ space: 15 }) {
6      Column({ space: 10 }){
7        Image($rawfile('apple.png'))
8          .width('100%')
9          .aspectRatio(1)
10          .sharedTransition('apple', { duration: 500 })
11        Text('鸣春谷 正宗甘肃特产花牛水果苹果 【天水直发】 4.5-5斤中果A(约13-16个)')
12          .fontSize(18)
13          .sharedTransition('text', { duration: 500 })
14      }
15      .padding(15)
16    }
17    .height('100%')
18    .width('100%')
19  }
20}

4. 拖动手势-阻尼和磁吸

拖动手势(PanGesture)

  • 拖动手势用于触发拖动手势事件,滑动达到最小滑动距离(默认值为5vp)时拖动手势识别成功。

实现下拉刷新效果:

  1. 使用 Stack 堆叠下拉刷新容器和列表容器
  2. 使用手势事件 PanGesture 实现拖动列表容器
  3. 使用 animateTo 实现磁吸动画效果
  4. 拖动距离超出阀值,模拟开启加载效果,控制文字显示和动画
1import promptAction from '@ohos.promptAction'
2@Entry
3@Component
4struct Index {
5  @State
6  translateY: number = 0
7  @State
8  text: string = '下拉即可刷新'
9  @State
10  loading: boolean = false
11
12  ease (originValue: number = 0) {
13    const space = 60
14    const damp = 0.3
15    if ( originValue > space ) {
16      return space + ( originValue - space ) * damp
17    }
18    return originValue
19  }
20
21  build() {
22    Stack({ alignContent: Alignment.Top }) {
23      Row() {
24        if (this.loading) {
25          LoadingProgress()
26            .width(32)
27            .aspectRatio(1)
28        }
29        Text(this.text)
30          .fontColor('#999')
31          .width(100)
32      }
33      .height(100)
34
35      List() {
36
37      }
38      .backgroundColor('#fff')
39      .height('100%')
40      .width('100%')
41      .translate({ y: this.translateY })
42      .gesture(
43        PanGesture()
44          .onActionUpdate((event: GestureEvent) => {
45            this.translateY = this.ease(event.offsetY)
46            if ( this.translateY > 100 ) {
47              this.text = '释放立即刷新'
48            }
49          })
50          .onActionEnd((event: GestureEvent) => {
51            if (this.translateY > 100) {
52              this.loading = true
53              this.text = '正在刷新中...'
54              animateTo({ duration: 300 }, () => {
55                this.translateY = 100
56              })
57              // 加载数据
58              setTimeout(() => {
59                this.loading = false
60                this.text = ''
61                animateTo({ duration: 300, onFinish: () => this.text = '下拉即可刷新' }, () => {
62                  this.translateY = 0
63                })
64                promptAction.showToast({ message: '刷新成功' })
65              }, 2000)
66            } else {
67              animateTo({ duration: 300 }, () => {
68                this.translateY = 0
69              })
70            }
71          })
72      )
73    }
74    .height('100%')
75    .width('100%')
76    .backgroundColor('#f3f4f5')
77  }
78}

系统能力

1. 数据管理-用户首选项

用户首选项为应用提供 Key-Value 键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。

  • Key 键为 string 类型,要求非空且长度不超过 80 个字节。
  • 如果 Value 值为 string 类型,可以为空,不为空时长度不超过 8192 个字节。
  • 内存会随着存储数据量的增大而增大,所以存储的数据量应该是轻量级的,建议存储的数据不超过一万条,否则会在内存方面产生较大的开销。

1)知晓 API 用法

1// 获取首选项实例
2getPreferences(context: Context, name: string): Promise<Preferences>
3
4// 首选项实→获取数据
5get(key: string, defValue: ValueType): Promise<ValueType>
6// 首选项实→设置|修改数据
7put(key: string, value: ValueType): Promise<void>
8// 首选项实→删除数据
9delete(key: string): Promise<void>

2)写一个存储用户的工具

utils/userStore.ets
1import preferences from '@ohos.data.preferences'
2
3export class User {
4  name?: string
5  age?: number
6}
7
8export class UserStore {
9  KEY = 'user-store'
10
11  getStore() {
12    return preferences.getPreferences(getContext(this), 'userStore')
13  }
14
15  async setData(data: User) {
16    const store = await this.getStore()
17    await store.put(this.KEY, JSON.stringify(data))
18    await store.flush()
19  }
20
21  async getData() {
22    const store = await this.getStore()
23    const data = await store.get(this.KEY, '{}') as string
24    return JSON.parse(data) as User
25  }
26
27  async delData() {
28    const store = await this.getStore()
29    await store.delete(this.KEY)
30    await store.flush()
31  }
32
33}
pages/Index.ets
1import router from '@ohos.router'
2import { User, UserStore } from '../utils/userStore'
3
4@Entry
5@Component
6struct Index {
7  userStore: UserStore
8  @State
9  user: User = {}
10
11  aboutToAppear() {
12    this.userStore = new UserStore()
13    this.getUser()
14  }
15
16  async getUser () {
17    const user = await this.userStore.getData()
18    this.user = user
19  }
20
21  build() {
22    Column({ space: 15 }) {
23
24      if ( this.user?.name && this.user?.age ) {
25        Text(`${ this.user.name } 今年 ${ this.user.age }`)
26      }
27
28      Button('设置 User')
29        .onClick(async () => {
30          await this.userStore.setData({ name: 'jack', age: 18 })
31          await this.getUser()
32        })
33
34      Button('修改 User')
35        .onClick(async () => {
36          await this.userStore.setData({ name: 'tom', age: 17 })
37          await this.getUser()
38        })
39
40      Button('删除 User')
41        .onClick(async () => {
42          await this.userStore.delData()
43          await this.getUser()
44        })
45
46      Button('换一页')
47        .onClick(() => {
48          router.pushUrl({
49            url: 'pages/DetailPage'
50          })
51        })
52    }
53    .height('100%')
54    .width('100%')
55    .justifyContent(FlexAlign.Center)
56  }
57}

2. 绘画能力-画布组件

1)了解 画布 基本用法

1@Entry
2@Component
3struct Index{
4
5  // 抗锯齿设置
6  private settings: RenderingContextSettings = new RenderingContextSettings(true)
7  // 画布实例
8  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
9
10  build() {
11    Column({ space: 15 }) {
12      // 画布组件
13      Canvas(this.context)
14        .width(252)
15        .aspectRatio(1)
16        .backgroundColor('#ccc')
17        .onReady(() => {
18          // 绘制边框盒子
19          this.context.strokeRect(100, 100, 50, 50)
20          // 绘制填充盒子
21          this.context.fillRect(150, 150, 50, 50)
22        })
23    }
24    .height('100%')
25    .width('100%')
26    .justifyContent(FlexAlign.Center)
27  }
28}

2)绘制表盘和秒针

前置知识:

  • drawImage(img: ImageBitmap, x, y, w, h) 图片需要 ImageBitmap 格式
  • rotate(angle: number) angle 是弧度,1° = Math.PI / 180,围绕 0 0 中心旋转
1@Entry
2@Component
3struct Index{
4
5  private settings: RenderingContextSettings = new RenderingContextSettings(true)
6  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
7  private panImg: ImageBitmap = new ImageBitmap('/images/ic_clock_pan.png')
8  private secondImg: ImageBitmap = new ImageBitmap('/images/ic_second_pointer.png')
9
10
11  build() {
12    Column({ space: 15 }) {
13      Canvas(this.context)
14        .width(252)
15        .aspectRatio(1)
16        .onReady(() => {
17          // 参数:图片、绘制原点x坐标、绘制原点y坐标、图片绘制宽度、图片绘制高度
18          this.context.drawImage(this.panImg, 0 ,0 , 252, 252)
19
20          this.context.translate(126, 126)
21          this.context.rotate(Math.PI / 180 * 180)
22          this.context.translate(-126, -126)
23          this.context.drawImage(this.secondImg, 122, 0, 8, 252)
24        })
25    }
26    .height('100%')
27    .width('100%')
28    .justifyContent(FlexAlign.Center)
29  }
30}

3. 公共事件与通知-通知管理

本模块提供通知管理的能力,包括发布、取消发布通知,创建、获取、移除通知通道,获取通知的使能状态、角标使能状态,获取通知的相关信息等。

参考文档 链接

1)发送通知和取消通知

1import notificationManager from '@ohos.notificationManager'
2
3@Entry
4@Component
5struct Index {
6  notifyId = 100
7
8  build() {
9    Column({ space: 15 }) {
10      Button('添加通知')
11        .onClick(async () => {
12          const request: notificationManager.NotificationRequest = {
13            id: this.notifyId,
14            content: {
15              contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
16              normal: {
17                title: "温馨提示",
18                text: "您的京东快递 100321120341233 快递已经达到北京!",
19                additionalText: "物流提醒"
20              }
21            },
22          }
23          notificationManager.publish(request)
24        })
25      Button('取消通知')
26        .onClick(() => {
27          notificationManager.cancel(this.notifyId)
28        })
29    }
30    .height('100%')
31    .width('100%')
32    .justifyContent(FlexAlign.Center)
33  }
34}
TIP
  • 运行上面通知如果遇到闪崩现象,告知 Invalid Parameter 的话,请擦除模拟器数据。

2)通知点击唤起应用

1import wantAgent from '@ohos.app.ability.wantAgent'
1// 获取 wantAgent
2const want = await wantAgent.getWantAgent({
3  wants: [
4    {
5      bundleName: 'com.itcast.myapplication',
6      abilityName: 'EntryAbility'
7    }
8  ],
9  operationType: wantAgent.OperationType.START_ABILITY,
10  requestCode: 0
11})
1const request: notificationManager.NotificationRequest = {
2  id: this.notifyId,
3  content: {
4    contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
5    normal: {
6      title: "温馨提示",
7      text: "您的京东快递 100321120341233 快递已经达到北京!",
8      additionalText: "物流提醒"
9    }
10  },
11  // 跳转应用
12+  wantAgent: want
13}

4. 后台任务-后台代理提醒

应用退到后台或进程终止后,仍然有一些提醒用户的定时类任务,
例如购物类应用抢购提醒等,为满足此类功能场景,系统提供了代理提醒(reminderAgentManager)的能力。

参考文档 链接

  • 倒计时类:基于倒计时的提醒功能
  • 日历类:基于日历的提醒功能
  • 闹钟类:基于时钟的提醒功能

需要开启 ohos.permission.PUBLISH_AGENT_REMINDER 权限

1)闹铃

1import reminderAgentManager from '@ohos.reminderAgentManager'
2import promptAction from '@ohos.promptAction'
3
4@Entry
5@Component
6struct Index {
7  reminderId = null
8
9  build() {
10    Column({ space: 15 }) {
11      Button('添加闹铃')
12        .onClick(async () => {
13          let targetReminderAgent: reminderAgentManager.ReminderRequestAlarm = {
14            // 提醒类型为闹钟类型
15            reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_ALARM, 
16            // 指明提醒的目标时刻
17            hour: 19, 
18            // 指明提醒的目标分钟
19            minute: 57, 
20            // 指明每周哪几天需要重复提醒
21            daysOfWeek: [], 
22            // 设置弹出的提醒通知信息上显示的按钮类型和标题
23            actionButton: [ 
24              {
25                title: '停止',
26                type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE
27              },
28            ],
29            // 响铃时长秒
30            ringDuration: 60, 
31            // 点击通知跳转的 want
32            wantAgent: {
33              pkgName: 'com.itcast.myapplication',
34              abilityName: 'EntryAbility',
35            },
36          }
37          // 记录id 清理使用
38          this.reminderId = await reminderAgentManager.publishReminder(targetReminderAgent)
39          promptAction.showToast({ message: '添加闹铃成功' })
40        })
41      Button('取消闹铃')
42        .onClick(async () => {
43          // 不能退出应用,否则 this.reminderId 是 null,开发的时候可以持久化
44          await reminderAgentManager.cancelReminder(this.reminderId)
45          promptAction.showToast({ message: '取消闹铃成功' })
46        })
47    }
48    .height('100%')
49    .width('100%')
50    .justifyContent(FlexAlign.Center)
51  }
52}