diff --git a/zh-cn/third-party-cases/Readme-CN.md b/zh-cn/third-party-cases/Readme-CN.md index 08e39e036dbbea1637eff05d97a44ed8649d9880..99ed832ca812f62074780f4b3e907d2a2caef77b 100644 --- a/zh-cn/third-party-cases/Readme-CN.md +++ b/zh-cn/third-party-cases/Readme-CN.md @@ -28,6 +28,8 @@ - [列表上拉加载更多内容](list-pullup-loading-data.md) - [如何删除多选框选项](delete-checkboxgroup-items.md) - [像素单位转换](pixel-format-transfer.md) +- [如何在UIAbility间进行跳转](jump-between-UIAbilities.md) +- [转场动画](transition-animation.md) ### 装饰器 - [控制页面刷新范围](overall-and-part-refresh.md) @@ -40,6 +42,7 @@ - [如何实现沉浸模式](immersion-mode.md) - [如何创建悬浮窗](float-window.md) - [保持屏幕常亮](keep-screen-on.md) +- [如何创建子窗口并与主窗口通信](subwindow-mainwindow-communication.md) ### 数据管理 - [用户首选项的基本使用](preferences-data-process.md) diff --git a/zh-cn/third-party-cases/figures/BottomTransition.gif b/zh-cn/third-party-cases/figures/BottomTransition.gif new file mode 100644 index 0000000000000000000000000000000000000000..56f2ce23cd8c234ec36f5b8e4b92a26d6d893e0f Binary files /dev/null and b/zh-cn/third-party-cases/figures/BottomTransition.gif differ diff --git a/zh-cn/third-party-cases/figures/ComponentTransition.gif b/zh-cn/third-party-cases/figures/ComponentTransition.gif new file mode 100644 index 0000000000000000000000000000000000000000..8f7d0c6387845cf65f7eb99950648a9a711fdfad Binary files /dev/null and b/zh-cn/third-party-cases/figures/ComponentTransition.gif differ diff --git a/zh-cn/third-party-cases/figures/CustomTransition.gif b/zh-cn/third-party-cases/figures/CustomTransition.gif new file mode 100644 index 0000000000000000000000000000000000000000..d598c4036b1750c2c30f319e53834621ac778f91 Binary files /dev/null and b/zh-cn/third-party-cases/figures/CustomTransition.gif differ diff --git a/zh-cn/third-party-cases/figures/FullCustomTransition.gif b/zh-cn/third-party-cases/figures/FullCustomTransition.gif new file mode 100644 index 0000000000000000000000000000000000000000..f68d2d82884f14dcfc1598be31c4cf475f421b0e Binary files /dev/null and b/zh-cn/third-party-cases/figures/FullCustomTransition.gif differ diff --git a/zh-cn/third-party-cases/figures/SharePage.gif b/zh-cn/third-party-cases/figures/SharePage.gif new file mode 100644 index 0000000000000000000000000000000000000000..5f55ec1c2436892080b8bf69e2cd534a1a42de5e Binary files /dev/null and b/zh-cn/third-party-cases/figures/SharePage.gif differ diff --git a/zh-cn/third-party-cases/figures/Transition-animation-tree.png b/zh-cn/third-party-cases/figures/Transition-animation-tree.png new file mode 100644 index 0000000000000000000000000000000000000000..e25c0b5b38e3384f7752465028c1402f7515f380 Binary files /dev/null and b/zh-cn/third-party-cases/figures/Transition-animation-tree.png differ diff --git a/zh-cn/third-party-cases/figures/UIAbility.gif b/zh-cn/third-party-cases/figures/UIAbility.gif new file mode 100644 index 0000000000000000000000000000000000000000..39478eb56793f0b1239d7d90a6e4570e0f6ef9a1 Binary files /dev/null and b/zh-cn/third-party-cases/figures/UIAbility.gif differ diff --git a/zh-cn/third-party-cases/figures/device-module.png b/zh-cn/third-party-cases/figures/device-module.png new file mode 100644 index 0000000000000000000000000000000000000000..dff2bce0c5533294f021598074424eca216b343d Binary files /dev/null and b/zh-cn/third-party-cases/figures/device-module.png differ diff --git a/zh-cn/third-party-cases/figures/subwindow-mainwindow-communication.gif b/zh-cn/third-party-cases/figures/subwindow-mainwindow-communication.gif new file mode 100644 index 0000000000000000000000000000000000000000..bfc9a607db95911482b7c52caa2294fca2c678f2 Binary files /dev/null and b/zh-cn/third-party-cases/figures/subwindow-mainwindow-communication.gif differ diff --git a/zh-cn/third-party-cases/jump-between-UIAbilities.md b/zh-cn/third-party-cases/jump-between-UIAbilities.md new file mode 100644 index 0000000000000000000000000000000000000000..fbc6746e1a65d9a67d7d5ec5f7424e72fd4095f2 --- /dev/null +++ b/zh-cn/third-party-cases/jump-between-UIAbilities.md @@ -0,0 +1,1037 @@ +# UIAbility内和UIAbility间页面的跳转 +## 场景介绍 +UIAbility组件是系统调度的基本单元,为应用提供绘制界面的窗口。一个应用可以包含一个或多个UIAbility组件。例如,在支付应用中,可以将入口功能和收付款功能分别配置为独立的UIAbility。 + +对于开发者而言,可以根据具体场景选择单个还是多个UIAbility,划分建议如下: +* 如果希望在任务视图中看到一个任务,则建议使用一个UIAbility,多个页面的方式。 +* 如果希望在任务视图中看到多个任务,或者需要同时开启多个窗口,则建议使用多个UIAbility开发不同的模块功能。 + +本例即为大家介绍如何基于Stage模型下的UIAbility开发,实现UIAbility内和UIAbility间页面的跳转与数据传递的功能。 + +## 效果呈现 +本例最终效果如下: +![UIAbility](figures/UIAbility.gif) + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: +- IDE: DevEco Studio 4.0 Beta1 +- SDK: Ohos_sdk_public 4.0.7.5 (API Version 10 Beta1) +## 实现思路 +本篇案例是基于Stage模型下的UIAbility开发,实现UIAbility内和UIAbility间页面的跳转。 +* UIAbility内页面的跳转: + entry模块中,通过添加页面路由router来实现,页面路由router根据页面url找到目标页面,从而实现跳转。 +* UIAbility间页面的跳转--跳转到指定UIAbility的首页: + 实现UIAbility间页面的跳转,需要启动另外一个UIAbility,可以通过UIAbilityContext的startAbility的方法来完成。 +* UIAbility间页面的跳转--跳转到指定UIAbility的指定页面(非首页): + 实现跳转到指定UIAbility的指定页面(非首页),就需要在跳转到指定UIAbility的首页的基础上,新建一个Second页面,使用UIAbilityContext.startAbilityForResult来实现。 +## 开发步骤 +由于本例重点介绍UIAbility之间的跳转,所以开发步骤会着重讲解相关实现,不相关的内容不做介绍,全量代码可参考完整代码章节。 +1. 从实现效果看,UIAbility之间的跳转,都是通过点击每个页面的button后实现的,因此我们可以先构建一个按钮点击后调用的方法类:ButtonClickMethod。 + 具体代码如下: + ```ts + // entry/src/main/ets/model/ButtonClickMethod.ets + + ... + // 按钮点击后调用的方法类 + Class ButtonClickMethod{ + ... + + } + export default new ButtonClickMethod(); + ``` + +2. UIAbility内页面的跳转。 + * 实现UIAbility内页面的跳转,首先构建Index页面,Index页面由一个Image组件、两个Text组件、三个Button组件组成。 + 具体代码如下: + ```ts + // entry/src/main/ets/pages/Index.ets + + @Entry + @Component + struct Index { + @State text: string = ''; + + build() { + Column() { + Image($r('app.media.right')) + ... + Text($r('app.string.main_index_page_name')) + ... + // 条件渲染:当text的值不为空时,显示该组件 + if (this.text !== '') { + Text(this.text) + ... + } + // 导航到EntryAbility的Second Page按钮 + Button($r('app.string.to_main_second_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + // 导航到SecondAbility的Index Page按钮 + Button($r('app.string.to_second_index_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + // 导航到SecondAbility的Index Page按钮 + Button($r('app.string.to_second_second_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + } + ... + } + } + ``` + * 构建Second页面,该页面由由一个Image组件、两个Text组件、一个Button组件组成。 + 具体代码如下: + ```ts + // entry/src/main/ets/pages/Second.ets + + @Entry + @Component + struct Second { + ... + + build() { + Column() { + Image($r('app.media.left')) + ... + + Text($r('app.string.main_second_page_name')) + ... + + Text(`${this.src}:${this.count}`) + ... + + // 返回到EntryAbility的Index Page按钮 + Button($r('app.string.back_main_index_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + } + ... + } + } + ``` + * entry模块的Index和Second页面之间的跳转以及数据的传递,需要通过router来实现。 + * 从EntryAbility首页跳转到Second页面: + 1. 导入router模块, 向按钮点击后调用的方法类ButtonClickMethod中添加toEntryAbilitySecond方法,使用router.pushUrl实现跳转,同时通过params来向新页面传入参数。 + 具体代码如下: + ```ts + // entry/src/main/ets/model/ButtonClickMethod.ets + import router from '@ohos.router'; + + // 按钮点击后调用的方法类 + Class ButtonClickMethod{ + // 导航entry模块的Second页面 + toEntryAbilitySecond() { + router.pushUrl({ + url: 'pages/Second', + params: { + src: textMessage, + count: CommonConstants.NUM_VALUES[0] + } + }); + } + ... + + } + export default new ButtonClickMethod(); + ``` + 2. 点击“导航到EntryAbility的Second Page”按钮后,调用ButtonClickMethod类中的toEntryAbilitySecond方法,跳转到EntryAbility的Second页面。 + 具体代码如下: + ```ts + // entry/src/main/ets/pages/Index.ets + ... + + @Entry + @Component + struct Index { + @State text: string = ''; + @State bottomMargin: string = StyleConstants.MAIN_INDEX_BUTTON_MARGIN_BOTTOM; + + build() { + Column() { + Image($r('app.media.right')) + ... + Text($r('app.string.main_index_page_name')) + ... + // 条件渲染:当text的值不为空时,显示该组件 + if (this.text !== '') { + Text(this.text) + ... + } + + Button($r('app.string.to_main_second_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + .onClick(() => { + // 导航到EntryAbility的Second Page + ButtonClickMethod.toSecondAbilityIndex(context); + this.text = ''; + this.bottomMargin = StyleConstants.MAIN_INDEX_BUTTON_MARGIN_BOTTOM; + }) + + Button($r('app.string.to_second_index_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + + Button($r('app.string.to_second_second_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + } + ... + } + } + ``` + * 从entry模块的EntryAbility的Second页面返回至EntryAbility首页: + 向EntryAbility的Second页面导入router模块,同时给button添加oncClick事件,使用router.back实现返回至EntryAbility的index页面。 + 具体代码如下: + ```ts + // entry/src/main/ets/pages/Second.ets + import router from '@ohos.router'; + + @Entry + @Component + struct Second { + ... + + build() { + Column() { + Image($r('app.media.left')) + ... + + Text($r('app.string.main_second_page_name')) + ... + + Text(`${this.src}:${this.count}`) + ... + + Button($r('app.string.back_main_index_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + .onClick(() => { + // 返回到EntryAbility的Index Page + router.back(); + }) + } + ... + } + } + ``` + +3. 跳转到指定的UIAbility的首页。 + * 实现跳转到指定UIAbility的首页,先构建另外一个模块,方法如下: + 在“Project”窗口,右键点击“entry 文件夹”,选择“New > Module > Empty Ability > Next”,在“Module name”中给新建的模块命名为“device”,点击“Next”,在“Ability name”中给新建模块的Ability命名为“SecondAbility”,点击“Finish”。可以看到文件目录结构如下: + ![device-modules](figures/device-module.png) + * 构建device模块下SecondAbility的Index页面,该页面由一个Image组件、两个Text组件、一个Button组件组成。 + 具体代码如下: + ```ts + // device/src/main/ets/pages/Index.ets + + @Entry + @Component + struct Index { + ... + + build() { + Column() { + Image($r('app.media.left')) + ... + + Text($r('app.string.second_index_page_name')) + ... + + Text(`${this.src}:${this.count}`) + ... + + // 停止SecondAbility自身按钮 + Button($r('app.string.terminate_second_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + } + ... + } + } + ``` + * 从entry模块的EntryAbility首页跳转至device模块的SecondAbility首页:需要通过UIAbilityContext的startAbility方法来实现。 + 1. 在EntryAbility的Index页面获取UIAbilityContext。 + > 使用UIAbilityContext中的方法,需要在对应的页面获取相应的UIAbilityContext。 + + 具体代码如下: + ```ts + // entry/src/main/ets/pages/Index.ets + ... + // 获取UIAbilityContext + let context = getContext(this); + ... + ``` + 2. 在EntryAbility的Index页面中,点击“导航到SecondAbility的Index Page”按钮后,调用ButtonClickMethod类中的toSecondAbilityIndex方法,拉起SecondAbility的Index页面,同时通过params来向新页面传入参数。 + * 向ButtonClickMethod类中添加toSecondAbilityIndex方法。 + 具体代码如下: + ```ts + // entry/src/main/ets/model/ButtonClickMethod.ets + import router from '@ohos.router'; + import Logger from '../common/utils/Logger'; + + // 按钮点击后调用的方法类 + Class ButtonClickMethod{ + ... + // 导航device模块的Index页面 + toSecondAbilityIndex(context) { + let want = { + 'deviceId': '', + 'bundleName': 'com.example.uiability', + 'abilityName': 'SecondAbility', + 'moduleName':'device', + 'parameters': { + src: textMessage, + count: 45 + } + }; + context.startAbility(want).then(() => { + Logger.info(CommonConstants.TAG, `start second ability index page succeed with ${JSON.stringify(want)}`); + }).catch((error) => { + Logger.error(CommonConstants.TAG, `start second ability index page failedwith ${error.code}`); + }); + } + ... + } + export default new ButtonClickMethod(); + ``` + * 在EntryAbility的Index页面中,给“导航到SecondAbility的Index Page”按钮添加onClick事件,调用ButtonClickMethod类中的toSecondAbilityIndex方法,实现到SecondAbility首页的跳转。 + 具体代码如下: + ```ts + // entry/src/main/ets/pages/Index.ets + ... + + // 获取UIAbilityContext + let context = getContext(this); + + @Entry + @Component + struct Index { + @State text: string = ''; + @State bottomMargin: string = StyleConstants.MAIN_INDEX_BUTTON_MARGIN_BOTTOM; + + build() { + Column() { + Image($r('app.media.right')) + ... + Text($r('app.string.main_index_page_name')) + ... + // 条件渲染:当text的值不为空时,显示该组件 + if (this.text !== '') { + Text(this.text) + ... + } + + Button($r('app.string.to_main_second_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + + Button($r('app.string.to_second_index_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + .onClick(() => { + // 导航到SecondAbility的Index页面 + ButtonClickMethod.toSecondAbilityIndex(context); + this.text = ''; + this.bottomMargin = StyleConstants.MAIN_INDEX_BUTTON_MARGIN_BOTTOM; + }) + + Button($r('app.string.to_second_second_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + } + ... + } + } + ``` + * 在SecondAbility的Index页面,获取从EntryAbility的Index页面传递过来的自定义参数,并用一个Text文本展示从Index页面传递过来的数据。 + 具体代码如下: + ```ts + // device/src/main/ets/pages/Index.ets + ... + @Entry + @Component + struct Index { + // 获取从EntryAbility的Index页面传递过来的自定义参数 + @State src: string = globalThis?.secondAbilityWant?.parameters?.src ?? '-'; + @State count: number = globalThis?.secondAbilityWant?.parameters?.count ?? 0; + + build() { + Column() { + Image($r('app.media.left')) + ... + Text($r('app.string.second_index_page_name')) + ... + // 用一个Text文本展示从Index页面传递过来的数据 + Text(`${this.src}:${this.count}`) + ... + // 停止SecondAbility自身按钮 + Button($r('app.string.terminate_second_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + } + ... + } + } + ``` + + * 从device模块的SecondAbility首页返回到entry模块的EntryAbility首页:通过点击device模块的Index页面的“停止SecondAbility自身”按钮,使用UIAbilityContext.terminateSelf方法手动销毁Ability。 + 1. 给ButtonClickMethod类中添加toSecondAbilityIndex方法。 + 具体代码如下: + ```ts + // entry/src/main/ets/model/ButtonClickMethod.ets + import router from '@ohos.router'; + import Logger from '../common/utils/Logger'; + + // 按钮点击后调用的方法类 + Class ButtonClickMethod{ + ... + // 停止SecondAbility自身 + terminateSecondAbility(context) { + context.terminateSelf().then(() => { + Logger.info(CommonConstants.TAG, 'terminate second ability self succeed'); + }).catch((error) => { + Logger.error(CommonConstants.TAG, `terminate second ability self failed with ${error.code}`); + }); + } + ... + } + export default new ButtonClickMethod(); + ``` + 2. 在SecondAbility的Index页面中,给“停止SecondAbility自身”按钮添加onClick事件,调用ButtonClickMethod类中的terminateSecondAbility方法,使用UIAbilityContext.terminateSelf方法手动销毁Ability,从而实现从SecondAbility的Index页面返回至entry的Index页面。 + 具体代码如下: + ```ts + // device/src/main/ets/model/Index.ets + + let context = getContext(this); + ... + @Entry + @Component + struct Index { + // 获取从EntryAbility的Index页面传递过来的自定义参数 + @State src: string = globalThis?.secondAbilityWant?.parameters?.src ?? '-'; + @State count: number = globalThis?.secondAbilityWant?.parameters?.count ?? 0; + + build() { + Column() { + Image($r('app.media.left')) + ... + Text($r('app.string.second_index_page_name')) + ... + // 用一个Text文本展示从EntryAbility的Index页面传递过来的数据 + Text(`${this.src}:${this.count}`) + ... + + Button($r('app.string.terminate_second_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + .onClick(() => { + // 停止SecondAbility自身 + ButtonClickMethod.terminateSecondAbility(context); + }) + } + ... + } + } + ``` + +4. 跳转到指定UIAbility的指定页面(非首页)。 + * 构建device模块下SecondAbility的Second页面。该页面由一个Image组件、两个Text组件、一个Button组件组成。 + 具体代码如下: + ```ts + // device/src/main/ets/pages/Second.ets + + @Entry + @Component + struct Index { + ... + + build() { + Column() { + Image($r('app.media.left')) + ... + + Text($r('app.string.second_second_page_name')) + ... + + // 用一个Text文本展示从EntryAbility的Index页面传递过来的数据 + Text(`${this.src}:${this.count}`) + ... + + // 停止SecondAbility自身且返回结果按钮 + Button($r('app.string.terminate_second_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + } + ... + } + } + ``` + * 从entry模块的EntryAbility的首页跳转至device模块的SecondAbility的Second页面:通过点击“导航到SecondAbility的Second Page”按钮后,调用ButtonClickMethod类中的toSecondAbilitySecond方法,拉起SecondAbility的Second页面。 + 1. 给ButtonClickMethod类中添加toSecondAbilitySecond方法,该方法中使用UIAbilityContext.startAbilityForResult来实现,并获取被拉起侧销毁后的返回结果。可以通过parameters来向被拉起方传递参数。 + 具体代码如下: + ```ts + // entry/src/main/ets/model/ButtonClickMethod.ets + import router from '@ohos.router'; + import Logger from '../common/utils/Logger'; + + let currentContext = getContext(this); + + // 按钮点击后调用的方法类 + Class ButtonClickMethod{ + ... + // 导航到SecondAbility的Second页面 + toSecondAbilitySecond(context, callback) { + let want = { + 'deviceId': '', + 'bundleName': 'com.example.uiability', + 'abilityName': 'SecondAbility', + 'moduleName':'device', + 'parameters': { + url: 'pages/Second', + src: textMessage, + count: 78 + } + }; + + // 被拉起侧销毁后,在startAbilityForResult回调中可以获取到被拉起侧销毁时传递过来的AbilityResult + context.startAbilityForResult(want).then((result) => { + callback(result); + Logger.info(CommonConstants.TAG, `start second ability second page succeed with ${JSON.stringify(want)}`); + }).catch((error) => { + Logger.error(CommonConstants.TAG, `start second ability second page failed with ${error.code}`); + }); + } + ... + } + export default new ButtonClickMethod(); + ``` + 2. 在EntryAbility的Index页面中,给“导航到SecondAbility的Second Page”按钮添加onClick事件,调用ButtonClickMethod类中的toSecondAbilityIndex方法,实现到SecondAbility首页的跳转。 + 具体代码如下: + ```ts + // entry/src/main/ets/pages/Index.ets + ... + + // 获取UIAbilityContext + let context = getContext(this); + + @Entry + @Component + struct Index { + @State text: string = ''; + @State bottomMargin: string = StyleConstants.MAIN_INDEX_BUTTON_MARGIN_BOTTOM; + + build() { + Column() { + Image($r('app.media.right')) + ... + Text($r('app.string.main_index_page_name')) + ... + // 条件渲染:当text的值不为空时,显示该组件 + if (this.text !== '') { + Text(this.text) + ... + } + + Button($r('app.string.to_main_second_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + + Button($r('app.string.to_second_index_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + + Button($r('app.string.to_second_second_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + .onClick(() => { + this.text = ''; + this.bottomMargin = StyleConstants.MAIN_INDEX_BUTTON_MARGIN_BOTTOM; + + // 导航到SecondAbility的Second页面 + ButtonClickMethod.toSecondAbilitySecond(context, (abilityResult) => { + // 获取SecondAbility被销毁时传递的abilityResult + if (abilityResult.resultCode === CommonConstants.RESULT_CODE) { + let src: string = abilityResult?.want?.parameters?.src ?? '-'; + let count: number = abilityResult?.want?.parameters?.count ?? 0; + this.text = `${src}:${count}`; + this.bottomMargin = StyleConstants.BUTTON_MARGIN_BOTTOM; + } + }); + }) + } + ... + } + } + ``` + * 从device模块的SecondAbility的Second页面,返回至entry模块的EntryAbility首页:通过点击“停止SecondAbility自身并返回结果”按钮,调用ButtonClickMethod类中的terminateSecondAbilityForResult方法,使用UIAbilityContext.terminateSelfWithResult方法,同时传入不同的resultCode和want,手动销毁Ability,成功后发起拉起侧会收到abilityResult的值, 通过Text的方式显示在界面上,从而实现从SecondAbility的Second页面返回至entry的Index页面。 + 1. 给ButtonClickMethod类中添加terminateSecondAbilityForResult方法。 + 具体代码如下: + ```ts + // entry/src/main/ets/model/ButtonClickMethod.ets + import router from '@ohos.router'; + import Logger from '../common/utils/Logger'; + + // 按钮点击后调用的方法类 + Class ButtonClickMethod{ + ... + // 停止SecondAbility自身 + terminateSecondAbilityForResult(context) { + let abilityResult = { + resultCode: CommonConstants.RESULT_CODE, + want: { + 'parameters': { + src: returnMessage, + count: 99 + } + } + }; + + // 停止SecondAbility自身,并将abilityResult返回给startAbilityForResult接口调用方 + context.terminateSelfWithResult(abilityResult).then(() => { + Logger.info(CommonConstants.TAG, `terminate second ability self succeed with ${JSON.stringify(abilityResult)}`); + }).catch((error) => { + Logger.error(CommonConstants.TAG, `terminate second ability self failed with ${error.code}`); + }); + } + ... + } + export default new ButtonClickMethod(); + ``` + 2. 在SecondAbility的Index页面中,给“停止SecondAbility自身并返回结果”按钮添加onClick事件,调用ButtonClickMethod类中的terminateSecondAbilityForResult方法,手动销毁自身Ability,。 + 具体代码如下: + ```ts + // device/src/main/ets/pages/Second.ets + let context = getContext(this); + + @Entry + @Component + struct Second { + // 用来接收parameters参数传过来的值 + @State src: string = globalThis?.secondAbilityWant?.parameters?.src ?? '-'; + @State count: number = globalThis?.secondAbilityWant?.parameters?.count ?? 0; + + build() { + Column() { + Image($r('app.media.left')) + ... + + Text($r('app.string.second_second_page_name')) + ... + + Text(`${this.src}:${this.count}`) + .. + + // 停止SecondAbility自身且返回结果按钮 + Button($r('app.string.terminate_second_for_result_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + ... + .onClick(() => { + // 停止SecondAbility自身且返回结果. + ButtonClickMethod.terminateSecondAbilityForResult(context); + }) + } + ... + } + } + ``` +## 完整代码 +本例完整代码如下: +1. entry模块的代码: + * 公共常量类:entry/src/main/ets/common/constants/CommonConstants.ets + 具体代码如下: + ```ts + class CommonConstants { + TAG = '[ButtonClickMethod.ts]'; + RESULT_CODE = 100; + } + export default new CommonConstants(); + + ``` + * 样式常量类:entry/src/main/ets/common/constants/StyleConstants.ets + 具体代码如下: + ```ts + class StyleConstants { + IMAGE_WIDTH = '78%'; + + IMAGE_HEIGHT = '25%'; + + IMAGE_MARGIN_TOP = '140vp'; + + IMAGE_MARGIN_BOTTOM = '55vp'; + + MAIN_INDEX_TEXT_MARGIN_BOTTOM = '148vp'; + + BUTTON_WIDTH = '87%'; + + BUTTON_HEIGHT = '5%'; + + BUTTON_MARGIN_BOTTOM = '12vp'; + + MAIN_INDEX_BUTTON_MARGIN_BOTTOM = '179vp'; + + TEXT_MARGIN_BOTTOM = '250vp'; + + FULL_PERCENT = '100%'; + + FONT_SIZE_BIG = 20; + + FONT_WEIGHT = 500; + + FONT_SIZE_SMALL = 16; + + OPACITY = 0.6; + } + export default new StyleConstants(); + ``` + * 按钮点击后调用的方法类:entry/src/main/ets/model/ButtonClickMethod.ets + 具体代码如下: + ```ts + import router from '@ohos.router'; + import Logger from '../common/utils/Logger'; + import CommonConstants from '../common/constants/CommonConstants'; + + let currentContext = getContext(this); + let textMessage: string = currentContext.resourceManager.getStringSync($r('app.string.text_message')); + let returnMessage: string = currentContext.resourceManager.getStringSync($r('app.string.return_message')); + + // 按钮点击后调用的方法类 + class ButtonClickMethod { + // 导航entry模块的Second页面 + toEntryAbilitySecond() { + router.pushUrl({ + url: 'pages/Second', + params: { + src: textMessage, + count: 12 + } + }); + } + + // 导航device模块的Index页面 + toSecondAbilityIndex(context) { + let want = { + 'deviceId': '', + 'bundleName': 'com.example.uiability', + 'abilityName': 'SecondAbility', + 'moduleName':'device', + + 'parameters': { + src: textMessage, + count: 45 + } + }; + context.startAbility(want).then(() => { + Logger.info(CommonConstants.TAG, `start second ability index page succeed with ${JSON.stringify(want)}`); + }).catch((error) => { + Logger.error(CommonConstants.TAG, `start second ability index page failedwith ${error.code}`); + }); + } + + // 导航到SecondAbility的Second页面 + toSecondAbilitySecond(context, callback) { + let want = { + 'deviceId': '', + 'bundleName': 'com.example.uiability', + 'abilityName': 'SecondAbility', + 'moduleName':'device', + 'parameters': { + url: 'pages/Second', + src: textMessage, + count: 78 + } + }; + + // 被拉起侧销毁后,在startAbilityForResult回调中可以获取到被拉起侧销毁时传递过来的AbilityResult + context.startAbilityForResult(want).then((result) => { + callback(result); + Logger.info(CommonConstants.TAG, `start second ability second page succeed with ${JSON.stringify(want)}`); + }).catch((error) => { + Logger.error(CommonConstants.TAG, `start second ability second page failed with ${error.code}`); + }); + } + + // 停止SecondAbility自身 + terminateSecondAbility(context) { + context.terminateSelf().then(() => { + Logger.info(CommonConstants.TAG, 'terminate second ability self succeed'); + }).catch((error) => { + Logger.error(CommonConstants.TAG, `terminate second ability self failed with ${error.code}`); + }); + } + + // 停止SecondAbility自身并返回结果 + terminateSecondAbilityForResult(context) { + let abilityResult = { + resultCode: CommonConstants.RESULT_CODE, + want: { + 'parameters': { + src: returnMessage, + count: 99 + } + } + }; + + // 停止SecondAbility自身,并将abilityResult返回给startAbilityForResult接口调用方 + context.terminateSelfWithResult(abilityResult).then(() => { + Logger.info(CommonConstants.TAG, `terminate second ability self succeed with ${JSON.stringify(abilityResult)}`); + }).catch((error) => { + Logger.error(CommonConstants.TAG, `terminate second ability self failed with ${error.code}`); + }); + } + } + + export default new ButtonClickMethod(); + ``` + * EntryAbility的Index页面:entry/src/main/ets/pages/Index.ets + 具体代码如下: + ```ts + import ButtonClickMethod from '../model/ButtonClickMethod'; + import StyleConstants from '../common/constants/StyleConstants'; + import CommonConstants from '../common/constants/CommonConstants'; + + // 获取EntryAbility的UIAbilityContext + let context = getContext(this); + + @Entry + @Component + struct Index { + @State text: string = ''; + @State bottomMargin: string = StyleConstants.MAIN_INDEX_BUTTON_MARGIN_BOTTOM; + + build() { + Column() { + Image($r('app.media.right')) + .objectFit(ImageFit.Contain) + .width(StyleConstants.IMAGE_WIDTH) + .height(StyleConstants.IMAGE_HEIGHT) + .margin({ + top: StyleConstants.IMAGE_MARGIN_TOP, + bottom: StyleConstants.IMAGE_MARGIN_BOTTOM + }) + + Text($r('app.string.main_index_page_name')) + .fontColor('#000') + .fontSize(StyleConstants.FONT_SIZE_BIG) + .fontWeight(StyleConstants.FONT_WEIGHT) + .margin({ bottom: this.bottomMargin }) + + // 条件渲染:当text的值不为空时,显示该组件 + if (this.text !== '') { + Text(this.text) + .fontColor('#000') + .fontSize(StyleConstants.FONT_SIZE_SMALL) + .opacity(StyleConstants.OPACITY) + .margin({ bottom: StyleConstants.MAIN_INDEX_TEXT_MARGIN_BOTTOM }) + } + + // 导航到EntryAbility的Second Page按钮 + Button($r('app.string.to_main_second_page_btn_text'),{ type: ButtonType.Capsule, stateEffect: true }) + .backgroundColor($r('app.color.button_background_color')) + .width(StyleConstants.BUTTON_WIDTH) + .height(StyleConstants.BUTTON_HEIGHT) + .margin({ bottom: StyleConstants.BUTTON_MARGIN_BOTTOM }) + .onClick(() => { + // 导航到EntryAbility的Second page + ButtonClickMethod.toEntryAbilitySecond(); + this.text = ''; + this.bottomMargin = StyleConstants.MAIN_INDEX_BUTTON_MARGIN_BOTTOM; + }) + + // 导航到SecondAbility的Index Page按钮 + Button($r('app.string.to_second_index_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + .backgroundColor($r('app.color.button_background_color')) + .width(StyleConstants.BUTTON_WIDTH) + .height(StyleConstants.BUTTON_HEIGHT) + .margin({ bottom: StyleConstants.BUTTON_MARGIN_BOTTOM }) + .onClick(() => { + // 导航到SecondAbility的Index Page + ButtonClickMethod.toSecondAbilityIndex(context); + this.text = ''; + this.bottomMargin = StyleConstants.MAIN_INDEX_BUTTON_MARGIN_BOTTOM; + }) + + // 导航到SecondAbility的Index Page按钮 + Button($r('app.string.to_second_second_page_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + .backgroundColor($r('app.color.button_background_color')) + .width(StyleConstants.BUTTON_WIDTH) + .height(StyleConstants.BUTTON_HEIGHT) + .onClick(() => { + this.text = ''; + this.bottomMargin = StyleConstants.MAIN_INDEX_BUTTON_MARGIN_BOTTOM; + // 导航到SecondAbility的Second Page + ButtonClickMethod.toSecondAbilitySecond(context, (abilityResult) => { + // 获取SecondAbility被销毁时传递的abilityResult + if (abilityResult.resultCode === CommonConstants.RESULT_CODE) { + let src: string = abilityResult?.want?.parameters?.src ?? '-'; + let count: number = abilityResult?.want?.parameters?.count ?? 0; + this.text = `${src}:${count}`; + this.bottomMargin = StyleConstants.BUTTON_MARGIN_BOTTOM; + } + }); + }) + } + .width(StyleConstants.FULL_PERCENT) + .height(StyleConstants.FULL_PERCENT) + .backgroundColor($r('app.color.background_color')) + } + } + ``` + * EntryAbility的Second页面:entry/src/main/ets/pages/Second.ets + 具体代码如下: + ```ts + import router from '@ohos.router'; + import StyleConstants from '../common/constants/StyleConstants'; + + @Entry + @Component + struct Second { + @State src: string = router?.getParams()?.['src'] ?? '-'; + @State count: number = router?.getParams()?.['count'] ?? 0; + + build() { + Column() { + Image($r('app.media.left')) + .objectFit(ImageFit.Contain) + .width(StyleConstants.IMAGE_WIDTH) + .height(StyleConstants.IMAGE_HEIGHT) + .margin({ + top: StyleConstants.IMAGE_MARGIN_TOP, + bottom: StyleConstants.IMAGE_MARGIN_BOTTOM + }) + + Text($r('app.string.main_second_page_name')) + .fontColor('#000') + .fontSize(StyleConstants.FONT_SIZE_BIG) + .fontWeight(StyleConstants.FONT_WEIGHT) + .margin({ bottom: StyleConstants.BUTTON_MARGIN_BOTTOM }) + + // 用一个Text文本展示从EntryAbility的Index页面传递过来的数据 + Text(`${this.src}:${this.count}`) + .fontColor('ccc') + .fontSize(StyleConstants.FONT_SIZE_SMALL) + .opacity(StyleConstants.OPACITY) + .margin({ bottom: StyleConstants.TEXT_MARGIN_BOTTOM }) + + // 返回到EntryAbility的Index Page按钮 + Button($r('app.string.back_main_index_page_btn_text'),{ type: ButtonType.Capsule, stateEffect: true }) + .backgroundColor($r('app.color.button_background_color')) + .width(StyleConstants.BUTTON_WIDTH) + .height(StyleConstants.BUTTON_HEIGHT) + .onClick(() => { + // 返回到EntryAbility的Index Page + router.back(); + }) + } + .width(StyleConstants.FULL_PERCENT) + .height(StyleConstants.FULL_PERCENT) + .backgroundColor($r('app.color.background_color')) + } + } + ``` +2. device模块的代码: + * SecondAbility的Index页面:device/src/main/ets/pages/Index.ets + 具体代码如下: + ```ts + import ButtonClickMethod from '../../../../../entry/src/main/ets/model/ButtonClickMethod'; + import StyleConstants from '../../../../../entry/src/main/ets/common/constants/StyleConstants'; + + // 获取SecondAbility的UIAbilityContext + let context = getContext(this); + + @Entry + @Component + struct Index { + // 获取从EntryAbility的Index页面传递过来的自定义参数 + @State src: string = globalThis?.secondAbilityWant?.parameters?.src ?? '-'; + @State count: number = globalThis?.secondAbilityWant?.parameters?.count ?? 0; + + build() { + Column() { + Image($r('app.media.left')) + .objectFit(ImageFit.Contain) + .width(StyleConstants.IMAGE_WIDTH) + .height(StyleConstants.IMAGE_HEIGHT) + .margin({ + top: StyleConstants.IMAGE_MARGIN_TOP, + bottom: StyleConstants.IMAGE_MARGIN_BOTTOM + }) + + // 用一个Text文本展示从EntryAbility的Index页面传递过来的数据 + Text($r('app.string.second_index_page_name')) + .fontColor('#000') + .fontSize(StyleConstants.FONT_SIZE_BIG) + .fontWeight(StyleConstants.FONT_WEIGHT) + .margin({ bottom: StyleConstants.BUTTON_MARGIN_BOTTOM }) + + // 用一个Text文本展示从Index页面传递过来的数据 + Text(`${this.src}:${this.count}`) + .fontColor('#000') + .fontSize(StyleConstants.FONT_SIZE_SMALL) + .opacity(StyleConstants.OPACITY) + .margin({ bottom: StyleConstants.TEXT_MARGIN_BOTTOM }) + + // 停止SecondAbility自身按钮 + Button($r('app.string.terminate_second_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + .backgroundColor($r('app.color.button_background_color')) + .width(StyleConstants.BUTTON_WIDTH) + .height(StyleConstants.BUTTON_HEIGHT) + .onClick(() => { + // 停止SecondAbility自身 + ButtonClickMethod.terminateSecondAbility(context); + }) + } + .width(StyleConstants.FULL_PERCENT) + .height(StyleConstants.FULL_PERCENT) + .backgroundColor($r('app.color.background_color')) + } + } + ``` + * SecondAbility的Second页面:device/src/main/ets/pages/Second.ets + 具体代码如下: + ```ts + import ButtonClickMethod from '../../../../../entry/src/main/ets/model/ButtonClickMethod'; + import StyleConstants from '../../../../../entry/src/main/ets/common/constants/StyleConstants'; + + let context = getContext(this); + + @Entry + @Component + struct Second { + // 用来接收parameters参数传过来的值 + @State src: string = globalThis?.secondAbilityWant?.parameters?.src ?? '-'; + @State count: number = globalThis?.secondAbilityWant?.parameters?.count ?? 0; + + build() { + Column() { + Image($r('app.media.left')) + .objectFit(ImageFit.Contain) + .width(StyleConstants.IMAGE_WIDTH) + .height(StyleConstants.IMAGE_HEIGHT) + .margin({ + top: StyleConstants.IMAGE_MARGIN_TOP, + bottom: StyleConstants.IMAGE_MARGIN_BOTTOM + }) + + Text($r('app.string.second_second_page_name')) + .fontColor('#000') + .fontSize(StyleConstants.FONT_SIZE_BIG) + .fontWeight(StyleConstants.FONT_WEIGHT) + .margin({ bottom: StyleConstants.BUTTON_MARGIN_BOTTOM }) + + // 用一个Text文本展示从EntryAbility的Index页面传递过来的数据 + Text(`${this.src}:${this.count}`) + .fontColor('#000') + .fontSize(StyleConstants.FONT_SIZE_SMALL) + .opacity(StyleConstants.OPACITY) + .margin({ bottom: StyleConstants.TEXT_MARGIN_BOTTOM }) + + // 停止SecondAbility自身且返回结果按钮 + Button($r('app.string.terminate_second_for_result_btn_text'), { type: ButtonType.Capsule, stateEffect: true }) + .backgroundColor($r('app.color.button_background_color')) + .width(StyleConstants.BUTTON_WIDTH) + .height(StyleConstants.BUTTON_HEIGHT) + .onClick(() => { + // 停止SecondAbility自身且返回结果 + ButtonClickMethod.terminateSecondAbilityForResult(context); + }) + } + .width(StyleConstants.FULL_PERCENT) + .height(StyleConstants.FULL_PERCENT) + .backgroundColor($r('app.color.background_color')) + } + } + ``` + +## 参考 + +- [UIAbility组件概述](../application-dev/application-models/uiability-overview.md) + +- [UIAbility](../application-dev/reference/apis/js-apis-app-ability-uiAbility.md) + +- [UIAbilityContext](../application-dev/reference/apis/js-apis-inner-application-uiAbilityContext.md) + +- [页面路由](../application-dev/reference/apis/js-apis-router.md) + +- [Want](../application-dev/reference/apis/js-apis-application-want.md) diff --git a/zh-cn/third-party-cases/subwindow-mainwindow-communication.md b/zh-cn/third-party-cases/subwindow-mainwindow-communication.md new file mode 100644 index 0000000000000000000000000000000000000000..93addc52110daf57f4a97683646ece20e0b5bcda --- /dev/null +++ b/zh-cn/third-party-cases/subwindow-mainwindow-communication.md @@ -0,0 +1,414 @@ +# 如何创建子窗口并与主窗口通信 + +## 场景介绍 +应用开发过程中,经常需要创建弹窗(子窗口)用来承载跟当前内容相关的业务,比如电话应用的拨号弹窗;阅读应用中长按当前内容触发的编辑弹窗;购物应用经常出现的抽奖活动弹窗等。 +本文为大家介绍如何创建子窗口并实现子窗口与主窗口的数据通信。 + +## 效果呈现 +本例最终效果如下: +![subwindow-mainwindow-communication](figures/subwindow-mainwindow-communication.gif) + +## 环境要求 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: + +- IDE: DevEco Studio 4.0 Beta1 +- SDK: Ohos_sdk_public 4.0.7.5 (API Version 10 Beta1) + + +## 实现思路 +本例关键特性及实现方案如下: +- 点击“创建子窗口”按钮创建子窗口:使用window模块的createSubWindow方法创建子窗口,在创建时设置子窗口的大小、位置、内容等。 +- 子窗口可以拖拽:通过gesture属性为子窗口绑定PanGesture拖拽事件,使用moveWindowTo方法将窗口移动到拖拽位置,呈现拖拽效果。 +- 点击主窗口的“子窗口数据+1”按钮,子窗口中的数据加1,反之亦然,即实现主窗口和子窗口间的数据通信:将数据变量存储在AppStorage中,在主窗口和子窗口中引用该数据,并通过@StorageLink与AppStorage中的数据进行双向绑定,从而实现主窗口和子窗口之间的数据联动。 +> ![icon-note.gif](../device-dev/public_sys-resources/icon-note.gif) **说明:** +> 本文使用AppStorage实现主窗口和子窗口之间的数据传递,除此之外,Emitter和EventHub等方式也可以实现,用户可以根据实际业务需要进行选择。 + + +## 开发步骤 +由于本例重点讲解子窗口的创建以及主窗口和子窗口之间的通信,所以开发步骤会着重讲解相关内容的开发,其余内容不做赘述,全量代码可参考完整代码章节。 +1. 创建子窗口。 + 使用createSubWindow方法创建名为“hiSubWindow”的子窗口,并设置窗口的位置、大小、显示内容。将创建子窗口的动作放在自定义成员方法showSubWindow()中,方便后续绑定到按钮上。具体代码如下: + ```ts + showSubWindow() { + // 创建应用子窗口。 + this.windowStage.createSubWindow("hiSubWindow", (err, data) => { + if (err.code) { + console.error('Failed to create the subwindow. Cause: ' + JSON.stringify(err)); + return; + } + this.sub_windowClass = data; + console.info('Succeeded in creating the subwindow. Data: ' + JSON.stringify(data)); + // 子窗口创建成功后,设置子窗口的位置 + this.sub_windowClass.moveWindowTo(300, 300, (err) => { + if (err.code) { + console.error('Failed to move the window. Cause:' + JSON.stringify(err)); + return; + } + console.info('Succeeded in moving the window.'); + }); + // 设置子窗口的大小 + this.sub_windowClass.resize(350, 350, (err) => { + if (err.code) { + console.error('Failed to change the window size. Cause:' + JSON.stringify(err)); + return; + } + console.info('Succeeded in changing the window size.'); + }); + // 为子窗口加载对应的目标页面。 + this.sub_windowClass.setUIContent("pages/SubWindow",(err) => { + if (err.code) { + console.error('Failed to load the content. Cause:' + JSON.stringify(err)); + return; + } + console.info('Succeeded in loading the content.'); + // 显示子窗口。 + this.sub_windowClass.showWindow((err) => { + if (err.code) { + console.error('Failed to show the window. Cause: ' + JSON.stringify(err)); + return; + } + console.info('Succeeded in showing the window.'); + }); + this.sub_windowClass.setWindowBackgroundColor('#E8A027') + }); + }) + } + ``` +2. 实现子窗口可拖拽。 + 为页面内容绑定PanGesture拖拽事件,拖拽事件发生时获取到触摸点的位置信息,使用@Watch监听到位置变量的变化,然后调用窗口的moveWindowTo方法将窗口移动到对应位置,从而实现拖拽效果。 + + 具体代码如下: + ```ts + import window from '@ohos.window'; + + interface Position { + x: number, + y: number + } + + @Entry + @Component + struct SubWindow{ + ... + // 创建位置变量,并使用@Watch监听,变量发生变化调用moveWindow方法移动窗口 + @State @Watch("moveWindow") windowPosition: Position = { x: 0, y: 0 }; + private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All }); + private subWindow: window.Window + // 通过悬浮窗名称“hiSubWindow”获取到创建的悬浮窗 + aboutToAppear() { + this.subWindow = window.findWindow("hiSubWindow") + } + // 将悬浮窗移动到指定位置 + moveWindow() { + this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y); + } + + build(){ + Column(){ + Text(`AppStorage保存的数据:${this.storData}`) + .fontSize(12) + .margin({bottom:10}) + Button('主窗口数据+1') + .fontSize(12) + .backgroundColor('#A4AE77') + .onClick(()=>{ + this.storData += 1 + }) + } + .height('100%') + .width('100%') + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + .gesture( + PanGesture(this.panOption) + .onActionStart((event: GestureEvent) => { + console.info('Pan start'); + }) + // 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition + .onActionUpdate((event: GestureEvent) => { + this.windowPosition.x += event.offsetX; + this.windowPosition.y += event.offsetY; + }) + .onActionEnd(() => { + console.info('Pan end'); + }) + ) + } + } + ``` +3. 实现主窗口和子窗口间的数据通信。本例中即实现点击主窗口的“子窗口数据+1”按钮,子窗口中的数据加1,反之亦然。本例使用应用全局UI状态存储AppStorage来实现对应效果。 + - 在创建窗口时触发的onWindowStageCreate回调中将自定义数据变量“data”存入AppStorage。 + ```ts + onWindowStageCreate(windowStage: window.WindowStage) { + // 将自定义数据变量“data”存入AppStorage + AppStorage.SetOrCreate('data', 1); + ... + windowStage.loadContent('pages/Index', (err, data) => { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); + }); + } + ``` + - 在主窗口中定义变量“storData”,并使用@StorageLink将其与AppStorage中的变量“data”进行双向绑定,这样一来,“mainData”的变化可以传导至“data”,并且该变化可以被UI框架监听到,从而完成UI状态刷新。 + ```ts + ... + // 使用@StorageLink将"mainData"与AppStorage中的变量"data"进行双向绑定 + @StorageLink('data') mainData: number = 1; + ... + build() { + Row() { + Column() { + Text(`AppStorage保存的数据:${this.mainData}`) + .margin({bottom:30}) + Button('子窗口数据+1') + .backgroundColor('#A4AE77') + .margin({bottom:30}) + .onClick(()=>{ + // 点击,storData的值加1 + this.mainData += 1 + }) + ... + } + .width('100%') + } + .height('100%') + } + ``` + - 在主窗口中定义变量“subData”,并使用@StorageLink将其与AppStorage中的变量“data”进行双向绑定。由于主窗口的“mainData”也与“data”进行了绑定,因此,“mainData”的值可以通过“data”传递给“subData”,反之亦然。这样就实现了主窗口和子窗口之间的数据同步。 + ```ts + ... + // 使用@StorageLink将"subData"与AppStorage中的变量"data"进行双向绑定 + @StorageLink('data') subData: number = 1; + ... + build(){ + Column(){ + Text(`AppStorage保存的数据:${this.subData}`) + .fontSize(12) + .margin({bottom:10}) + Button('主窗口数据+1') + .fontSize(12) + .backgroundColor('#A4AE77') + .onClick(()=>{ + // 点击,subData的值加1 + this.subData += 1 + }) + } + ... + } + ``` + +## 完整代码 +本例完整代码如下: +EntryAbility文件代码: +```ts +// EntryAbility.ts +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; +import hilog from '@ohos.hilog'; +import UIAbility from '@ohos.app.ability.UIAbility'; +import Want from '@ohos.app.ability.Want'; +import window from '@ohos.window'; + +let sub_windowClass = null; +export default class EntryAbility extends UIAbility { + + destroySubWindow() { + // 销毁子窗口。当不再需要子窗口时,可根据具体实现逻辑,使用destroy对其进行销毁。 + sub_windowClass.destroyWindow((err) => { + if (err.code) { + console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err)); + return; + } + console.info('Succeeded in destroying the window.'); + }); + } + + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { + + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage) { + // 将自定义数据变量“data”存入AppStorage + AppStorage.SetOrCreate('data', 1); + AppStorage.SetOrCreate('window', windowStage); + // 为主窗口添加加载页面 + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + + windowStage.loadContent('pages/Index', (err, data) => { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); + }); + } + + onWindowStageDestroy() { + this.destroySubWindow(); + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); + } +} +``` +主窗口代码: +```ts +// Index.ets +import window from '@ohos.window'; + +@Entry +@Component +struct Index { + // 使用@StorageLink将"mainData"与AppStorage中的变量"data"进行双向绑定 + @StorageLink('data') mainData: number = 1; + @StorageLink('window') storWindow:window.WindowStage = null + private windowStage = this.storWindow + private sub_windowClass = null + + showSubWindow() { + // 创建应用子窗口。 + this.windowStage.createSubWindow("hiSubWindow", (err, data) => { + if (err.code) { + console.error('Failed to create the subwindow. Cause: ' + JSON.stringify(err)); + return; + } + this.sub_windowClass = data; + console.info('Succeeded in creating the subwindow. Data: ' + JSON.stringify(data)); + // 子窗口创建成功后,设置子窗口的位置、大小及相关属性等。 + this.sub_windowClass.moveWindowTo(300, 300, (err) => { + if (err.code) { + console.error('Failed to move the window. Cause:' + JSON.stringify(err)); + return; + } + console.info('Succeeded in moving the window.'); + }); + this.sub_windowClass.resize(350, 350, (err) => { + if (err.code) { + console.error('Failed to change the window size. Cause:' + JSON.stringify(err)); + return; + } + console.info('Succeeded in changing the window size.'); + }); + // 为子窗口加载对应的目标页面。 + this.sub_windowClass.setUIContent("pages/SubWindow",(err) => { + if (err.code) { + console.error('Failed to load the content. Cause:' + JSON.stringify(err)); + return; + } + console.info('Succeeded in loading the content.'); + // 显示子窗口。 + this.sub_windowClass.showWindow((err) => { + if (err.code) { + console.error('Failed to show the window. Cause: ' + JSON.stringify(err)); + return; + } + console.info('Succeeded in showing the window.'); + }); + this.sub_windowClass.setWindowBackgroundColor('#E8A027') + }); + }) + } + + build() { + Row() { + Column() { + Text(`AppStorage保存的数据:${this.mainData}`) + .margin({bottom:30}) + Button('子窗口数据+1') + .backgroundColor('#A4AE77') + .margin({bottom:30}) + .onClick(()=>{ + // 点击,storData的值加1 + this.mainData += 1 + }) + Button('创建子窗口') + .backgroundColor('#A4AE77') + .onClick(()=>{ + // 点击弹出子窗口 + this.showSubWindow() + }) + } + .width('100%') + } + .height('100%') + } +} +``` +子窗口代码: +```ts +// SubWindow.ets +import window from '@ohos.window'; + +interface Position { + x: number, + y: number +} + +@Entry +@Component +struct SubWindow{ + // 使用@StorageLink将"subData"与AppStorage中的变量"data"进行双向绑定 + @StorageLink('data') subData: number = 1; + // 创建位置变量,并使用@Watch监听,变量发生变化调用moveWindow方法移动窗口 + @State @Watch("moveWindow") windowPosition: Position = { x: 0, y: 0 }; + private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All }); + private subWindow: window.Window + // 通过悬浮窗名称“hiSubWindow”获取到创建的悬浮窗 + aboutToAppear() { + this.subWindow = window.findWindow("hiSubWindow") + } + // 将悬浮窗移动到指定位置 + moveWindow() { + this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y); + } + + build(){ + Column(){ + Text(`AppStorage保存的数据:${this.subData}`) + .fontSize(12) + .margin({bottom:10}) + Button('主窗口数据+1') + .fontSize(12) + .backgroundColor('#A4AE77') + .onClick(()=>{ + // 点击,subData的值加1 + this.subData += 1 + }) + } + .height('100%') + .width('100%') + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + .gesture( + PanGesture(this.panOption) + .onActionStart((event: GestureEvent) => { + console.info('Pan start'); + }) + // 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition + .onActionUpdate((event: GestureEvent) => { + this.windowPosition.x += event.offsetX; + this.windowPosition.y += event.offsetY; + }) + .onActionEnd(() => { + console.info('Pan end'); + }) + ) + } +} +``` +## 参考 +- [窗口开发](../application-dev/windowmanager/application-window-stage.md) +- [AppStorage:应用全局的UI状态存储](../application-dev/quick-start/arkts-appstorage.md) \ No newline at end of file diff --git a/zh-cn/third-party-cases/transition-animation.md b/zh-cn/third-party-cases/transition-animation.md new file mode 100644 index 0000000000000000000000000000000000000000..7142e5c05dc0ba3c7deb02c77bd233d81211066a --- /dev/null +++ b/zh-cn/third-party-cases/transition-animation.md @@ -0,0 +1,958 @@ +# 转场动画的使用(ArkTs) + +日常在应用时,经常需要衔接两个场景,或者两个镜头画面之间进行切换,切换时需要呈现一种平滑过渡效果。 + +本例将为大家介绍下如何通过转场动画实现上述过渡效果。 + +## 效果呈现 +本例最终效果如下: + +| 场景 | 效果图 | +| ---------------------------------- | ----------------------------------------------------- | +| 页面间转场--底部滑入转场 | ![BottomTransition.gif](figures/BottomTransition.gif) | +| 页面间转场--自定义1:缩放动画转场 | ![](figures/CustomTransition.gif) | +| 页面间转场---自定义2:旋转动画转场 | ![](figures/FullCustomTransition.gif) | +| 组件内转场 | ![](figures/ComponentTransition.gif) | +| 共享元素转场 | ![](figures/SharePage.gif) | + + + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: +- IDE: DevEco Studio 4.0 Beta1 +- SDK: Ohos_sdk_public 4.0.7.5 (API Version 10 Beta1) +## 实现思路 +* 构建应用首页,主要由5个相同样式的功能菜单组成,通过添加路由实现主页面与对应功能页面的链接跳转。 + +* 功能页面的实现 + + * 页面间转场 + + * 底部滑入转场 + + 通过给pageTransition()方法定义入场效果PageTransitionEnter以及出场效果PageTransitionExit,同时通过设置slide属性为SlideEffect.Bottom来实现从底部滑入动效。 + + * 缩放动画转场 + + 通过设置pageTransition方法,配置进行配置转场参数。 + + * 旋转动画转场 + + 在FullCustomTransition.ets的Column组件中添加TransitionElement组件,并且定义pageTransition方法。给Clomn组件添加opacity、scale、rotate属性,定义变量animValue用来控制Clomn组件的动效,在PageTransitionEnter和PageTransitionExit组件中动态改变myProgress的值,从而控制动画效果。 + + * 组件间转场 + + * 通过Image、Column、Text、Button等组件构建ComponentTransition.ets页面。 + + * 新建一个Image组件,并且添加两个transition属性,分别用于定义组件的插入动效和移除动效,来实现组件转场间。 + + * 设置变量isShow,用来控制上述步骤中Image组件的添加和移除,同时向Button组件的onClick添加animateTo方法,来使ComponentItem子组件动效生效。 + * isShow默认状态为false,删除隐藏Image组件,同时删除动效生效。 + + * 当isShow状态更新为true时,插入Image组件,同时插入动效生效。 + + * 共享转场 + + 通过给两个页面“SharedItem”和“SharePage” 的Image组件设置sharedTransition属性来实现,两个页面的组件配置为同一个id,则转场过程中会执行共享元素转场效果。 + +## 开发步骤 +1. 创建主界面。 + + 添加媒体资源至resources > base > media目录下。 + + ![Transition-animation-tree.png](figures/Transition-animation-tree.png) + + 首页Index.ets引入首页列表常量数据:INDEX_ANIMATION_MODE(imgRes:设置按钮的背景图片,url:设置页面路由的地址),通过ForEach方法循环渲染列表常量数据。 + 具体代码如下: + + ```ts + // entry/src/main/ets/pages/Index.ets + + // 引入列表常量数据INDEX_ANIMATION_MODE + export const INDEX_ANIMATION_MODE = [ + { imgRes: $r('app.media.bg_bottom_anim_transition'), url: 'pages/BottomTransition' }, + { imgRes: $r('app.media.bg_custom1_anim_transition'), url: 'pages/CustomTransition' }, + { imgRes: $r('app.media.bg_custom2_anim_transition'), url: 'pages/FullCustomTransition' }, + { imgRes: $r('app.media.bg_element_anim_transition'), url: 'pages/ComponentTransition' }, + { imgRes: $r('app.media.bg_share_anim_transition'), url: 'pages/ShareItem' } + ]; + + ... + Column() { + // ForEach循环渲染 + ForEach(INDEX_ANIMATION_MODE, ({ imgRes , url }) => { + Row() + .backgroundImage(imgRes) + .backgroundImageSize(ImageSize.Cover) + .backgroundColor('#00000000') + .height(130) + .margin({ bottom: 30 }) + .width('100%') + .borderRadius(32) + .onClick(() => { + router.pushUrl({ url: url }) + }) + }, item => JSON.stringify(item)) + } + ``` + + 添加其它组件,以及样式,完成UI构建。 + + 具体代码如下: + ```ts + // entry/src/main/ets/pages/Index.ets + import router from '@ohos.router'; + import hilog from '@ohos.hilog'; + @Entry + @Component + struct Index { + build() { + Column() { + Text($r('app.string.main_page_title')) + .fontSize(30) + .fontWeight(FontWeight.Regular) + .width('100%') + .margin({ top: 13, bottom: 27,left: 24}) + + Scroll() { + Column() { + ForEach(INDEX_ANIMATION_MODE, ({ imgRes , url }) => { + Row() + .backgroundImage(imgRes) + .backgroundImageSize(ImageSize.Cover) + .backgroundColor('#00000000') + .height(130) + .margin({ bottom: 30 }) + .width('100%') + .borderRadius(32) + .onClick(() => { + router.pushUrl({ url: url }) + .catch(err => { + hilog.error(0xff00, '[ReadingRecorder]', `%{public}s, %{public}s`, err); + }); + }) + }, item => JSON.stringify(item)) + } + } + .align(Alignment.Top) + .layoutWeight(1) + .scrollBar(BarState.Off) + } + .height('100%') + .backgroundColor('#F1F3F5') + .padding({left:12 , right:12}) + } + } + ``` + +2. 实现页面间转场。 + * 效果1:底部滑入。 + + 该效果的实现,主要是通过在BottomTransition.ets中设置全局pageTransition()方法,该方法中自定义入场效果PageTransitionEnter以及出场效果PageTransitionExit,同时通过设置slide属性为SlideEffect.Bottom来实现从底部滑入动效。 + + 具体代码如下: + + ```ts + // entry/src/main/ets/pages/BottomTransition.ets + + @Entry + @Component + struct BottomTransition { + private imgRes: string | Resource = $r('app.media.bg_transition'); + private imgFit: ImageFit = ImageFit.Fill; + + build() { + Column() { + Image(this.imgRes) + .objectFit(this.imgFit) + .width('100%') + .height('100%') + } + + } + + // 页面转场通过全局pageTransition方法进行配置转场参数 + pageTransition() { + // PageTransitionEnter自定义入场效果:设置slide属性为SlideEffect.Bottom 表示入场时从屏幕下方滑入。 + PageTransitionEnter({ duration: 600, curve: Curve.Smooth }).slide(SlideEffect.Bottom); + // PageTransitionExit自定义出场效果:设置slide属性为SlideEffect.Bottom 退场时从屏幕下方滑出。 + PageTransitionExit({ duration: 600, curve: Curve.Smooth }).slide(SlideEffect.Bottom); + } + } + + ``` + + * 效果2:页面入场时淡入和放大,退场时从右下角滑出。 + + * 在CustomTransition.ets中设置全局pageTransition()方法。 + * pageTransition方法中自定义入场效果PageTransitionEnter:透明度设置从0.2到1;x、y轴缩放从0变化到1。 + * pageTransition方法中自定义出场效果PageTransitionExit: x、y轴的偏移量为500。 + + 具体代码如下: + + ```ts + // entry/src/main/ets/pages/CustomTransition.ets + + @Entry + @Component + struct CustomTransition { + private imgRes: string | Resource = $r('app.media.bg_transition'); + private imgFit: ImageFit = ImageFit.Fill; + + build() { + Column() { + Image(this.imgRes) + .objectFit(this.imgFit) + .width('100%') + .height('100%') + } + } + + // 页面转场通过全局pageTransition方法进行配置转场参数 + pageTransition() { + // 进场时透明度设置从0.2到1;x、y轴缩放从0变化到1 + PageTransitionEnter({ duration: 600, curve: Curve.Smooth }).opacity(0.2).scale({ x: 0, y: 0 }) + // 退场时x、y轴的偏移量为500 + PageTransitionExit({ duration: 600, curve: Curve.Smooth }).translate({ x: 500, y: 500 }) + } + } + ``` + + * 效果3:页面入场时淡入和放大,同时顺时针旋转;退场时淡出和缩小,同时逆时针旋转。 + + * 在FullCustomTransition.ets中添加Column组件。 + + * 向Column组件添加属性:opacity、scale、rotate,来控制动效的淡入淡出、缩放以及旋转效果。 + + * 定义变量animValue用来,通过animValue值得变化来控制Column组件的动效。 + * 在FullCustomTransition.ets中定义全局pageTransition()方法。 + * pageTransition方法中自定义入场效果PageTransitionEnter。 + + ​ animValue值实时变化,0 --> 1,从而渲染 入场时淡入、放大以及顺时针旋转效果。 + * pageTransition方法中自定义出场效果PageTransitionExit。 + + ​ animValue值实时变化,1 --> 0,从而渲染 出场时淡出、缩小以及逆时针旋转效果。 + + 具体代码如下: + + ```ts + // entry/src/main/ets/pages/FullCustomTransition.ets + + @Entry + @Component + struct FullCustomTransition { + @State animValue: number = 1; + private imgRes: string | Resource = $r('app.media.bg_transition'); + private imgFit: ImageFit = ImageFit.Fill; + + build() { + Column() { + Image(this.imgRes) + .objectFit(this.imgFit) + .width('100%') + .height('100%') + } + // 设置淡入、淡出效果 + .opacity(this.animValue) + // 设置缩放 + .scale({ x: this.animValue, y: this.animValue }) + // 设置旋转角度 + .rotate({ + z: 1, + angle: 360 * this.animValue + }) + } + + // 页面转场通过全局pageTransition方法进行配置转场参数 + pageTransition() { + PageTransitionEnter({ duration: 600, curve: Curve.Smooth }) + // 进场过程中会逐帧触发onEnter回调,入参为动效的归一化进度(0 - 1) + .onEnter((type: RouteType, progress: number) => { + // 入场动效过程中,实时更新this.animValue的值 + this.animValue = progress + }); + PageTransitionExit({ duration: 600, curve: Curve.Smooth }) + // 出场过程中会逐帧触发onExit回调,入参为动效的归一化进度(0 - 1) + .onExit((type: RouteType, progress: number) => { + // 入场动效过程中,实时更新this.animValue的值 + this.animValue = 1 - progress + }); + } + } + ``` + + +3. 实现组件内转场。 + + * 通过Image、Column、Text、Button等组件构建ComponentTransition.ets页面。 + + 具体代码如下: + + ```ts + // entry/src/main/ets/pages/ComponentTransition.ets + + @Entry + @Component + struct ComponentTransition { + + build() { + Column() { + Row() { + Image($r('app.media.ic_public_back')) + .width(20) + .height(20) + .responseRegion({width:'100%',height: '100%'}) + .onClick(() => { + router.back(); + }) + + Text($r('app.string.Component_transition_header')) + .fontColor(Color.Black) + .fontWeight(FontWeight.Regular) + .fontSize(25) + .margin({left:18,right:18}) + } + .height(30) + .width('100%') + .margin({ top: 20, bottom: 27,left: 24}) + + + Image($r('app.media.bg_element')) + .objectFit(ImageFit.Fill) + .borderRadius(20) + .margin({ bottom: 20 }) + .width('100%') + .height(300) + + Button($r('app.string.Component_transition_toggle')) + .height(40) + .width(120) + .fontColor(Color.White) + .backgroundColor($r('app.color.light_blue')) + } + .padding({left:20,right:20}) + .height('100%') + .width('100%') + } + } + + ``` + + * 新建一个Image组件,并且添加两个transition属性,分别用于定义组件的插入动效和移除动效,来实现组件转场间。 + + 具体代码如下: + + ```ts + // entry/src/main/ets/pages/ComponentTransition.ets + ... + Image($r('app.media.bg_share')) + .objectFit(ImageFit.Fill) + .borderRadius(20) + .margin({ bottom: 20 }) + .height(300) + .width('100%') + // 插入动效 + .transition({ + type: TransitionType.Insert, + scale: { x: 0.5, y: 0.5 }, + opacity: 0 + }) + // 删除隐藏动效 + .transition({ + type: TransitionType.Delete, + rotate: { x: 0, y: 1, z: 0, angle: 360 }, + opacity: 0 + }) + ``` + + + - 设置变量isShow,用来控制上述步骤中Image组件的添加和移除,同时向Button组件的onClick添加animateTo方法,来使ComponentItem子组件动效生效。 + + * isShow默认状态为false,删除隐藏Image组件,同时删除动效生效。 + + * 当isShow状态更新为true时,插入Image组件,同时插入动效生效。 + + 具体代码如下: + + ```ts + // entry/src/main/ets/pages/ComponentTransition.ets + + ... + @State isShow: boolean = false; + ... + // isShow为True,插入Image组件,同时插入动效生效;isShow为False,删除隐藏Image组件,同时删除动效生效 + if (this.isShow) { + Image($r('app.media.bg_share')) + .objectFit(ImageFit.Fill) + .borderRadius(20) + .margin({ bottom: 20 }) + .height(300) + .width('100%') + // 插入动效 + .transition({ + type: TransitionType.Insert, + scale: { x: 0.5, y: 0.5 }, + opacity: 0 + }) + // 删除隐藏动效 + .transition({ + type: TransitionType.Delete, + rotate: { x: 0, y: 1, z: 0, angle: 360 }, + opacity: 0 + }) + } + ... + Button($r('app.string.Component_transition_toggle')) + ... + .onClick(() => { + animateTo({ duration: 600 }, () => { + this.isShow = !this.isShow; + }) + }) + ``` + + ComponentTransition.ets的完整代码如下: + + ```ts + // entry/src/main/ets/pages/ComponentTransition.ets + import router from '@ohos.router'; + + @Entry + @Component + struct ComponentTransition { + @State isShow: boolean = false; + + build() { + Column() { + // 页面title区域,含返回功能以及title显示 + Row() { + Image($r('app.media.ic_public_back')) + .width(20) + .height(20) + .responseRegion({ + width:'100%', + height: '100%' + }) + .onClick(() => { + router.back(); + }) + + Text($r('app.string.Component_transition_header')) + .fontColor(Color.Black) + .fontWeight(FontWeight.Regular) + .fontSize(25) + .height(300) + .margin({ left:18, right:18 }) + } + .height(30) + .width('100%') + .margin({ top: 20, bottom: 27,left: 24}) + + // 页面内容区域 + // isShow为True,插入Image组件,同时插入动效生效;isShow为False,删除隐藏Image组件,同时删除动效生效 + if (this.isShow) { + Image($r('app.media.bg_share')) + .objectFit(ImageFit.Fill) + .borderRadius(20) + .margin({ bottom: 20 }) + .height(300) + .width('100%') + // 插入动效 + .transition({ + type: TransitionType.Insert, + scale: { x: 0.5, y: 0.5 }, + opacity: 0 + }) + // 删除隐藏动效 + .transition({ + type: TransitionType.Delete, + rotate: { x: 0, y: 1, z: 0, angle: 360 }, + opacity: 0 + }) + } + + Image($r('app.media.bg_element')) + .objectFit(ImageFit.Fill) + .borderRadius(20) + .margin({ bottom: 20 }) + .width('100%') + .height(300) + Button($r('app.string.Component_transition_toggle')) + .height(40) + .width(120) + .fontColor(Color.White) + .backgroundColor($r('app.color.light_blue')) + .onClick(() => { + animateTo({ duration: 600 }, () => { + console.log('console-- ' +this.isShow) + this.isShow = !this.isShow; + }) + }) + } + .padding({ + left:(20), + right:(20) + }) + .height('100%') + .width('100%') + } + } + ``` + + +4. 实现元素共享转场。 + + 共享元素转场通过给组件设置sharedTransition属性来实现,两个页面的组件配置为同一个id,则转场过程中会执行共享元素转场效果。 + + * 通过Image、Column、Text等组件构建ShareItem.ets页面,给内容区域的Image组件设置sharedTransition属性标记该元素为共享元素,组件转场id设置为“shareID”, 同时设置共享元素转场效果。 + + 具体代码如下: + + ```ts + // entry/src/main/ets/pages/ShareItem.ets + + import hilog from '@ohos.hilog'; + + @Entry + @Component + struct ShareItem { + // 自定义页面内容区域 + @Builder PreviewArea() { + Column() { + Image($r('app.media.bg_transition')) + .width('100%') + .height(300) + .borderRadius(24) + .margin({ bottom: 12 }) + // 设置sharedTransition属性标记该元素为共享元素,转场id为“shareId” + .sharedTransition('shareId', { + duration: 600, + curve: Curve.Smooth, + delay: 100 + }) + + .onClick(() => { + // 路由切换 + router.pushUrl({ url: 'pages/SharePage' }) + .catch(err => { + hilog.error(0xFF00, '[ReadingRecorder]', `%{public}s, %{public}s`, err); + }); + }) + Text($r('app.string.Share_Item_hint')) + .width('100%') + .textAlign(TextAlign.Center) + .fontSize(20) + .fontWeight(FontWeight.Regular) + .fontColor($r('app.color.share_item_content_font')) + } + .borderRadius(24) + .backgroundColor(Color.White) + .width('100%') + .padding({ top: 13, left: 12, right: 12,bottom:12}) + } + + build() { + Column() { + // 页面title区域,含返回功能以及title显示 + Row() { + Image($r('app.media.ic_public_back')) + .width(20) + .height(20) + .responseRegion({ + width:'100%', + height: '100%' + }) + .onClick(() => { + router.back(); + }) + + Text($r('app.string.Share_Item_header')) + .fontColor(Color.Black) + .fontWeight(FontWeight.Regular) + .fontSize(25) + .margin({ left:18, right:18 }) + } + .height(30) + .width('100%') + .margin({ top: 20, bottom: 27,left: 24}) + this.PreviewArea() + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.grey_light')) + .padding({left:12,right:12}) + } + } + ``` + + * pages/SharePage.ets页面中,给Image组件设置sharedTransition属性,同时组件转场id设置为“shareID”,从而可以共享上述步骤的转场动效。 + + 具体代码如下: + ```ts + // entry/src/main/ets/pages/SharePage.ets + @Entry + @Component + struct SharePage { + build() { + Column() { + Image($r('app.media.bg_transition')) + .objectFit(ImageFit.Fill) + .width('100%') + .height('100%') + .sharedTransition('shareId', { + duration: 600, + curve: Curve.Smooth, + delay: 100 + }) + } + } + } + ``` + + +## 完整代码 +本例完整代码如下: + +应用首页: /entry/src/main/ets/pages/Index.ets。 + +```ts +// entry/src/main/ets/pages/Index.ets +import router from '@ohos.router'; +import hilog from '@ohos.hilog'; + +@Entry +@Component +struct Index { + build() { + Column() { + Text($r('app.string.main_page_title')) + .fontSize(30) + .fontWeight(FontWeight.Regular) + .width('100%') + .margin({ top: 13, bottom: 27,left: 24}) + + Scroll() { + Column() { + ForEach(INDEX_ANIMATION_MODE, ({ imgRes , url }) => { + Row() + .backgroundImage(imgRes) + .backgroundImageSize(ImageSize.Cover) + .backgroundColor('#00000000') + .height(130) + .margin({ bottom: 30 }) + .width('100%') + .borderRadius(32) + .onClick(() => { + router.pushUrl({ url: url }) + .catch(err => { + hilog.error(0xff00, '[ReadingRecorder]', `%{public}s, %{public}s`, err); + }); + }) + }, item => JSON.stringify(item)) + } + } + .align(Alignment.Top) + .layoutWeight(1) + .scrollBar(BarState.Off) + } + .height('100%') + .backgroundColor('#F1F3F5') + .padding({left:12 , right:12}) + } +} +``` + +底部滑出页面:/entry/src/main/ets/pages/BottomTransition.ets。 + +```ts +// entry/src/main/ets/pages/BottomTransition.ets + +@Entry +@Component +struct BottomTransition { + private imgRes: string | Resource = $r('app.media.bg_transition'); + private imgFit: ImageFit = ImageFit.Fill; + + build() { + Column() { + Image(this.imgRes) + .objectFit(this.imgFit) + .width('100%') + .height('100%') + } + + } + + // 页面转场通过全局pageTransition方法进行配置转场参数 + pageTransition() { + // PageTransitionEnter自定义入场效果:设置slide属性为SlideEffect.Bottom 表示入场时从屏幕下方滑入。 + PageTransitionEnter({ duration: 600, curve: Curve.Smooth }).slide(SlideEffect.Bottom); + // PageTransitionExit自定义出场效果:设置slide属性为SlideEffect.Bottom 退场时从屏幕下方滑出。 + PageTransitionExit({ duration: 600, curve: Curve.Smooth }).slide(SlideEffect.Bottom); + } +} +``` + +自定义1 缩放动画转场页面:/entry/src/main/ets/pages/CustomTransition.ets。 + +```ts +// entry/src/main/ets/pages/CustomTransition.ets +@Entry +@Component +struct CustomTransition { + private imgRes: string | Resource = $r('app.media.bg_transition'); + private imgFit: ImageFit = ImageFit.Fill; + build() { + Column() { + Image(this.imgRes) + .objectFit(this.imgFit) + .width('100%') + .height('100%') + } + } + // 页面转场通过全局pageTransition方法进行配置转场参数 + pageTransition() { + // 进场时透明度设置从0.2到1;x、y轴缩放从0变化到1 + PageTransitionEnter({ duration: 600, curve: Curve.Smooth }).opacity(0.2).scale({ x: 0, y: 0 }) + // 退场时x、y轴的偏移量为500 + PageTransitionExit({ duration: 600, curve: Curve.Smooth }).translate({ x: 500, y: 500 }) + } +} +``` + +自定义2 旋转动画转场: /entry/src/main/ets/pages/FullCustomTransition.ets。 + +```ts +@Entry +@Component +struct FullCustomTransition { + @State animValue: number = 1; + private imgRes: string | Resource = $r('app.media.bg_transition'); + private imgFit: ImageFit = ImageFit.Fill; + build() { + Column() { + Image(this.imgRes) + .objectFit(this.imgFit) + .width('100%') + .height('100%') + } + // 设置淡入、淡出效果 + .opacity(this.animValue) + // 设置缩放 + .scale({ x: this.animValue, y: this.animValue }) + // 设置旋转角度 + .rotate({ + z: 1, + angle: 360 * this.animValue + }) + } + // 页面转场通过全局pageTransition方法进行配置转场参数 + pageTransition() { + PageTransitionEnter({ duration: 600, curve: Curve.Smooth }) + // 进场过程中会逐帧触发onEnter回调,入参为动效的归一化进度(0 - 1) + .onEnter((type: RouteType, progress: number) => { + // 入场动效过程中,实时更新this.animValue的值 + this.animValue = progress + }); + PageTransitionExit({ duration: 600, curve: Curve.Smooth }) + // 出场过程中会逐帧触发onExit回调,入参为动效的归一化进度(0 - 1) + .onExit((type: RouteType, progress: number) => { + // 入场动效过程中,实时更新this.animValue的值 + this.animValue = 1 - progress + }); + } +} +``` + +组件内转场页面: /entry/src/main/ets/pages/ComponentTransition.ets。 + +```ts + import router from '@ohos.router'; + @Entry + @Component + struct ComponentTransition { + @State isShow: boolean = false; + build() { + Column() { + // 页面title区域,含返回功能以及title显示 + Row() { + Image($r('app.media.ic_public_back')) + .width(20) + .height(20) + .responseRegion({ + width:'100%', + height: '100%' + }) + .onClick(() => { + router.back(); + }) + Text($r('app.string.Component_transition_header')) + .fontColor(Color.Black) + .fontWeight(FontWeight.Regular) + .fontSize(25) + .margin({left:18, right:18}) + } + .height(30) + .width('100%') + .margin({ top: 20, bottom: 27,left: 24}) + + // 页面内容区域 + // isShow为True,插入Image组件,同时插入动效生效;isShow为False,删除隐藏Image组件,同时删除动效生效 + if (this.isShow) { + Image($r('app.media.bg_share')) + .objectFit(ImageFit.Fill) + .borderRadius(20) + .margin({ bottom: 20 }) + .height(300) + .width('100%') + // 插入动效 + .transition({ + type: TransitionType.Insert, + scale: { x: 0.5, y: 0.5 }, + opacity: 0 + }) + // 删除隐藏动效 + .transition({ + type: TransitionType.Delete, + rotate: { x: 0, y: 1, z: 0, angle: 360 }, + opacity: 0 + }) + } + + + Image($r('app.media.bg_element')) + .objectFit(ImageFit.Fill) + .borderRadius(20) + .margin({ bottom: 20 }) + .width('100%') + .height(300) + + Button($r('app.string.Component_transition_toggle')) + .height(40) + .width(120) + .fontColor(Color.White) + .backgroundColor($r('app.color.light_blue')) + .onClick(() => { + animateTo({ duration: 600 }, () => { + this.isShow = !this.isShow; + }) + }) + } + .padding({left:20,right:20}) + .height('100%') + .width('100%') + } + } +``` + + +共享元素转场部件:/entry/src/main/ets/pages/ShareItem.ets。 + +```ts +import hilog from '@ohos.hilog'; + +@Entry +@Component +struct ShareItem { + // 自定义页面内容区域 + @Builder PreviewArea() { + Column() { + Image($r('app.media.bg_transition')) + .width('100%') + .height(300) + .borderRadius(24) + .margin({ bottom: 12 }) + // 设置sharedTransition属性标记该元素为共享元素,转场id为“shareId” + .sharedTransition('shareId', { + duration: 600, + curve: Curve.Smooth, + delay: 100 + }) + + .onClick(() => { + // 路由切换 + router.pushUrl({ url: 'pages/SharePage' }) + .catch(err => { + hilog.error(0xFF00, '[ReadingRecorder]', `%{public}s, %{public}s`, err); + }); + }) + Text($r('app.string.Share_Item_hint')) + .width('100%') + .textAlign(TextAlign.Center) + .fontSize(20) + .fontWeight(FontWeight.Regular) + .fontColor($r('app.color.share_item_content_font')) + } + .borderRadius(24) + .backgroundColor(Color.White) + .width('100%') + .padding({ top: 13, left: 12, right: 12,bottom:12}) + } + + build() { + Column() { + // 页面title区域,含返回功能以及title显示 + Row() { + Image($r('app.media.ic_public_back')) + .width(20) + .height(20) + .responseRegion({ + width:'100%', + height: '100%' + }) + .onClick(() => { + router.back(); + }) + + Text($r('app.string.Share_Item_header')) + .fontColor(Color.Black) + .fontWeight(FontWeight.Regular) + .fontSize(25) + .margin({ left:18, right:18 }) + } + .height(30) + .width('100%') + .margin({ top: 20, bottom: 27,left: 24}) + this.PreviewArea() + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.grey_light')) + .padding({left:12,right:12}) + } +} +``` + +共享元素转场页面:/entry/src/main/ets/pages/SharePage.ets。 + +```ts +@Entry +@Component +struct SharePage { + build() { + Column() { + Image($r('app.media.bg_transition')) + .objectFit(ImageFit.Fill) + .width('100%') + .height('100%') + .sharedTransition('shareId', { + duration: 600, + curve: Curve.Smooth, + delay: 100 + }) + } + } +} +``` + + ## 参考 + +- [图形变换](../application-dev/reference/arkui-ts/ts-universal-attributes-transformation.md) + +- [页面间转场](../application-dev/reference/arkui-ts/ts-page-transition-animation.md) + +- [组件内转场](../application-dev/ui/arkts-transition-animation-within-component.md) + +- [共享元素转场](../application-dev/reference/arkui-ts/ts-transition-animation-shared-elements.md) + +