0


鸿蒙(HarmonyOS)--声明式UI、自定义组件

在声明式 UI 中,开发者通过描述界面的结构和外观,以及数据与界面元素之间的关系,而不是直接编写实现界面更新的具体步骤。这意味着开发者只需要明确告诉系统界面应该呈现的样子,而不需要关心底层如何实现这种呈现以及如何处理各种状态变化时的更新操作。

在 ArkTS 中,您可以使用声明式的语法来构建用户界面。通过定义组件的结构、样式和数据绑定等,让框架自动处理界面的渲染和更新。

@Entry
@Component
struct MyComponent {
  @State count: number = 0

  build() {
    Column() {
      Text(`Count: ${this.count}`)
       .fontSize(20)
      Button('Increment')
       .onClick(() => {
          this.count++;
        })
    }
  }
}

在上述示例中,通过声明式的方式构建了一个包含文本和按钮的列布局,并实现了点击按钮增加计数的功能。


1. 基础语法概述

说明

自定义变量不能与基础通用属性/事件名重复。

  • 装饰器: 用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如上述示例中@Entry、@Component和@State都是装饰器
  1. @Component表示自定义组件
  2. @Entry表示该自定义组件为入口组件
  3. @State表示组件中的状态变量状态变量变化会触发UI刷新
  • UI描述:以声明式的方式来描述UI的结构,例如build()方法中的代码块。
  • 自定义组件:可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello。
  • 系统组件:ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的Column、Text、Divider、Button。
  • 属性方法:组件可以通过链式调用配置多项属性,如fontSize()、width()、height()、backgroundColor()等。
  • 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,如跟随在Button后面的onClick()。
  • 系统组件、属性方法、事件方法具体使用可参考基于ArkTS的声明式开发范式。

除此之外,ArkTS扩展了多种语法范式来使开发更加便捷:

  • @Builder/@BuilderParam:特殊的封装UI描述的方法,细粒度的封装和复用UI描述。
  • @Extend/@Styles:扩展内置组件和封装属性样式,更灵活地组合内置组件。

stateStyles:多态样式,可以依据组件的内部状态的不同,设置不同样式。


2.声明式UI描述

声明式 UI 是一种编程模式,在这种模式下,开发者通过描述用户界面应该呈现的最终状态,而非详细地编写实现这个状态的一系列步骤。

以 ArkTS 为例,在声明式 UI 中,您可能会这样描述一个界面组件:

@Entry
@Component
struct MyComponent {
  build() {
    Column() {
      Text('Hello, World!')
       .fontSize(20)
       .color('#0000FF')
      Button('Click Me')
       .onClick(() => {
          // 处理点击事件的逻辑
        })
    }
  }
}

在上述代码中,您没有直接告诉程序如何一步一步地创建和布置这些界面元素,而是直接描述了界面应该包含的元素(如

Text

Button

)、它们的属性(如字体大小、颜色)和行为(如按钮的点击事件)。

声明式 UI 的主要优点包括:

  1. 提高开发效率:开发者可以更专注于业务逻辑和界面的设计,而不需要过多关注底层的实现细节。
  2. 增强可读性和可维护性:代码更清晰、简洁,易于理解和修改。
  3. 自动优化和响应式更新:框架能够根据数据的变化自动更新界面,无需开发者手动管理界面的更新过程。

2.1 创建组件

根据组件构造方法的不同,创建组件包含有参数和无参数两种方式。

2.1.1 无参数

如果组件的接口定义没有包含必选构造参数,则组件后面的“()”不需要配置任何内容。


interface TimeNow{
  da: Date;
}
@Entry
@Component
struct PageTest {
  @State message: TimeNow={da: new Date()};

  build() {
    RelativeContainer() {
      Text()
        .id('PageTestHelloWorld')
        .fontSize(50)
        .width('100%')
        .height('50')
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .backgroundColor(Color.Pink)
    }
    .height('100%')
    .width('100%')
  }
}

2.1.2 有参数

如果组件的接口定义包含构造参数,则在组件后面的“()”需要配置相应参数。


interface TimeNow{
  da: Date;
}
@Entry
@Component
struct PageTest {
  @State message: TimeNow={da: new Date()};

  build() {
    RelativeContainer() {
      Text(this.message.da.toLocaleString().substring(0, 10))
        .id('PageTestHelloWorld')
        .fontSize(50)
        .width('100%')
        .height('50')
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .backgroundColor(Color.Pink)
    }
    .height('100%')
    .width('100%')
  }
}

2.2 配置属性

属性方法以“ **. **”链式调用的方式配置系统组件的样式和其他属性,建议每个属性方法单独写一行。

  • 配置Text组件的字体大小。Text('test').fontSize(12)

  • 配置组件的多个属性。Image('test.jpg') .alt('error.jpg') .width(100) .height(100)

  • 除了直接传递常量参数外,还可以传递变量或表达式。Text('hello') .fontSize(this.size)Image('test.jpg') .width(this.count % 2 === 0 ? 100 : 200) .height(this.offset + 100)

  • 对于系统组件,ArkUI还为其属性预定义了一些枚举类型供开发者调用,枚举类型可以作为参数传递,但必须满足参数类型要求。例如,可以按以下方式配置Text组件的颜色和字体样2.3Text('hello') .fontSize(20) .fontColor(Color.Red) .fontWeight(FontWeight.Bold)

2.3 配置事件

事件方法以“.”链式调用的方式配置系统组件支持的事件,建议每个事件方法单独写一行。

  • 使用箭头函数配置组件的事件方法。Button('Click me') .onClick(() => { this.myText = 'ArkUI'; })
  • 使用箭头函数表达式配置组件的事件方法,要求使用“() => {...}”,以确保函数与组件绑定,同时符合ArkTS语法规范。Button('add counter') .onClick(() => { this.counter += 2; })
  • 使用组件的成员函数配置组件的事件方法,需要bind this。ArkTS语法不推荐使用成员函数配合bind this去配置组件的事件方法。myClickHandler(): void { this.counter += 2;}...Button('add counter') .onClick(this.myClickHandler.bind(this))
  • 使用声明的箭头函数,可以直接调用,不需要bind thisfn = () => { console.info(`counter: ${this.counter}`) this.counter++}...Button('add counter') .onClick(this.fn)

箭头函数内部的this是词法作用域,由上下文确定。匿名函数可能会有this指向不明确问题,在ArkTS中不允许使用。

2.4 配置子组件

如果组件支持子组件配置,则需在尾随闭包"{...}"中为组件添加子组件的UI描述。Column、Row、Stack、Grid、List等组件都是容器组件。

  • 以下是简单的Column组件配置子组件的示例。Column() { Text('Hello') .fontSize(100) Divider() Text(this.myText) .fontSize(100) .fontColor(Color.Red)}

容器组件均支持子组件配置,可以实现相对复杂的多级嵌套。

Column() {
  Row() {
    Image('test1.jpg')
      .width(100)
      .height(100)
    Button('click +1')
      .onClick(() => {
        console.info('+1 clicked!');
      })
  }
}

3.自定义组件

在ArkUI中,UI显示的内容均为组件,由框架直接提供的称为系统组件,由开发者定义的称为自定义组件。在进行 UI 界面开发时,通常不是简单的将系统组件进行组合使用,而是需要考虑代码可复用性、业务逻辑与UI分离,后续版本演进等因素。因此,将UI和部分业务逻辑封装成自定义组件是不可或缺的能力。

自定义组件具有以下特点:

  • 可组合:允许开发者组合使用系统组件、及其属性和方法。
  • 可重用:自定义组件可以被其他组件重用,并作为不同的实例在不同的父组件或容器中使用。
  • 数据驱动UI更新:通过状态变量的改变,来驱动UI的刷新。

3.1创建自定义组件

3.1.1 基本使用

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

// 定义
@Component
struct MyComponent {
  // 状态变量
  @State message:string =''
  build(){
    // .... 描述 UI
  }
}

//----------使用-----------

// 1.不传递参数使用
// MyComponent() 

// 2.传递参数使用:通过传递参数的方式 设置子组件中 messsage 的值
// MyComponent({message:'xxx'})
  • struct:自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。
  • @Component:@Component装饰器仅能装饰struct关键字声明的数据结构。struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。
  • build()函数:build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。
  • @Entry:@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。
  • @Preview:如果想要单独预览组件,可以使用@Preview 进行装饰
  • @Reusable:@Reusable装饰的自定义组件具备可复用能力

写在一个文件中

// 定义组件
@Component
struct HelloComponent {
  // 自己的状态
  @State message: string = 'Hello, World!';

  build() {
    // HelloComponent自定义组件组合系统组件Row和Text
    Row() {
      Text(this.message)
        .onClick(() => {
          // 状态变量message的改变驱动UI刷新,UI从'Hello, World!'刷新为'Hello, ArkUI!'
          this.message = 'Hello, ArkUI!';
        })
    }
  }
}

// 使用组件
@Entry
@Component
struct CustomComponentDemo {
  build() {
    Column() {
      HelloComponent()
      // 只要愿意,可以 使用任意多次
      HelloComponent()
      // 还可以 传递参数给子组件,覆盖子组件成员变量的值
      HelloComponent({message:'hello itheima'})
    }
  }
}

写在多个文件中

// HelloComponent.ets
// 定义组件
@Component
export struct HelloComponent {
  // 自己的状态
  @State message: string = 'Hello, World!';

  build() {
    // HelloComponent自定义组件组合系统组件Row和Text
    Row() {
      Text(this.message)
        .onClick(() => {
          // 状态变量message的改变驱动UI刷新,UI从'Hello, World!'刷新为'Hello, ArkUI!'
          this.message = 'Hello, ArkUI!';
        })
    }
  }
}

// 页面.ets-------------------
import {HelloComponent} from 'HelloComponent'

// 使用组件
@Entry
@Component
struct CustomComponentDemo {
  build() {
    Column() {
      HelloComponent()
      // 只要愿意,可以 使用任意多次
      HelloComponent()
      // 还可以 传递参数给子组件,覆盖子组件成员变量的值
      HelloComponent({message:'hello itheima'})
    }
  }
}

3.1.2 组件属性、方法

自定义组件除了必须要实现build()函数外,其他【属性】和【方法】的写法 跟之前一样

注意:

  1. 方法、变量均为私有
  2. 方法名(){},组件内部抽取逻辑用这种写法
@Component
struct MyComponent {
  // 属性:状态变量
  @State message:string=''
  // 属性:成员变量-数据
  info:string = ''
  
  // 方法:普通写法
  sayHi(){
    
  }
  
  build(){
    // .... 描述 UI
  }
}

参考代码

// 自定义组件
@Component
export struct HelloComponent {
  // 属性:状态变量
  @State message: string = 'Hello, World!';
  // 属性:普通变量
  info: string = '感觉自己闷闷哒'

  // 方法:传统写法
  sayHi() {
    AlertDialog.show({ message: '你好呀' })
  }

  build() {
    // HelloComponent自定义组件组合系统组件Row和Text
    Column() {
      Text(this.message)
      Text(this.info)
      Button('修改数据')
        .onClick(() => {
          this.info = '(*  ̄3)(ε ̄ *)'
          this.message = 'Hello,ArkTS'
          this.sayHi()
        })

    }
    .border({ width: 1 })
    .padding(5)
    .margin(5)
  }
}

@Entry
@Component
struct Page02_Component_Param {
  @State message: string = '组件成员变量';

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        // 不传递参数,使用组件内部的默认值
        HelloComponent()
        // 传递参数,会覆盖组件内部的默认值
        HelloComponent({ info: '你好', message: 'ArkTS' })

      }
      .width('100%')
    }
    .height('100%')
  }
}

3.1.3 通用样式事件

自定义组件可以通过点语法的形式设置通用样式,通用事件

子组件()
  .width(100)
  .height(100)
  .backgroundColor(Color.Orange)
  .onClick(() => {
      console.log('外部添加的点击事件')
    })

参考代码

@Component
struct MyComponent2 {
  build() {
    Button(`Hello World`)
  }
}

@Entry
@Component
struct MyComponent {
  build() {
    Row() {
      MyComponent2()
        .width(200)
        .height(300)
        .backgroundColor(Color.Red)
        .onClick(() => {
            console.log('外部添加的点击事件')
          })
    }
  }
}

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

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

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

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

  • onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景。
  • onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。
  • onBackPress:当用户点击返回按钮时触发。

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

  • aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
  • onDidBuild:组件build()函数执行完成之后回调该接口,不建议在onDidBuild函数中更改状态变量、使用animateTo等功能,这可能会导致不稳定的UI表现。
  • aboutToDisappear:aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。

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

根据上面的流程图,我们从自定义组件的初始创建、重新渲染和删除来详细解释。

3.2.1 自定义组件的创建和渲染流程

  1. 自定义组件的创建:自定义组件的实例由ArkUI框架创建。
  2. 初始化自定义组件的成员变量:通过本地默认值或者构造方法传递参数来初始化自定义组件的成员变量,初始化顺序为成员变量的定义顺序。
  3. 如果开发者定义了aboutToAppear,则执行aboutToAppear方法。
  4. 在首次渲染的时候,执行build方法渲染系统组件,如果子组件为自定义组件,则创建自定义组件的实例。在首次渲染的过程中,框架会记录状态变量和组件的映射关系,当状态变量改变时,驱动其相关的组件刷新。
  5. 如果开发者定义了onDidBuild,则执行onDidBuild方法。

3.2.2 自定义组件重新渲染

当事件句柄被触发(比如设置了点击事件,即触发点击事件)改变了状态变量时,或者LocalStorage / AppStorage中的属性更改,并导致绑定的状态变量更改其值时:

  1. 框架观察到了变化,将启动重新渲染。
  2. 根据框架持有的两个map(自定义组件的创建和渲染流程中第4步),框架可以知道该状态变量管理了哪些UI组件,以及这些UI组件对应的更新函数。执行这些UI组件的更新函数,实现最小化更新。

3.2.3 自定义组件的删除

如果if组件的分支改变,或者ForEach循环渲染中数组的个数改变,组件将被删除:

  1. 在删除组件之前,将调用其aboutToDisappear生命周期函数,标记着该节点将要被销毁。ArkUI的节点删除机制是:后端节点直接从组件树上摘下,后端节点被销毁,对前端节点解引用,前端节点已经没有引用时,将被JS虚拟机垃圾回收。
  2. 自定义组件和它的变量将被删除,如果其有同步的变量,比如@Link、@Prop、@StorageLink,将从同步源上取消注册。

不建议在生命周期aboutToDisappear内使用async await,如果在生命周期的aboutToDisappear使用异步操作(Promise或者回调方法),自定义组件将被保留在Promise的闭包中,直到回调方法被执行完,这个行为阻止了自定义组件的垃圾回收。

下示例展示了生命周期的调用时机:

// Index.ets
import { router } from '@kit.ArkUI';

@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');
  }

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

  // 组件生命周期
  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')
  }

  // 组件生命周期
  onDidBuild() {
    console.info('[lifeCycle] Child onDidBuild');
  }

  // 组件生命周期
  aboutToAppear() {
    console.info('[lifeCycle] Child aboutToAppear')
  }

  build() {
    Text(this.title)
      .fontSize(50)
      .margin(20)
      .onClick(() => {
        this.title = 'Hello ArkUI';
      })
  }
}
// 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 / onDidBuild/aboutToDisappear)。

  • 应用冷启动的初始化流程为:MyComponent aboutToAppear --> MyComponent build --> MyComponent onDidBuild--> Child aboutToAppear --> Child build --> Child onDidBuild --> 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。

3.2.3 自定义组件监听页面生命周期

使用无感监听页面路由的能力,能够实现在自定义组件中监听页面的生命周期。

// Index.ets
import { uiObserver, router, UIObserver } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  listener: (info: uiObserver.RouterPageInfo) => void = (info: uiObserver.RouterPageInfo) => {
    let routerInfo: uiObserver.RouterPageInfo | undefined = this.queryRouterPageInfo();
    if (info.pageId == routerInfo?.pageId) {
      if (info.state == uiObserver.RouterPageState.ON_PAGE_SHOW) {
        console.log(`Index onPageShow`);
      } else if (info.state == uiObserver.RouterPageState.ON_PAGE_HIDE) {
        console.log(`Index onPageHide`);
      }
    }
  }
  aboutToAppear(): void {
    let uiObserver: UIObserver = this.getUIContext().getUIObserver();
    uiObserver.on('routerPageUpdate', this.listener);
  }
  aboutToDisappear(): void {
    let uiObserver: UIObserver = this.getUIContext().getUIObserver();
    uiObserver.off('routerPageUpdate', this.listener);
  }
  build() {
    Column() {
      Text(`this page is ${this.queryRouterPageInfo()?.pageId}`)
        .fontSize(25)
      Button("push self")
        .onClick(() => {
          router.pushUrl({
            url: 'pages/Index'
          })
        })
      Column() {
        SubComponent()
      }
    }
  }
}
@Component
struct SubComponent {
  listener: (info: uiObserver.RouterPageInfo) => void = (info: uiObserver.RouterPageInfo) => {
    let routerInfo: uiObserver.RouterPageInfo | undefined = this.queryRouterPageInfo();
    if (info.pageId == routerInfo?.pageId) {
      if (info.state == uiObserver.RouterPageState.ON_PAGE_SHOW) {
        console.log(`SubComponent onPageShow`);
      } else if (info.state == uiObserver.RouterPageState.ON_PAGE_HIDE) {
        console.log(`SubComponent onPageHide`);
      }
    }
  }
  aboutToAppear(): void {
    let uiObserver: UIObserver = this.getUIContext().getUIObserver();
    uiObserver.on('routerPageUpdate', this.listener);
  }
  aboutToDisappear(): void {
    let uiObserver: UIObserver = this.getUIContext().getUIObserver();
    uiObserver.off('routerPageUpdate', this.listener);
  }
  build() {
    Column() {
      Text(`SubComponent`)
    }
  }
}

3.3 自定义组件的自定义布局

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

  • onMeasureSize:组件每次布局时触发,计算子组件的尺寸,其执行时间先于onPlaceChildren。
  • onPlaceChildren:组件每次布局时触发,设置子组件的起始位置。
// 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 {
  @Builder
  doNothingBuilder() {
  };

  @BuilderParam builder: () => void = this.doNothingBuilder;
  @State startSize: number = 100;
  result: SizeResult = {
    width: 0,
    height: 0
  };

  // 第一步:计算各子组件的大小
  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方式传入。

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

3.4 自定义组件成员属性访问限定符使用限制

3.4.1 使用限制

  • 对于@State/@Prop/@Provide/@BuilderParam/常规成员变量(不涉及更新的普通变量),当使用private修饰时,在自定义组件构造时,不允许进行赋值传参,否则会有编译告警日志提示。
  • 对于@StorageLink/@StorageProp/@LocalStorageLink/@LocalStorageProp/@Consume变量,当使用public修饰时,会有编译告警日志提示。
  • 对于@Link/@ObjectLink变量,当使用private修饰时,会有编译告警日志提示。
  • 由于struct没有继承能力,上述所有的这些变量使用protected修饰时,会有编译告警日志提示。
  • 当@Require和private同时修饰自定义组件struct的@State/@Prop/@Provide/@BuilderParam/常规成员变量(不涉及更新的普通变量)时,会有编译告警日志提示。

3.4.2错误使用场景示例

1.当成员变量被private访问限定符和@State/@Prop/@Provide/@BuilderParam装饰器同时修饰时,ArkTS会进行校验并产生告警日志。

@Entry
@Component
struct AccessRestrictions {
  @Builder buildTest() {
    Text("Parent builder")
  }
  build() {
    Column() {
      ComponentsChild({state_value: "Hello", prop_value: "Hello", provide_value: "Hello", builder_value: this.buildTest, regular_value: "Hello"})
    }
    .width('100%')
  }
}

@Component
struct ComponentsChild {
  @State private state_value: string = "Hello";
  @Prop private prop_value: string = "Hello";
  @Provide private provide_value: string = "Hello";
  @BuilderParam private builder_value: () => void = this.buildTest;
  private regular_value: string = "Hello";
  @Builder buildTest() {
    Text("Child builder")
  }
  build() {
    Column() {
      Text("Hello")
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
    }
  }
}

编译告警日志如下:

Property 'state_value' is private and can not be initialized through the component constructor.
Property 'prop_value' is private and can not be initialized through the component constructor.
Property 'provide_value' is private and can not be initialized through the component constructor.
Property 'builder_value' is private and can not be initialized through the component constructor.
Property 'regular_value' is private and can not be initialized through the component constructor.

这个错误提示表明您正在尝试通过组件的构造函数来初始化一些私有的属性(

state_value

prop_value

provide_value

builder_value

regular_value

),但这是不允许的。

通常,私有属性应该在组件内部通过特定的方法或逻辑进行初始化和操作,而不是从外部直接初始化。

2.当成员变量被public访问限定符和@StorageLink/@StorageProp/@LocalStorageLink/@LocalStorageProp/@Consume装饰器同时修饰时,ArkTS会进行校验并产生告警日志。

@Entry
@Component
struct AccessRestrictions {
  @Provide consume_value: string = "Hello";
  build() {
    Column() {
      ComponentChild()
    }
    .width('100%')
  }
}

@Component
struct ComponentChild {
  @LocalStorageProp("sessionLocalProp") public local_prop_value: string = "Hello";
  @LocalStorageLink("sessionLocalLink") public local_link_value: string = "Hello";
  @StorageProp("sessionProp") public storage_prop_value: string = "Hello";
  @StorageLink("sessionLink") public storage_link_value: string = "Hello";
  @Consume public consume_value: string;
  build() {
    Column() {
      Text("Hello")
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
    }
  }
}

编译告警日志如下:

Property 'local_prop_value' can not be decorated with both @LocalStorageProp and public.
Property 'local_link_value' can not be decorated with both @LocalStorageLink and public.
Property 'storage_prop_value' can not be decorated with both @StorageProp and public.
Property 'storage_link_value' can not be decorated with both @StorageLink and public.
Property 'consume_value' can not be decorated with both @Consume and public.

这些错误提示表明您对某些属性(

local_prop_value

local_link_value

storage_prop_value

storage_link_value

consume_value

)同时使用了不兼容的装饰器(如

@LocalStorageProp

@LocalStorageLink

@StorageProp

@StorageLink

@Consume

)和

public

修饰符。

您需要检查这些属性的定义,选择合适的修饰方式,不能同时使用相互冲突的修饰。可能需要删除

public

修饰符或者调整装饰器的使用。

3.当成员变量被private访问限定符和@Link/@ObjectLink装饰器同时修饰时,ArkTS会进行校验并产生告警日志。

@Entry
@Component
struct AccessRestrictions {
  @State link_value: string = "Hello";
  @State objectLink_value: ComponentObj = new ComponentObj();
  build() {
    Column() {
      ComponentChild({link_value: this.link_value, objectLink_value: this.objectLink_value})
    }
    .width('100%')
  }
}

@Observed
class ComponentObj {
  count: number = 0;
}
@Component
struct ComponentChild {
  @Link private link_value: string;
  @ObjectLink private objectLink_value: ComponentObj;
  build() {
    Column() {
      Text("Hello")
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
    }
  }
}

编译告警日志如下:

Property 'link_value' can not be decorated with both @Link and private.
Property 'objectLink_value' can not be decorated with both @ObjectLink and private.

这些错误表示属性

link_value

不能同时被

@Link

装饰器和

private

修饰符修饰,属性

objectLink_value

不能同时被

@ObjectLink

装饰器和

private

修饰符修饰。

您需要决定该属性是私有访问还是与链接相关的访问,并相应地只使用一种合适的修饰方式。要么将其设置为私有(仅在当前类内部可访问)并去除链接相关的装饰器,要么保留链接相关的装饰器并将其设置为非私有的访问权限(例如

public

或不添加任何访问修饰符,默认通常是公开的)

4.当成员变量被protected访问限定符修饰时,ArkTS会进行校验并产生告警日志。

@Entry
@Component
struct AccessRestrictions {
  build() {
    Column() {
      ComponentChild({regular_value: "Hello"})
    }
    .width('100%')
  }
}

@Component
struct ComponentChild {
  protected regular_value: string = "Hello";
  build() {
    Column() {
      Text("Hello")
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
    }
  }
}

编译告警日志如下:

The member attributes of a struct can not be protected.

这个错误提示意味着在结构体中不能使用

protected

修饰成员属性。在大多数编程语言中,结构体的成员属性通常只有

public

(公共的,可在任何地方访问)和

private

(私有的,只能在结构体内部访问)两种访问修饰符。

您可能需要将成员属性的修饰符修改为

public

private

来解决这个问题。

5.当成员变量被private访问限定符、@Require和@State/@Prop/@Provide/@BuilderParam装饰器同时修饰时,ArkTS会进行校验并产生告警日志。

@Entry
@Component
struct AccessRestrictions {
  build() {
    Column() {
      ComponentChild({prop_value: "Hello"})
    }
    .width('100%')
  }
}
@Component
struct ComponentChild {
  @Require @Prop private prop_value: string = "Hello";
  build() {
    Column() {
      Text("Hello")
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
    }
  }
}

编译告警日志如下:

Property 'prop_value' can not be decorated with both @Require and private.
Property 'prop_value' is private and can not be initialized through the component constructor.
  1. 属性 prop_value 不能同时被 @Require 装饰器和 private 修饰符修饰。
  2. 属性 prop_value 是私有的,不能通过组件的构造函数进行初始化。

您需要做出以下修改之一:

  1. 如果您希望使用 @Require 装饰器,应将 prop_value 的修饰符改为非 private ,比如 public 或不添加修饰符(默认通常为 public )。
  2. 如果您希望 prop_value 保持为私有,那么需要移除 @Require 装饰器,并且不能在组件构造函数中对其进行初始化。
标签: ui

本文转载自: https://blog.csdn.net/hqy1989/article/details/142434678
版权归原作者 谢道韫666 所有, 如有侵权,请联系我们删除。

“鸿蒙(HarmonyOS)--声明式UI、自定义组件”的评论:

还没有评论