diff --git a/zh-cn/third-party-cases/collapse-and-expand.md b/zh-cn/third-party-cases/collapse-and-expand.md new file mode 100644 index 0000000000000000000000000000000000000000..836fb09b2201d08f671d62d286884bc6a1aedb8c --- /dev/null +++ b/zh-cn/third-party-cases/collapse-and-expand.md @@ -0,0 +1,311 @@ +# 折叠展开动效 +## 场景介绍 +由于目前移动端需要展示的内容越来越多,但是移动端的空间弥足珍贵,在有限的空间内不可能罗列展示全部种类内容,因此折叠/展开功能就可以解决当前问题,本文就介绍下如何使用ArkTS来实现折叠展开动效。 + +## 效果呈现 +折叠展开动效定义:点击展开按钮,下拉动画展示内容,点击折叠按钮,折叠动画折叠内容。 +本例最终效果如下: +![collapse_and_expand](figures/collapse_and_expand.gif) + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其它适配的版本进行开发: +- IDE: DevEco Studio 3.1 Release +- SDK: Ohos_sdk_public 3.2.12.5(API Version 9 Release) +## 实现思路 +创建折叠时的文本组件,根据List组件中的groupcollapse和groupexpand事件自定义一个CollapseAndExpand组件,父组件通过维护flag和onFlagChange来控制折叠/展开的动效,设置动效所需的参数,添加逻辑来展示展开后的文本。 + +## 开发步骤 +1. 创建自定义接口IRowItem。 + 具体代码如下: + + ```ts + interface IRowItem { + id?: number; + title?: string; + name1?: string; + name2?: string; + name3?: string; + flag?: boolean; + type?: string; + onFlagChange?: () => void; + } + ``` + +2. 创建自定义组件CollapseAndExpandDemo,根据自定义接口IRowItem添加内容,创建UI展示文本。 +具体代码如下: + + ```ts + @Entry + @Component{ + ... + build() { + Column() { + Row() { + Image($r("app.media.ic_public_back")) + .width(20) + .height(20) + Text('周免英雄') + .fontSize(18) + .fontWeight(FontWeight.Bold) + .margin({ left: 10 }) + } + .width('100%') + .margin({ bottom: 30 }) + + Column() { + RowItem({ props: { title: '英雄', name1: '孙悟空', name2: '蛮族之王', name3: '武器大师' } }) + // 文本折叠时,type为DOWN + RowItem({ props: { name1: '伊泽瑞尔', name2: '加里奥', name3: '卡特琳娜', type: 'DOWN', onFlagChange: this.onFlagChange } }) + + //被折叠的文本内容 + ... + + RowItem({ props: { title: '商城', name1: '免费', name2: '特价', name3: 'VIP' } }) + RowItem({ props: { title: '分类', name1: '按职业', name2: '按位置', name3: '按城市' } }) + } + .width('100%') + } + ``` + + 被折叠文本信息。 + 具体代码如下: + + ```ts + CollapseAndExpand({ + items: [ + { id: 0, name1: '潮汐海灵', name2: '暗夜猎手', name3: '厄斐琉斯' }, + { id: 1, name1: '涤魂圣枪', name2: '圣枪游侠', name3: '法外狂徒' }, + { id: 2, name1: '北地之怒', name2: '不羁之悦', name3: '傲之追猎者' }, + // 文本展开时,type为UP + { id: 3, name1: '艾瑞莉娅', name2: '战争之影', name3: '时间刺客', type: 'UP', onFlagChange: this.onFlagChange } + ], + }) + ``` + + +3. 将步骤2创建的文本进行渲染。 +具体如下: + + ```ts + build() { + Flex() { + Text(this.props.title) + .fontSize(14) + .fontWeight(FontWeight.Bold) + .layoutWeight(1) + .fontColor(Color.Red) + .margin({ right: 10 }) + Flex({ alignItems: ItemAlign.Center }) { + Text(this.props.name1).fontSize(14).margin({ right: 10 }) + Text(this.props.name2).fontSize(14).margin({ right: 10 }) + Text(this.props.name3).fontSize(14).margin({ right: 10 }) + ... + } + } + } + ``` +4. 创建自定义组件CollapseAndExpand。 +根据自定义组件说明动效,@Provide负责数据更新,并且触发渲染;@Consume在感知数据更新后,重新渲染。 +具体代码如下: + + ```ts + @Entry + @Component + struct CollapseAndExpandDemo { + @Provide("flag") flag: boolean = false + private onFlagChange = () => { + animateTo({ + duration: 650, + curve: Curve.Smooth + }, () => { + this.flag = !this.flag; + }) + } + + ... + + @Component + struct CollapseAndExpand { + private items: IRowItem[] = []; + @Consume("flag") flag: boolean; + + build() { + Column() { + ForEach(this.items, (item: IRowItem) => { + RowItem({ props: item }) + }, (item: IRowItem) => item.id.toString()) + } + .width('100%') + .clip(true) + .height(this.flag ? 130 : 0) + } + } + ``` +5. 根据步骤4最终的flag以及props的type值,判断折叠展开的效果实现。 + 具体代码如下: + + ```ts + build() { + ... + // 当文本折叠(flag为false且type为down)时,展示展开按钮 + // 当文本展开(flag为true且type为up)时,展示折叠按钮 + if (!this.flag && this.props.type === 'DOWN' || this.flag && this.props.type === 'UP') { + Image($r("app.media.icon")) + .width(16) + .height(16) + .objectFit(ImageFit.Contain) + .rotate({ angle: !this.flag && this.props.type === 'DOWN' ? 0 : 180 }) + // 点击按钮后旋转180°,展示折叠按钮 + .onClick(() => + this.props.onFlagChange() + ) + .transition({ type: TransitionType.All, opacity: 0 }) + } + } + ``` + + +## 完整代码 +示例代码如下: +```ts +interface IRowItem { + id?: number; + title?: string; + name1?: string; + name2?: string; + name3?: string; + flag?: boolean; + type?: string; + onFlagChange?: () => void; +} + +@Entry +@Component +struct CollapseAndExpandDemo { + Provide("flag") flag: boolean = false + private onFlagChange = () => { + animateTo({ + duration: 650, + curve: Curve.Smooth + }, () => { + this.flag = !this.flag; + }) + } + + build() { + Column() { + Row() { + Image($r("app.media.ic_public_back")).width(20).height(20) + Text('周免英雄') + .fontSize(18) + .fontWeight(FontWeight.Bold) + .margin({ left: 10 }) + } + .width('100%') + .margin({ bottom: 30 }) + + Column() { + RowItem({ + props: { title: '英雄', name1: '孙悟空', name2: '蛮族之王', name3: '武器大师' } }) + RowItem({ + props: { + name1: '伊泽瑞尔', + name2: '加里奥', + name3: '卡特琳娜', + // 文本折叠时,type为DOWN + type: 'DOWN', + onFlagChange: this.onFlagChange + } + }) + // 直接调用折叠展开组件 + CollapseAndExpand({ + items: [ + { id: 0, name1: '潮汐海灵', name2: '暗夜猎手', name3: '厄斐琉斯' }, + { id: 1, name1: '涤魂圣枪', name2: '圣枪游侠', name3: '法外狂徒' }, + { id: 2, name1: '北地之怒', name2: '不羁之悦', name3: '傲之追猎者' }, + { id: 3, + name1: '艾瑞莉娅', + name2: '战争之影', + name3: '时间刺客', + // 文本折叠时,type为UP + type: 'UP', + onFlagChange: this.onFlagChange } + ], + }) + + RowItem({ props: { title: '商城', name1: '免费', name2: '特价', name3: 'VIP' } }) + RowItem({ props: { title: '分类', name1: '按职业', name2: '按位置', name3: '按城市' } }) + } + .width('100%') + + } + .height('100%') + .padding({ top: 30, right: 30, left: 30 }) + } +} + +@Component +struct RowItem { + private props: IRowItem; + Consume("flag") flag: boolean + + build() { + Flex() { + Text(this.props.title) + .fontSize(14) + .fontWeight(FontWeight.Bold) + .layoutWeight(1) + .fontColor(Color.Red) + .margin({ right: 10 }) + Flex({ alignItems: ItemAlign.Center }) { + Text(this.props.name1).fontSize(14).margin({ right: 10 }) + Text(this.props.name2).fontSize(14).margin({ right: 10 }) + Text(this.props.name3).fontSize(14).margin({ right: 10 }) + + // 当文本折叠(flag为false且type为down)时,展示展开按钮 + // 当文本展开(flag为true且type为up)时,展示折叠按钮 + if (!this.flag && this.props.type === 'DOWN' || this.flag && this.props.type === 'UP') { + Image($r("app.media.ic_public_arrow_down_0")) + .width(16) + .height(16) + .objectFit(ImageFit.Contain) + .rotate({ angle: !this.flag && this.props.type === 'DOWN' ? 0 : 180 }) + // 点击展开按钮后旋转180°,展示折叠按钮 + .onClick(() => this.props.onFlagChange()) + .transition({ type: TransitionType.All, opacity: 0 }) + } + } + .layoutWeight(3) + } + .width('100%') + .height(16) + .margin({ top: 15 }) + } +} + +@Component +struct CollapseAndExpand { + private items: IRowItem[] = []; + Consume("flag") flag: boolean; + + build() { + Column() { + ForEach(this.items, (item: IRowItem) => { + RowItem({ props: item }) + }, (item: IRowItem) => item.id.toString()) + } + .width('100%') + .clip(true) + .height(this.flag ? 130 : 0) + } +} +``` +## 参考 +[显示动画](../application-dev/reference/arkui-ts/ts-explicit-animation.md/) +[@Provide和@Consume:与后代组件双向同步](../application-dev/quick-start/arkts-provide-and-consume.md/) +[list开发指导](../zh-cn/application-dev/ui/ui-js-components-list.md/) + + + + + diff --git a/zh-cn/third-party-cases/figures/collapse_and_expand.gif b/zh-cn/third-party-cases/figures/collapse_and_expand.gif new file mode 100644 index 0000000000000000000000000000000000000000..64e65e378929810e3e047a6da09cd53bfd71b50b Binary files /dev/null and b/zh-cn/third-party-cases/figures/collapse_and_expand.gif differ diff --git a/zh-cn/third-party-cases/figures/first-layer-variable.gif b/zh-cn/third-party-cases/figures/first-layer-variable.gif new file mode 100644 index 0000000000000000000000000000000000000000..140be7d220e5f36ba66f459323a8eab0ba73f3b9 Binary files /dev/null and b/zh-cn/third-party-cases/figures/first-layer-variable.gif differ diff --git a/zh-cn/third-party-cases/figures/float-window.gif b/zh-cn/third-party-cases/figures/float-window.gif new file mode 100644 index 0000000000000000000000000000000000000000..db5ffe0afbfeb0fa566c6b251a4c7bec96407007 Binary files /dev/null and b/zh-cn/third-party-cases/figures/float-window.gif differ diff --git a/zh-cn/third-party-cases/figures/navigation_Foldable.PNG b/zh-cn/third-party-cases/figures/navigation_Foldable.PNG new file mode 100644 index 0000000000000000000000000000000000000000..1a611747004f435aa2bf8462e33364f99f026d0d Binary files /dev/null and b/zh-cn/third-party-cases/figures/navigation_Foldable.PNG differ diff --git a/zh-cn/third-party-cases/figures/navigation_phone.PNG b/zh-cn/third-party-cases/figures/navigation_phone.PNG new file mode 100644 index 0000000000000000000000000000000000000000..efb159addc528d6a874287620df22ff7d91a1bd1 Binary files /dev/null and b/zh-cn/third-party-cases/figures/navigation_phone.PNG differ diff --git a/zh-cn/third-party-cases/figures/second-layer-with-state.gif b/zh-cn/third-party-cases/figures/second-layer-with-state.gif new file mode 100644 index 0000000000000000000000000000000000000000..ca46d13c73711943279ddb33f04f3d069a03e4ba Binary files /dev/null and b/zh-cn/third-party-cases/figures/second-layer-with-state.gif differ diff --git a/zh-cn/third-party-cases/figures/second-variable-with-observed-objectlink.gif b/zh-cn/third-party-cases/figures/second-variable-with-observed-objectlink.gif new file mode 100644 index 0000000000000000000000000000000000000000..fd3799891b044414b85a7e4f911bb70117f9bc24 Binary files /dev/null and b/zh-cn/third-party-cases/figures/second-variable-with-observed-objectlink.gif differ diff --git a/zh-cn/third-party-cases/figures/variable-layers-decorators.png b/zh-cn/third-party-cases/figures/variable-layers-decorators.png new file mode 100644 index 0000000000000000000000000000000000000000..fed4da2a5e97e0a5a9f3dcaae7766e06a4b6e53a Binary files /dev/null and b/zh-cn/third-party-cases/figures/variable-layers-decorators.png differ diff --git a/zh-cn/third-party-cases/float-window.md b/zh-cn/third-party-cases/float-window.md new file mode 100644 index 0000000000000000000000000000000000000000..ffe2509dde661c97ff01be3ba5a3f9cb3894ec81 --- /dev/null +++ b/zh-cn/third-party-cases/float-window.md @@ -0,0 +1,397 @@ +# 如何创建悬浮窗 + +## 场景说明 +悬浮窗功能可以基于当前任务创建一个始终在前台显示的窗口。即使创建悬浮窗的任务退至后台,悬浮窗仍然可以在前台显示,通常悬浮窗位于所有应用窗口之上。很多应用都具有悬浮窗的功能,常见的如视频应用的视频播放窗口,在视频应用切换到后台后,视频播放窗口还可以在前台以小窗形式继续播放。本例即为大家介绍如何开发悬浮窗。 + +## 效果呈现 +本例效果如下: + +![float-window](figures/float-window.gif) + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: + +- IDE: DevEco Studio 4.0 Beta1 +- SDK: Ohos_sdk_public 4.0.7.5 (API Version 10 Beta1) + + +## 实现思路 +本例中主要涉及三项关键操作,相关实现方案如下: +- 创建悬浮窗:使用window类的createWindow方法创建窗口,窗口类型设置为window.WindowType.TYPE_FLOAT +- 悬浮窗可拖拽:通过gesture为窗口绑定手势事件,使用PanGesture监听拖拽手势并记录窗口位置,通过moveWindowTo方法将窗口移动到拖拽位置从而实现窗口拖拽。 +- 退出悬浮窗口:使用destroyWindow方法,销毁悬浮窗。 + +## 开发步骤 +由于本例重点讲解悬浮窗的创建和使用,所以开发步骤会着重讲解相关实现,不相关的内容不做介绍,全量代码可参考完整代码章节。 +1. 申请权限。 + + 创建悬浮窗需要先申请ohos.permission.SYSTEM_FLOAT_WINDOW权限,要在module.json5文件的requestPermissions对象中进行配置,如下: + ```json + { + "module": { + "requestPermissions":[ + { + "name" : "ohos.permission.SYSTEM_FLOAT_WINDOW", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when":"inuse" + } + } + ] + } + } + ``` +2. 创建悬浮窗。 + + 使用window类的createWindow方法创建窗口,窗口类型设置为window.WindowType.TYPE_FLOAT。由于本例通过按钮的点击事件控制悬浮窗的创建和销毁,为了便于操作,本例将创建和销毁悬浮窗的操作写在自定义的方法中,以便绑定到按钮的点击时间中。 + 创建悬浮窗的操作在自定义方法createFloatWindow中实现。 + 具体代码如下: + ```ts + // 引入window类 + import window from '@ohos.window'; + ... + // 自定义创建悬浮窗方法 + createFloatWindow() { + let windowClass = null; + // 窗口类型设置为window.WindowType.TYPE_FLOAT + let config = {name: "floatWindow", windowType: window.WindowType.TYPE_FLOAT, ctx: getContext(this)}; + // 创建悬浮窗 + window.createWindow(config, (err, data) => { + if (err.code) { + console.error('Failed to create the floatWindow. Cause: ' + JSON.stringify(err)); + return; + } + console.info('Succeeded in creating the floatWindow. Data: ' + JSON.stringify(data)); + windowClass = data; + } + } + ``` +3. 设置窗口信息。 + + 创建悬浮窗时,可以对窗口的位置、大小、内容等进行设置。 + 具体代码如下: + ```ts + ... + window.createWindow(config, (err, data) => { + ... + windowClass = data; + // 设置悬浮窗位置 + 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.'); + }); + // 设置悬浮窗大小 + windowClass.resize(500, 500, (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.'); + }); + //为悬浮窗加载页面内容,这里可以设置在main_pages.json中配置的页面 + windowClass.setUIContent("pages/FloatContent", (err) => { + if (err.code) { + console.error('Failed to load the content. Cause:' + JSON.stringify(err)); + return; + } + console.info('Succeeded in loading the content.'); + // 显示悬浮窗。 + 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.'); + }); + }); + }); + ``` +4. 销毁悬浮窗。 + + 使用destroyWindow方法销毁悬浮窗,为了便于通过按钮点击控制悬浮窗的销毁,我们这里将销毁逻辑写在自定义方法destroyFloatWindow中。 + 具体代码如下: + ```ts + // 定义windowClass变量,用来接收创建的悬浮窗 + private windowClass: window.Window; + + createFloatWindow() { + ... + // 创建悬浮窗。 + window.createWindow(config, (err, data) => { + if (err.code) { + console.error('Failed to create the floatWindow. Cause: ' + JSON.stringify(err)); + return; + } + console.info('Succeeded in creating the floatWindow. Data: ' + JSON.stringify(data)); + // 用windowClass变量接收创建的悬浮窗 + this.windowClass = data; + ... + } + } + // 自定义销毁悬浮窗方法 + destroyFloatWindow() { + // 用windowClass调用destroyWindow销毁悬浮窗 + this.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.'); + }); + } + ``` +5. 构建主页面UI。 + + 将创建悬浮窗和销毁悬浮窗绑定到对应的按钮上。 + 具体代码如下: + ```ts + ... + build() { + Row() { + Column() { + Button('创建悬浮窗') + .onClick(() => { + // 点击按钮调用创建悬浮窗方法 + this.createFloatWindow(); + }) + Button('销毁悬浮窗') + .margin({top:20}) + .onClick(() => { + // 点击按钮调用销毁悬浮窗方法 + this.destroyFloatWindow(); + }) + } + .width('100%') + } + .height('100%') + } + ... + ``` +6. 创建悬浮窗的显示页面并实现悬浮窗可拖拽。 + + 为页面内容绑定PanGesture拖拽事件,拖拽事件发生时获取到触摸点的位置信息,使用@Watch监听到位置变量的变化,然后调用窗口的moveWindowTo方法将窗口移动到对应位置,从而实现拖拽效果。 + 具体代码如下: + ```ts + import window from '@ohos.window'; + + interface Position { + x: number, + y: number + } + @Entry + @Component + struct FloatContent { + @State message: string = 'float window' + private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All }); + // 创建位置变量,并使用@Watch监听,变量发生变化调用moveWindow方法移动窗口 + @State @Watch("moveWindow") windowPosition: Position = { x: 0, y: 0 }; + floatWindow: window.Window + // 通过悬浮窗名称“floatWindow”获取到创建的悬浮窗 + aboutToAppear() { + this.floatWindow = window.findWindow("floatWindow") + } + // 将悬浮窗移动到指定位置 + moveWindow() { + this.floatWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y); + } + + build() { + Row() { + Column() { + Text(this.message) + .fontSize(50) + .fontWeight(FontWeight.Bold) + } + .width('100%') + } + .height('100%') + .gesture( + // 绑定PanGesture事件,监听拖拽动作 + PanGesture(this.panOption) + .onActionStart((event: GestureEvent) => { + console.info('Pan start'); + }) + .onActionUpdate((event: GestureEvent) => { + // 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition + this.windowPosition.x += event.offsetX; + this.windowPosition.y += event.offsetY; + }) + .onActionEnd(() => { + console.info('Pan end'); + }) + ) + .border({ + style: BorderStyle.Dotted + }) + .backgroundColor(Color.Yellow) + } + } + ``` + +## 完整代码 +本例完整代码如下: +主窗口代码(FloatWindow.ets): +```ts +//FloatWindow.ets +// 引入window类 +import window from '@ohos.window'; + +@Entry +@Component +struct FloatWindow { + // 定义windowClass变量,用来接收创建的悬浮窗 + private windowClass: window.Window; + // 自定义创建悬浮窗方法 + createFloatWindow() { + let windowClass = null; + // 窗口类型设置为window.WindowType.TYPE_FLOAT + let config = {name: "floatWindow", windowType: window.WindowType.TYPE_FLOAT, ctx: getContext(this)}; + // 创建悬浮窗 + window.createWindow(config, (err, data) => { + if (err.code) { + console.error('Failed to create the floatWindow. Cause: ' + JSON.stringify(err)); + return; + } + console.info('Succeeded in creating the floatWindow. Data: ' + JSON.stringify(data)); + windowClass = data; + // 用windowClass变量接收创建的悬浮窗 + this.windowClass = data; + // 设置悬浮窗位置 + 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.'); + }); + // 设置悬浮窗大小 + windowClass.resize(500, 500, (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.'); + }); + // 为悬浮窗加载页面内容,这里可以设置在main_pages.json中配置的页面 + windowClass.setUIContent("pages/FloatContent", (err) => { + if (err.code) { + console.error('Failed to load the content. Cause:' + JSON.stringify(err)); + return; + } + console.info('Succeeded in loading the content.'); + // 显示悬浮窗。 + 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.'); + }); + }); + + }); + } + // 自定义销毁悬浮窗方法 + destroyFloatWindow() { + // 用windowClass调用destroyWindow销毁悬浮窗 + this.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.'); + }); + } + + build() { + Row() { + Column() { + Button('创建悬浮窗') + .backgroundColor('#F9C449') + .onClick(() => { + // 点击按钮调用创建悬浮窗方法 + this.createFloatWindow(); + }) + Button('销毁悬浮窗') + .margin({top:20}) + .backgroundColor('#F9C449') + .onClick(() => { + // 点击按钮调用销毁悬浮窗方法 + this.destroyFloatWindow(); + }) + } + .width('100%') + } + .height('100%') + } +} +``` +悬浮窗内容页代码(FloatContent.ets): +```ts +//FloatContent.ets +import window from '@ohos.window'; + +interface Position { + x: number, + y: number +} +@Entry +@Component +struct FloatContent { + @State message: string = 'float window' + private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All }); + // 创建位置变量,并使用@Watch监听,变量发生变化调用moveWindow方法移动窗口 + @State @Watch("moveWindow") windowPosition: Position = { x: 0, y: 0 }; + floatWindow: window.Window + // 通过悬浮窗名称“floatWindow”获取到创建的悬浮窗 + aboutToAppear() { + this.floatWindow = window.findWindow("floatWindow") + } + // 将悬浮窗移动到指定位置 + moveWindow() { + this.floatWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y); + } + + build() { + Row() { + Column() { + Text(this.message) + .fontSize(30) + .fontColor(Color.White) + .fontWeight(FontWeight.Bold) + } + .width('100%') + } + .height('100%') + .gesture( + // 绑定PanGesture事件,监听拖拽动作 + 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'); + }) + ) + .border({ + style: BorderStyle.Dotted + }) + .backgroundColor("#E8A49C") + } +} +``` +## 参考 +- [管理应用窗口(Stage模型)](../application-dev/windowmanager/application-window-stage.md) +- [@ohos.window (窗口)](../application-dev/reference/apis/js-apis-window.md) +- [单一手势](../application-dev/ui/arkts-gesture-events-single-gesture.md) +- [@Watch:状态变量更改通知](../application-dev/quick-start/arkts-watch.md) \ No newline at end of file diff --git a/zh-cn/third-party-cases/multi-device-app-dev.md b/zh-cn/third-party-cases/multi-device-app-dev.md new file mode 100644 index 0000000000000000000000000000000000000000..917f7948204dfa3d64d1d4bf15ad2b61bab48385 --- /dev/null +++ b/zh-cn/third-party-cases/multi-device-app-dev.md @@ -0,0 +1,340 @@ +# Navigation如何实现多场景UI适配 +## 场景介绍 +日常应用如微信、钉钉、welink等,聊天窗口在不同的设备上拥有不同的表现,如: +* 在phone设备上,导航栏和内容区在多窗口中体现。 + +* 在Foldable设备上,导航栏和内容区在同一窗口体现。 + + 但是此时开发者又想通过一套代码来实现,ArkUI针对这种场景提供了分栏组件,本例见简单介绍下如何使用分栏组件实现上述场景。 + +## 效果呈现 +效果图如下所示(模拟器设备:phone、Foldable): +phone设备效果图: + +![navigation_phone](figures/navigation_phone.PNG) +Foldable设备效果图: + +![navigation_Foldable](figures/navigation_Foldable.PNG) +## 运行环境 +本例基于以下环境开发,开发者也可以基于其它适配的版本进行开发: +- IDE:DevEco Studio 3.1 Release +- SDK: Ohos_sdk_public 3.2.12.5(API Version 9 Release) +## 实现思路 +想要实现一多效果,所有的页面元素必须在Navigation的容器中展示,Navigation一般作为页面的根容器,包括单页面、分栏和自适应三种显示模式,可通过mode属性设置页面的显示模式。 + +导航区中使用NavRouter子组件实现导航栏功能,内容页主要显示NavDestination子组件中的内容。 + +NavRouter是和Navigation搭配使用的特殊子组件,默认提供点击响应处理,不需要开发者自定义点击事件逻辑。NavRouter有且仅有两个根节点,第二个根节点是NavDestination。NavDestination用于显示Navigation组件的内容页。当开发者点击NavRouter组件时,会跳转到对应的NavDestination内容区。 + + +本例涉及一些关键特性以及实现方法如下: +- 创建Navigation组件,同时通过设置mode属性为auto来控制页面显示效果。 +- Navigation通过与NavRouter组件搭配使用,实现页面分栏效果。 + + > NavRouter必须包含两个子组件,其子组件即为实现分栏效果的组件,其中第二个子组件必须为NavDestination(第一个即可理解为为导航栏,第二个组件可理解为内容区)。 + +- 通过向父组件NavRouter添加子组件NavDestination,创建导航内容区并添加文本。 +- 内容区域的补充:根据应用的场景,添加TextArea组件完善内容区。 + + +## 开发步骤 +1. 创建Navigation组件,同时通过设置mode属性为auto来控制页面显示(自适应模式下,当设备宽度大于520vp时,Navigation组件采用分栏模式,反之采用单页面模式)。 +具体代码如下: + ```ts + build() { + Column() { + Navigation() { + ... + } + // Navigation组件mode属性设置为auto。自适应模式下,当设备宽度大于520vp时,Navigation组件采用分栏模式,反之采用单页面模式。 + .mode(NavigationMode.Auto) + } + .height('100%') + } + ``` +2. 通过NavRouter组件创建导航栏:Navigation通过与NavRouter组件搭配实现页面分栏效果。 + * 自定义导航栏NavigationTitle。 + * 添加Navigation子组件NavRoute,创建导航栏。 + * 通过ForEach循环渲染导航栏内容,且导航栏内容通过List组件显示。 + 具体代码如下: + ```ts + // 自定义导航栏title + @Builder NavigationTitle(index) { + Column() { + Row() { + Text('互动交流' + index + '群') + .fontColor('#182431') + .fontSize(20) + } + } + .width($r("app.float.titHeightFloat")) + } + + build() { + Column() { + Navigation() { + Text('联系人(' + this.arr.length + ')') + .fontWeight(500) + .margin({ top: 10, right: 10, left: 19 }) + .fontSize(17) + + List({ initialIndex: 0 }) { + // 通过ForEach循环渲染导航栏内容 + ForEach(this.arr, (item: number, index: number) => { + ListItem() { + // 导航组件,默认提供点击响应处理 + NavRouter() { + // 导航区内容 + Column() { + Row() { + Image($r('app.media.icon1')) + .width(35) + .height(35) + .borderRadius(35) + .margin({ left: 3, right: 10 }) + Text('互动交流' + item + '群') + .fontSize(22) + .textAlign(TextAlign.Center) + } + .padding({ left: 10 }) + .width('100%') + .height(80) + .backgroundColor(this.dex == index ? '#eee' : '#fff') + + Divider().strokeWidth(1).color('#F1F3F5') + }.width('100%') + + ... + + } + .width('100%') + } + }, item => item) + } + .height('100%').margin({ top: 12 }) + } + // Navigation组件默认为自适应模式,此时mode属性为NavigationMode.Auto。自适应模式下,当设备宽度大于520vp时,Navigation组件采用分栏模式,反之采用单页面模式。 + .mode(NavigationMode.Auto) + .hideTitleBar(true) + .hideToolBar(true) + } + .height('100%') + } + ``` + +3. 通过添加组件NavDestination,创建内容栏并添加文本。 + NavRouter包含两个子组件,其子组件即为实现分栏效果的组件,其中第二个子组件必须为NavDestination,用于显示导航内容区(第一个即可理解为为导航栏,第二个组件可理解为内容区); + 内容区部分代码: + + ```ts + build() { + Column() { + Navigation() { + ... + + // 导航组件,默认提供点击响应处理 + NavRouter() { + // 导航区内容 + ... + + // NavRouter组件的子组件,用于显示导航内容区。 + NavDestination() { + // 内容区 + ForEach([0, 1], (item: number) => { + Flex({ direction: FlexDirection.Row }) { + Row() { + Image($r('app.media.icon2')) + .width(40) + .height(40) + .borderRadius(40) + .margin({ right: 15 }) + Text('今天幸运数字' + index.toString()) + .fontSize(20) + .height(40) + .backgroundColor('#f1f9ff') + .borderRadius(10) + .padding(10) + } + .padding({ left: 15 }) + .margin({ top: 15 }) + } + }, item => item) + .... + } + + // 设置内容区标题 + .title(this.NavigationTitle(index)) + } + } + // Navigation组件默认为自适应模式,此时mode属性为NavigationMode.Auto。自适应模式下,当设备宽度大于520vp时,Navigation组件采用分栏模式,反之采用单页面模式。 + .mode(NavigationMode.Auto) + .hideTitleBar(true) + .hideToolBar(true) + } + .height('100%') + } + ``` + + +4. 内容区域的补充:完善内容区域文本组件。 +具体代码块如下: + + ```ts + ... + Column() { + TextArea({ + placeholder: '请输入文字', + }) + .placeholderFont({ size: 16, weight: 400 }) + .width('100%') + .height($r("app.float.heightFloat")) + .fontSize(16) + .fontColor('#182431') + .backgroundColor('#FFFFFF') + .borderRadius(0) + } + .margin({ top: $r("app.float.marHeightFloat") }) + .height($r("app.float.ColHeightFloat")) + .justifyContent(FlexAlign.End) + ... + ``` + +## 完整代码 +示例完整代码如下: + +```ts +@Entry +@Component +struct NavigationExample { + @State arr: number[] = [0, 1, 2, 3, 4, 5] + @State dex: number = 0 + + @Builder NavigationTitle(index) { + Column() { + Row() { + Text('互动交流' + index + '群') + .fontColor('#182431') + .fontSize(20) + } + } + .width($r("app.float.titHeightFloat")) + } + + build() { + Column() { + Navigation() { + Text('联系人(' + this.arr.length + ')') + .fontWeight(500) + .margin({ top: 10, right: 10, left: 19 }) + .fontSize(17) + List({ initialIndex: 0 }) { + // 通过ForEach循环渲染导航栏内容 + ForEach(this.arr, (item: number, index: number) => { + ListItem() { + // 导航组件,默认提供点击响应处理 + NavRouter() { + // 导航区内容 + Column() { + Row() { + Image($r('app.media.icon1')) + .width(35) + .height(35) + .borderRadius(35) + .margin({ left: 3, right: 10 }) + Text('互动交流' + item + '群') + .fontSize(22) + .textAlign(TextAlign.Center) + } + .padding({ left: 10 }) + .width('100%') + .height(80) + .backgroundColor(this.dex == index ? '#eee' : '#fff') + + Divider().strokeWidth(1).color('#F1F3F5') + }.width('100%') + + // NavRouter组件的子组件,用于显示导航内容区。 + NavDestination() { + ForEach([0, 1], (item: number) => { + Flex({ direction: FlexDirection.Row }) { + Row() { + Image($r('app.media.icon2')) + .width(40) + .height(40) + .borderRadius(40) + .margin({ right: 15 }) + Text('今天幸运数字' + index.toString()) + .fontSize(20) + .height(40) + .backgroundColor('#f1f9ff') + .borderRadius(10) + .padding(10) + } + .padding({ left: 15 }) + .margin({ top: 15 }) + } + }, item => item) + + Row() { + Text('幸运数字' + item.toString()) + .fontSize(20) + .margin({ right: 10 }) + .height(40) + .backgroundColor('#68c059') + .borderRadius(10) + .padding(10) + Image($r('app.media.icon3')) + .width(40) + .height(40) + .borderRadius(40) + .margin({ right: 15 }) + } + .padding({ left: 15 }) + .margin({ top: 150 }) + .width('100%') + .direction(Direction.Rtl) + + Column() { + TextArea({placeholder: '请输入文字',}) + .placeholderFont({ size: 16, weight: 400 }) + .width('100%') + .height($r("app.float.heightFloat")) + .fontSize(16) + .fontColor('#182431') + .backgroundColor('#FFFFFF') + .borderRadius(0) + } + .margin({ top: $r("app.float.marHeightFloat") }) + .height($r("app.float.ColHeightFloat")) + .justifyContent(FlexAlign.End) + } + .backgroundColor('#eee') + // 设置内容区标题 + .title(this.NavigationTitle(index)) + } + .width('100%') + } + }, item => item) + } + .height('100%').margin({ top: 12 }) + } + // Navigation组件mode属性设置为auto。自适应模式下,当设备宽度大于520vp时,Navigation组件采用分栏模式,反之采用单页面模式。 + .mode(NavigationMode.Auto) + .hideTitleBar(true) + .hideToolBar(true) + } + .height('100%') + } +} +``` +## 参考 +[List组件](../application-dev/reference/arkui-js/js-components-container-list.md/) + +[Flex组件](../application-dev/reference/arkui-ts/ts-container-flex.md/) + +[Navigation](../application-dev/reference/arkui-ts/ts-basic-components-navigation.md/) + +[NavRouter](../application-dev/reference/arkui-ts/ts-basic-components-navrouter.md/) + +[NavDestination](../application-dev/reference/arkui-ts/ts-basic-components-navdestination.md/) + diff --git a/zh-cn/third-party-cases/observed-and-objectlink.md b/zh-cn/third-party-cases/observed-and-objectlink.md new file mode 100644 index 0000000000000000000000000000000000000000..1c7419c028ef305a2b98d976368ddf5c196d48a4 --- /dev/null +++ b/zh-cn/third-party-cases/observed-and-objectlink.md @@ -0,0 +1,240 @@ +# 如何监听多层状态变化 + +## 场景说明 +应用开发过程中,当希望通过状态变量控制页面刷新时,大家通常想到的就是装饰器@State,但是在嵌套场景下,单单使用@State并不能监听到变量的状态变化,这就引出了@Observed/@ObjectLink装饰器。本文就为大家介绍如何配合使用@State、@Observed、@ObjectLink三个装饰器监听多层状态变化。 + +## 概念原理 +在讲解具体操作前,大家先理解以下几个概念: +- 第一层状态变化:指不包含嵌套关系的变量的变化,比如string、number、boolean等基础数据类型的状态变化,以及嵌套结构中第一层变量的状态变化。 + +- 多层状态变化:指包含嵌套关系的二层及以下变量的变化,比如嵌套类中被嵌套类的成员变量的状态变化,嵌套数组中被嵌套数组的状态变化等。 + +第一层变量的状态变化可以用@State监听,二层及以下变量的状态变化则需要使用@Observed/@ObjectLink监听。以嵌套结构举例,如下图: + +![variable-layers](figures/variable-layers-decorators.png) + + +为便于理解,通过以下例子具体说明单层和多层状态变化: +```ts +class ClassA { + public c: number; + + constructor(c: number) { + this.c = c; + } +} + +class ClassB { + // ClassB成员变量的类型为ClassA,ClassA为被嵌套类 + public a: ClassA; + + constructor(a: ClassA) { + this.a = a; + } +} + +b: ClassB +// 变量a为ClassB的成员变量,为第一层变量,所以变量a的状态变化即为第一层状态变化 +this.b.a = new ClassA(0) +// 变量c为被嵌套类ClassA的成员变量,变量c的状态变化即为第二层状态变化 +this.b.a.c = 5 +``` + +## 监听第一层状态变化 +监听第一层状态变化可以使用@State修饰变量,变量发生变化后即可同步刷新UI,这是大家最常用的场景,为便于理解,此处举例说明一下: +```ts +class ClassA { + public a:number + + constructor(a:number) { + this.a = a; + } +} + +@Entry +@Component +struct ViewA { + // 使用@State修饰变量class_A,以监听其变化 + @State class_A: ClassA = new ClassA(0); + + build() { + Column() { + Row(){ + Button(`第一层变量+1`) + .margin({top:10,right:20}) + .backgroundColor('#E8A027') + .onClick(() => { + // class_A的成员变量a加1,class_A发生变化 + this.class_A.a += 1; + }) + // 将第一层变量在UI呈现出来 + Text(`${this.class_A.a}`) + } + .margin({top:50}) + + Row(){ + Button(`第一层变量变为2`) + .margin({top:10,right:20}) + .onClick(() => { + // 将新的ClassA实例赋值给class_A,class_A发生变化 + this.class_A = new ClassA(2); + }) + // 将第一层变量在UI呈现出来 + Text(`${this.class_A.a}`) + } + } + .width('100%') + .justifyContent(FlexAlign.Center) + } +} +``` +效果如下,如图可以看出第一层变量发生变化后可以实时在UI呈现出来,所以@State可以有效的监听第一层变量的状态变化: + +![first-layer-variable](figures/first-layer-variable.gif) + +## 监听多层状态变化 +接下来,我们介绍如何使用@Observed/@ObjectLink监听多层状态变化。 +在第一层状态监听的基础上我们引入ClassB,构造一个嵌套结构,从而具有多层变量,如下: +```ts +// 引入ClassB +class ClassB { + public b: number; + + constructor(b: number) { + this.b = b; + } +} + + +class ClassA { + // ClassA成员变量a的类型为ClassB,从而形成嵌套结构,ClassB的成员变量b为第二层变量 + public a:ClassB + + constructor(a:ClassB) { + this.a = a; + } +} +``` +此时我们可以验证一下,如果仅使用@State是否可以监听到第二层变量的变化: + +```ts +// 引入ClassB +class ClassB { + public b: number; + + constructor(b: number) { + this.b = b; + } +} + +class ClassA { + // ClassA成员变量a的类型为ClassB,从而形成嵌套结构,ClassB的成员变量b为第二层变量 + public a:ClassB + + constructor(a:ClassB) { + this.a = a; + } +} + +@Entry +@Component +struct ViewA { + // 使用@State修饰变量class_A + @State class_A: ClassA = new ClassA(new ClassB(0)); + + build() { + Column() { + Row(){ + // 点击按钮,第二层变量发生变化 + Button('第二层变量+1') + .margin({top:10,right:20}) + .backgroundColor('#E8A027') + .onClick(() => { + // 第二层变量变化,嵌套类ClassB的成员变量b加1 + this.class_A.a.b += 1; + }) + // 将第二层变量在UI呈现出来 + Text(`${this.class_A.a.b}`) + } + .margin({top:50}) + } + .width('100%') + .justifyContent(FlexAlign.Center) + } +} +``` +效果如下,可以看出当第二层变量发生变化时,UI没有任何变化,所以单纯使用@State不能监听到二层及以下变量的变化: + +![second-layer-with-state](figures/second-layer-with-state.gif) + +接下来我们使用@Observed/@ObjectLink监听本例中第二层变量的变化。 +根据使用规则,需要使用@Observed修饰嵌套类,使用@ObjectLink修饰嵌套类的实例,且@ObjectLink不能在被@Entry修饰的组件中使用,所以我们构建一个子组件,然后在父组件中进行引用,具体代码如下: +```ts +// 使用@Observed修饰ClassB +@Observed +class ClassB { + public b: number; + + constructor(b: number) { + this.b = b; + } +} + +class ClassA { + // ClassA成员变量a的类型为ClassB,从而形成嵌套结构,ClassB的成员变量b为第二层变量 + public a:ClassB + + constructor(a:ClassB) { + this.a = a; + } +} + +// 构建子组件ViewB用于承载@ObjectLink修饰的变量 +@Component +struct ViewB { + // 使用@ObjectLink修饰ClassB的实例class_B + @ObjectLink class_B: ClassB; + build() { + Row() { + // 将ClassB的成员变量b在UI呈现出来 + Text(`${this.class_B.b}`) + } + .margin({top:100}) + } +} + +@Entry +@Component +struct ViewA { + @State class_A: ClassA = new ClassA(new ClassB(0)); + + build() { + Column() { + ViewB({ class_B: this.class_A.a }) + Row(){ + // 点击按钮,第二层变量发生变化 + Button('第二层变量class_B.b加1') + .margin({top:10,right:20}) + .backgroundColor('#E8A027') + .onClick(() => { + // 第二层变量变化,嵌套类ClassB的成员变量b加1 + this.class_A.a.b += 1; + }) + } + .margin({top:50}) + } + .width('100%') + .justifyContent(FlexAlign.Center) + } +} +``` +我们来看下效果: + +![second-variable-with-observed-objectlink](figures/second-variable-with-observed-objectlink.gif) + +如图,现在当二层变量发生变化时,可以完美的被监听到,并在UI中刷新出来了。 + +当然,嵌套数组等也是同样的原理,大家可以参考[官方指南](../application-dev/quick-start/arkts-observed-and-objectlink.md)进行尝试。 + +## 参考 +[@Observed和@ObjectLink:嵌套类对象属性变化](../application-dev/quick-start/arkts-observed-and-objectlink.md) \ No newline at end of file