提交 d6697ef1 编写于 作者: D duangavin123

新增案例

Signed-off-by: Nduangavin123 <duanxichao@huawei.com>
上级 531c2c69
# 折叠展开动效
## 场景介绍
由于目前移动端需要展示的内容越来越多,但是移动端的空间弥足珍贵,在有限的空间内不可能罗列展示全部种类内容,因此折叠/展开功能就可以解决当前问题,本文就介绍下如何使用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/)
# 如何创建悬浮窗
## 场景说明
悬浮窗功能可以基于当前任务创建一个始终在前台显示的窗口。即使创建悬浮窗的任务退至后台,悬浮窗仍然可以在前台显示,通常悬浮窗位于所有应用窗口之上。很多应用都具有悬浮窗的功能,常见的如视频应用的视频播放窗口,在视频应用切换到后台后,视频播放窗口还可以在前台以小窗形式继续播放。本例即为大家介绍如何开发悬浮窗。
## 效果呈现
本例效果如下:
![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
# 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/)
# 如何监听多层状态变化
## 场景说明
应用开发过程中,当希望通过状态变量控制页面刷新时,大家通常想到的就是装饰器@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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册