HarmonyOS NEXT开发进阶(三):自定义组件

一、前言

🔊:自定义组件必须使用struct定义,并且被Component装饰器修饰。

arkTs中,自定义组件分为两种:

  • 根组件 :就是被装饰器@Entry装饰的入口组件,这也是自定义组件(父组件)。
typescript 复制代码
// 根组件(父组件)
@Entry
@Component
struct FatherComponent {}
  • 子组件 :没有被@Entry装饰的自定义组件,只有@Component装饰器(子组件)。
typescript 复制代码
// 子组件
@Component
struct SonComponent {}

❗️注意:

  1. 子组件必须被父组件调用,才能在页面上展示出来,它自己无法展示。页面由一个父组件和无数个子组件、系统组件构成。
  2. 如果在另外的文件中引用该自定义组件,需要使用export关键字导出,并在使用的页面import该自定义组件。

二、自定义组件的基本结构

struct:自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new

🍀说明:自定义组件名、类名、函数名不能和系统组件名相同。

  • @Component@Component装饰器仅能装饰struct关键字声明的数据结构。struct@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。@Component可以接受一个可选的bool类型参数。
typescript 复制代码
@Component
struct MyComponent {
  build() {
    //描述UI
  }
}

🍀说明:从API version 11开始,@Component可以接受一个可选的bool类型参数。

自定义组件处于非激活状态时,状态变量将不响应更新,即@Watch不会调用,状态变量关联的节点不会刷新。通过freezeWhenInactive属性来决定是否使用冻结功能,不传参数时默认不使用。支持的场景有:页面路由TabContentLazyForEachNavigation

typescript 复制代码
@Component({freezeWhenInactive: true})
struct MyComponent {
  build() {
    //描述UI
  }
}
  • build()函数:build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。
typescript 复制代码
@Component
struct MyComponent {
  build() {
    //描述UI
  }
}
  • @Entry@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用一个@Entry装饰一个自定义组件。@Entry可以接受一个可选的LocalStorage的参数。
typescript 复制代码
@Entry
@Component
struct MyComponent {
  build() {
    //描述UI
  }
}

🍀说明:从API version 10开始,@Entry可以接受一个可选的LocalStorage的参数或者一个可选的EntryOptions参数。

typescript 复制代码
@Entry({ routeName : 'myPage' })
@Component
struct MyComponent {
  build() {
    //描述UI
  }
}
  • @Reusable@Reusable装饰的自定义组件具备可复用能力。

🍀说明:从API version 10开始,该装饰器支持在ArkTS卡片中使用。

三、页面和自定义组件生命周期

在开始之前,先明确自定义组件和页面的关系:

  • 自定义组件@Component装饰的UI单元,可以组合多个系统组件实现UI的复用,可以调用组件的生命周期。

  • 页面 :即应用的UI页面。可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。

页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:

  • onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景。

  • onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。

  • onBackPress:当用户点击返回按钮时触发。

组件生命周期,即一般用@Component装饰的自定义组件的生命周期,提供以下生命周期接口:

  • aboutToAppear :组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。

  • aboutToDisappearaboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。

生命周期流程图,下图展示的是被@Entry装饰的组件(页面)生命周期。

示例:

index.ets

typescript 复制代码
// Index.ets
import router from '@ohos.router';

@Entry
@Component
struct MyComponent {
  @State showChild: boolean = true;
  @State btnColor:string = "#FF007DFF"

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageShow() {
    console.info('Index onPageShow');
  }
  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageHide() {
    console.info('Index onPageHide');
  }

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onBackPress() {
    console.info('Index onBackPress');
    this.btnColor ="#FFEE0606"
    return true // 返回true表示页面自己处理返回逻辑,不进行页面路由;返回false表示使用默认的路由返回逻辑,不设置返回值按照false处理
  }

  // 组件生命周期
  aboutToAppear() {
    console.info('MyComponent aboutToAppear');
  }

  // 组件生命周期
  aboutToDisappear() {
    console.info('MyComponent aboutToDisappear');
  }

  build() {
    Column() {
      // this.showChild为true,创建Child子组件,执行Child aboutToAppear
      if (this.showChild) {
        Child()
      }
      // this.showChild为false,删除Child子组件,执行Child aboutToDisappear
      Button('delete Child')
      .margin(20)
      .backgroundColor(this.btnColor)
      .onClick(() => {
        this.showChild = false;
      })
      // push到page页面,执行onPageHide
      Button('push to next page')
        .onClick(() => {
          router.pushUrl({ url: 'pages/page' });
        })
    }

  }
}

@Component
struct Child {
  @State title: string = 'Hello World';
  // 组件生命周期
  aboutToDisappear() {
    console.info('[lifeCycle] Child aboutToDisappear')
  }
  // 组件生命周期
  aboutToAppear() {
    console.info('[lifeCycle] Child aboutToAppear')
  }

  build() {
    Text(this.title).fontSize(50).margin(20).onClick(() => {
      this.title = 'Hello ArkUI';
    })
  }
}

page.ets

typescript 复制代码
// page.ets
@Entry
@Component
struct page {
  @State textColor: Color = Color.Black;
  @State num: number = 0

  onPageShow() {
    this.num = 5
  }

  onPageHide() {
    console.log("page onPageHide");
  }

  onBackPress() { // 不设置返回值按照false处理
    this.textColor = Color.Grey
    this.num = 0
  }

  aboutToAppear() {
    this.textColor = Color.Blue
  }

  build() {
    Column() {
      Text(`num 的值为:${this.num}`)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.textColor)
        .margin(20)
        .onClick(() => {
          this.num += 5
        })
    }
    .width('100%')
  }
}

以上示例中,Index页面包含两个自定义组件,一个是被@Entry装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。只有@Entry装饰的节点才可以使页面级别的生命周期方法生效,因此在MyComponent中声明当前Index页面的页面生命周期函数(onPageShow / onPageHide / onBackPress)。MyComponent和其子组件Child分别声明了各自的组件级别生命周期函数(aboutToAppear / aboutToDisappear)。

应用冷启动的初始化流程为:MyComponent aboutToAppear --> MyComponent build --> Child aboutToAppear --> Child build --> Child build执行完毕 --> MyComponent build执行完毕 --> Index onPageShow

点击"delete Child",if绑定的this.showChild变成false,删除Child组件,会执行Child aboutToDisappear方法。

点击"push to next page",调用router.pushUrl接口,跳转到另外一个页面,当前Index页面隐藏,执行页面生命周期Index onPageHide。此处调用的是router.pushUrl接口,Index页面被隐藏,并没有销毁,所以只调用onPageHide。跳转到新页面后,执行初始化新页面的生命周期的流程。

如果调用的是router.replaceUrl,则当前Index页面被销毁,执行的生命周期流程将变为:Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已经提到,组件的销毁是从组件树上直接摘下子树,所以先调用父组件的aboutToDisappear,再调用子组件的aboutToDisappear,然后执行初始化新页面的生命周期流程。

点击返回按钮,触发页面生命周期Index onBackPress,且触发返回一个页面后会导致当前Index页面被销毁。

最小化应用或者应用进入后台,触发Index onPageHide。当前Index页面没有被销毁,所以并不会执行组件的aboutToDisappear。应用回到前台,执行Index onPageShow。

退出应用,执行Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear

四、自定义组件的自定义布局

如果需要通过测算的方式布局自定义组件内子组件的位置,建议使用以下接口:

  • onMeasureSize:组件每次布局时触发,计算子组件的尺寸,其执行时间先于onPlaceChildren

  • onPlaceChildren:组件每次布局时触发,设置子组件的起始位置。

应用示例:

typescript 复制代码
// xxx.ets
@Entry
@Component
struct Index {
  build() {
    Column() {
      CustomLayout({ builder: ColumnChildren })
    }
  }
}

// 通过builder的方式传递多个组件,作为自定义组件的一级子组件(即不包含容器组件,如Column)
@Builder
function ColumnChildren() {
  ForEach([1, 2, 3], (index: number) => { // 暂不支持lazyForEach的写法
    Text('S' + index)
      .fontSize(30)
      .width(100)
      .height(100)
      .borderWidth(2)
      .offset({ x: 10, y: 20 })
  })
}

@Component
struct CustomLayout {
  /** 属性 */
  @State startSize: number = 100;
  @BuilderParam builder: () => void = this.doNothingBuilder;
  private result: SizeResult = {
    width: 0,
    height: 0
  };

  /** 自定义构建函数 */
  @Builder
  doNothingBuilder() {
  };

  /** 自定义布局 */
  // 第一步:计算各子组件的大小
  onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>, constraint: ConstraintSizeOptions) {
    let size = 100;
    children.forEach((child) => {
      let result: MeasureResult = child.measure({ minHeight: size, minWidth: size, maxWidth: size, maxHeight: size })
      size += result.width / 2;
    })
    this.result.width = 100;
    this.result.height = 400;
    return this.result;
  }

  // 第二步:放置各子组件的位置
  onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, constraint: ConstraintSizeOptions) {
    let startPos = 300;
    children.forEach((child) => {
      let pos = startPos - child.measureResult.height;
      child.layout({ x: pos, y: pos })
    })
  }

  build() {
    this.builder()
  }
}

以上示例中,Index页面包含一个实现了自定义布局的自定义组件,且对应自定义组件的子组件通过index页面内的builder方式传入。

而在自定义组件中,调用了onMeasureSizeonPlaceChildren设置子组件大小和放置位置。例如,在本示例中,在onMeasureSize中初始化组件大小size=100,后续的每一个子组件size会加上上一个子组件大小的一半,实现组件大小递增的效果。而在onPlaceChildren中,定义startPos=300,设置每一个子组件的位置为startPos减去子组件自身的高度,所有子组件右下角一致在顶点位置(300,300),实现一个从右下角开始展示组件的类Stack组件。

五、自定义组件冻结功能场景

5.1 页面跳转

当页面A调用router.pushUrl接口跳转到页面B时,页面A为隐藏不可见状态,此时如果更新页面A中的状态变量,不会触发页面A刷新。

当应用退到后台运行时无法被冻结。

页面A:

typescript 复制代码
import router from '@ohos.router';

@Entry
@Component({ freezeWhenInactive: true })
struct FirstTest {
  /** 状态变量 */
  @StorageLink('PropA') @Watch("first") storageLink: number = 47;

  /** 自定义方法 */
  private  first() {
    console.info("first page " + `${this.storageLink}`)
  }

  /** 系统构建函数 */
  build() {
    Column({space:10}) {
      Text(`From fist Page ${this.storageLink}`)
        .fontSize(30)

      Button('first page storageLink + 1')
        .fontSize(18)
        .onClick(() => {
          this.storageLink += 1
        })

      Button('go to next page')
        .fontSize(18)
        .onClick(() => {
          router.pushUrl({ url: 'pages/second' })
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

页面B:

typescript 复制代码
import router from '@ohos.router';

@Entry
@Component({ freezeWhenInactive: true })
struct SecondTest {
  /** 状态变量 */
  @StorageLink('PropA') @Watch("second") storageLink2: number = 1;

  /** 自定义函数 */
  private second() {
    console.info("second page: " + `${this.storageLink2}`)
  }

  build() {
    Column({ space: 10 }) {
      Text(`second Page ${this.storageLink2}`).fontSize(30)

      Button('Change Divider.strokeWidth')
        .fontSize(18)
        .onClick(() => {
          router.back()
        })

      Button('second page storageLink2 + 2')
        .fontSize(18)
        .onClick(() => {
          this.storageLink2 += 2
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

结果:

当页面A 设置 @Component({ freezeWhenInactive: false }) ,点击页面B点击 'second page storeageLink2' 按钮时,结果如下:

当页面A 设置 @Component({ freezeWhenInactive: true }) ,点击页面B 点击 'second page storeageLink2' 按钮时,结果如下:

在上面的示例中:

  1. 点击页面A中的Button "first page storageLink + 1",storLink状态变量改变,@Watch中注册的方法first会被调用。

  2. 通过router.pushUrl({url: 'pages/second'}),跳转到页面B,页面A隐藏,状态由active变为inactive

  3. 点击页面B中的Button "this.storageLink2 += 2",只回调页面B@Watch中注册的方法second,因为页面A的状态变量此时已被冻结。

  4. 点击"back",页面B被销毁,页面A的状态由inactive变为active,重新刷新在inactive时被冻结的状态变量,页面A @Watch中注册的方法first被再次调用。

5.2 TabContent

对Tabs中当前不可见的TabContent进行冻结,不会触发组件的更新。

需要注意的是:在首次渲染的时候,Tab只会创建当前正在显示的TabContent,当切换全部的TabContent后,TabContent才会被全部创建。

⏰ 示例

typescript 复制代码
@Entry
@Component
struct TabContentTest {
  /** 状态变量 */
  @State @Watch("onMessageUpdated") message: number = 0;
  private data: number[] = [0, 1]

  /** 自定义函数 */
  onMessageUpdated() {
    console.info(`TabContent message callback func ${this.message}`)
  }

  /** 系统构建函数 */
  build() {
    Row() {
      Column() {
        Button('change message')
          .margin({ top: 10 })
          .onClick(() => {
            this.message++
          })
        
        Tabs() {
          ForEach(this.data, (item: number) => {
            TabContent() {
              FreezeChild({ message: this.message, index: item })
            }.tabBar(`tab${item}`)
          }, (item: number) => item.toString())
        }
      }
      .width('100%')
    }
    .height('100%')
  }
}

@Component({ freezeWhenInactive: true })
struct FreezeChild {
  /** 状态变量 */
  @Link @Watch("onMessageUpdated") message: number
  private index: number = 0

  /** 自定义函数 */
  onMessageUpdated() {
    console.info(`FreezeChild message callback func ${this.message}, index: ${this.index}`)
  }

  /** 系统构建函数 */
  build() {
    Text("message" + `${this.message}, index: ${this.index}`)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
  }
}

结果:

当设置自定义组件 FreezeChild 设置 @Component({ freezeWhenInactive: false })

当设置自定义组件 FreezeChild 设置 @Component({ freezeWhenInactive: true })

相关推荐
Lalolander5 小时前
国产自主可控新征程:华为原生鸿蒙系统与鲲鹏认证
安全·华为·harmonyos·持续集成
Alfredorw5 小时前
鸿蒙元服务上架
华为·harmonyos
techSista9 小时前
华为FreeBuds Pro 4丢了如何找回?(附查找功能使用方法)
华为·智能手机·音视频
谢道韫66611 小时前
鸿蒙面试---1210
华为·harmonyos
神码兄弟11 小时前
从 Router 到 Navigation:HarmonyOS 路由框架的全面升级与迁移指南
华为·harmonyos
JJH的创世纪12 小时前
华为USG系列防火墙 RESTCONF NAT配置 Python实现
网络·华为·restconf
yg_小小程序员13 小时前
鸿蒙开发(4)Arkts文档
华为·harmonyos
SoraLuna14 小时前
「Mac玩转仓颉内测版52」基础篇14 - 递归函数与尾递归优化
开发语言·算法·macos·华为·cangjie
青瓷看世界18 小时前
华为HarmonyOS帮助应用接入意图框架 -- 2 接入方案
华为·harmonyos