HarmonyOS UI范式-基本語法

參考文檔

https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V4/2_4ui_u8303_u5f0f-0000001637400294-V4

目錄

  • 基本語法概述
  • 聲明式UI描述
  • 創(chuàng)建自定義組件
  • 頁面和自定義組件生命周期
  • 裝飾器

基本語法概述

我們用一個具體的示例來說明ArkTS的基本組成。如下圖所示,當開發(fā)者點擊按鈕時,文本內(nèi)容從“Hello World”變?yōu)椤癏ello ArkUI”:

本示例中,ArkTS的基本組成如下所示:

  • 裝飾器: 用于裝飾類、結(jié)構(gòu)、方法以及變量,并賦予其特殊的含義。如上述示例中@Entry、@Component和@State都是裝飾器,@Component表示自定義組件,@Entry表示該自定義組件為入口組件,@State表示組件中的狀態(tài)變量,狀態(tài)變量變化會觸發(fā)UI刷新。

  • UI描述:以聲明式的方式來描述UI的結(jié)構(gòu),例如build()方法中的代碼塊。

  • 自定義組件:可復(fù)用的UI單元,可組合其他組件,如上述被@Component裝飾的struct Hello。

  • 系統(tǒng)組件:ArkUI框架中默認內(nèi)置的基礎(chǔ)和容器組件,可直接被開發(fā)者調(diào)用,比如示例中的Column、Text、Divider、Button。

  • 屬性方法:組件可以通過鏈式調(diào)用配置多項屬性,如fontSize()、width()、height()、backgroundColor()等。

  • 事件方法:組件可以通過鏈式調(diào)用設(shè)置多個事件的響應(yīng)邏輯,如跟隨在Button后面的onClick()。

  • 系統(tǒng)組件、屬性方法、事件方法具體使用可參考基于ArkTS的聲明式開發(fā)范式。

聲明式UI描述

ArkTS以聲明方式組合和擴展組件來描述應(yīng)用程序的UI,同時還提供了基本的屬性、事件和子組件配置方法,幫助開發(fā)者實現(xiàn)應(yīng)用交互邏輯。

創(chuàng)建組件

根據(jù)組件構(gòu)造方法的不同,創(chuàng)建組件包含有參數(shù)和無參數(shù)兩種方式。

Column() {
  // string類型的參數(shù)
  Text('item 1')
  // 無參數(shù)
  Divider()
  // $r形式引入應(yīng)用資源,可應(yīng)用于多語言場景
  Text($r('app.string.title_value'))
  // 使用變量作為參數(shù)
  Image(this.imagePath)
  // 使用表達式作為參數(shù)
  Image('https://' + this.imageUrl)
}

配置屬性

屬性方法以“.”鏈式調(diào)用的方式配置系統(tǒng)組件的樣式和其他屬性,建議每個屬性方法單獨寫一行。

Text('test')
  // 配置字體大小
  .fontSize(12)
  // 使用預(yù)定義的枚舉作為參數(shù)
  .fontColor(Color.Red)
Image('test.jpg')
  // 使用變量或表達式作為參數(shù)
  .width(this.count % 2 === 0 ? 100 : 200)
  .height(this.offset)

配置事件

使用匿名函數(shù)表達式配置組件的事件方法,要求使用“() => {...}”,以確保函數(shù)與組件綁定,同時符合ArtTS語法規(guī)范。

Button('Click me')
  .onClick(() => {
    this.myText = 'ArkUI';
  })

使用組件的成員函數(shù)配置組件的事件方法。

myClickHandler(): void {
  this.myText = 'ArkUI';
}
...
Button('add counter')
  .onClick(this.myClickHandler)

配置子組件

容器組件均支持子組件配置,可以實現(xiàn)相對復(fù)雜的多級嵌套。
Column、Row、Stack、Grid、List等組件都是容器組件。

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

創(chuàng)建自定義組件

在ArkUI中,UI顯示的內(nèi)容均為組件,由框架直接提供的稱為系統(tǒng)組件,由開發(fā)者定義的稱為自定義組件。在進行 UI 界面開發(fā)時,通常不是簡單的將系統(tǒng)組件進行組合使用,而是需要考慮代碼可復(fù)用性、業(yè)務(wù)邏輯與UI分離,后續(xù)版本演進等因素。因此,將UI和部分業(yè)務(wù)邏輯封裝成自定義組件是不可或缺的能力。

自定義組件具有以下特點:

  • 可組合:允許開發(fā)者組合使用系統(tǒng)組件、及其屬性和方法。
  • 可重用:自定義組件可以被其他組件重用,并作為不同的實例在不同的父組件或容器中使用。
  • 數(shù)據(jù)驅(qū)動UI更新:通過狀態(tài)變量的改變,來驅(qū)動UI的刷新。

以下示例展示了自定義組件的基本用法。

@Component
struct HelloComponent {
  @State message: string = 'Hello, World!';

  build() {
    // HelloComponent自定義組件組合系統(tǒng)組件Row和Text
    Row() {
      Text(this.message)
        .onClick(() => {
          // 狀態(tài)變量message的改變驅(qū)動UI刷新,UI從'Hello, World!'刷新為'Hello, ArkUI!'
          this.message = 'Hello, ArkUI!';
        })
    }
  }
}

HelloComponent可以在其他自定義組件中的build()函數(shù)中多次創(chuàng)建,實現(xiàn)自定義組件的重用。

自定義組件的基本結(jié)構(gòu)
  • struct:自定義組件基于struct實現(xiàn),struct + 自定義組件名 + {...}的組合構(gòu)成自定義組件,不能有繼承關(guān)系。對于struct的實例化,可以省略new。
  • @Component:@Component裝飾器僅能裝飾struct關(guān)鍵字聲明的數(shù)據(jù)結(jié)構(gòu)。struct被@Component裝飾后具備組件化的能力,需要實現(xiàn)build方法描述UI,一個struct只能被一個@Component裝飾。
  • @Entry:@Entry裝飾的自定義組件將作為UI頁面的入口。在單個UI頁面中,最多可以使用@Entry裝飾一個自定義組件。@Entry可以接受一個可選的LocalStorage的參數(shù)。

自定義組件的參數(shù)規(guī)定

我們可以在build方法或者@Builder裝飾的函數(shù)里創(chuàng)建自定義組件,并傳入?yún)?shù)。

@Component
struct MyComponent {
  private countDownFrom: number = 0;
  private color: Color = Color.Blue;

  build() {
  }
}

@Entry
@Component
struct ParentComponent {
  private someColor: Color = Color.Pink;

  build() {
    Column() {
      // 創(chuàng)建MyComponent實例,并將創(chuàng)建MyComponent成員變量countDownFrom初始化為10,將成員變量color初始化為this.someColor
      MyComponent({ countDownFrom: 10, color: this.someColor })
    }
  }
}

build()函數(shù)

所有聲明在build()函數(shù)的語言,我們統(tǒng)稱為UI描述語言,UI描述語言需要遵循以下規(guī)則:

  • @Entry裝飾的自定義組件,其build()函數(shù)下的根節(jié)點唯一且必要,且必須為容器組件,其中ForEach禁止作為根節(jié)點。
    @Component裝飾的自定義組件,其build()函數(shù)下的根節(jié)點唯一且必要,可以為非容器組件,其中ForEach禁止作為根節(jié)點。
@Entry
@Component
struct MyComponent {
  build() {
    // 根節(jié)點唯一且必要,必須為容器組件
    Row() {
      ChildComponent()
    }
  }
}

@Component
struct ChildComponent {
  build() {
    // 根節(jié)點唯一且必要,可為非容器組件
    Image('test.jpg')
  }
}
  • 不允許聲明本地變量,反例如下。
build() {
  // 反例:不允許聲明本地變量
  let a: number = 1;
}
  • 不允許在UI描述里直接使用console.info,但允許在方法或者函數(shù)里使用,反例如下。
build() {
  // 反例:不允許console.info
  console.info('print debug log');
}
  • 不允許創(chuàng)建本地的作用域,反例如下。
build() {
  // 反例:不允許本地作用域
  {
    ...
  }
}
  • 不允許調(diào)用除了被@Builder裝飾以外的方法,允許系統(tǒng)組件的參數(shù)是TS方法的返回值。
@Component
struct ParentComponent {
  doSomeCalculations() {
  }

  calcTextValue(): string {
    return 'Hello World';
  }

  @Builder doSomeRender() {
    Text(`Hello World`)
  }

  build() {
    Column() {
      // 反例:不能調(diào)用沒有用@Builder裝飾的方法
      this.doSomeCalculations();
      // 正例:可以調(diào)用
      this.doSomeRender();
      // 正例:參數(shù)可以為調(diào)用TS方法的返回值
      Text(this.calcTextValue())
    }
  }
}
  • 不允許switch語法,如果需要使用條件判斷,請使用if。反例如下。
build() {
  Column() {
    // 反例:不允許使用switch語法
    switch (expression) {
      case 1:
        Text('...')
        break;
      case 2:
        Image('...')
        break;
      default:
        Text('...')
        break;
    }
  }
}
  • 不允許使用表達式,反例如下。
build() {
  Column() {
    // 反例:不允許使用表達式
    (this.aVar > 10) ? Text('...') : Image('...')
  }
}

自定義組件通用樣式

自定義組件通過“.”鏈式調(diào)用的形式設(shè)置通用樣式。

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

@Entry
@Component
struct MyComponent {
  build() {
    Row() {
      MyComponent2()
        .width(200)
        .height(300)
        .backgroundColor(Color.Red)
    }
  }
}

頁面和自定義組件生命周期

在開始之前,我們先明確自定義組件和頁面的關(guān)系:

  • 自定義組件:@Component裝飾的UI單元,可以組合多個系統(tǒng)組件實現(xiàn)UI的復(fù)用,可以調(diào)用組件的生命周期。
  • 頁面:即應(yīng)用的UI頁面??梢杂梢粋€或者多個自定義組件組成,@Entry裝飾的自定義組件為頁面的入口組件,即頁面的根節(jié)點,一個頁面有且僅能有一個@Entry。只有被@Entry裝飾的組件才可以調(diào)用頁面的生命周期。

頁面生命周期,即被@Entry裝飾的組件生命周期,提供以下生命周期接口:

  • onPageShow:頁面每次顯示時觸發(fā)一次,包括路由過程、應(yīng)用進入前臺等場景,僅@Entry裝飾的自定義組件生效。(相當于iOS中viewDidAppear)

  • onPageHide:頁面每次隱藏時觸發(fā)一次,包括路由過程、應(yīng)用進入后臺等場景,僅@Entry裝飾的自定義組件生效。(相當于iOS中viewDidDisappear)

  • onBackPress:當用戶點擊返回按鈕時觸發(fā),僅@Entry裝飾的自定義組件生效。

組件生命周期,即一般用@Component裝飾的自定義組件的生命周期,提供以下生命周期接口:

  • aboutToAppear:組件即將出現(xiàn)時回調(diào)該接口,具體時機為在創(chuàng)建自定義組件的新實例后,在執(zhí)行其build()函數(shù)之前執(zhí)行。(在iOS中類似super.init()之后)

  • aboutToDisappear:aboutToDisappear函數(shù)在自定義組件析構(gòu)銷毀之前執(zhí)行。不允許在aboutToDisappear函數(shù)中改變狀態(tài)變量,特別是@Link變量的修改可能會導(dǎo)致應(yīng)用程序行為不穩(wěn)定。(在iOS中類似deinit())

生命周期流程如下圖所示,下圖展示的是被@Entry裝飾的組件(首頁)生命周期。

根據(jù)上面的流程圖,我們從自定義組件的初始創(chuàng)建、重新渲染和刪除來詳細解釋。

自定義組件的創(chuàng)建和渲染流程

  1. 自定義組件的創(chuàng)建:自定義組件的實例由ArkUI框架創(chuàng)建。

  2. 初始化自定義組件的成員變量:通過本地默認值或者構(gòu)造方法傳遞參數(shù)來初始化自定義組件的成員變量,初始化順序為成員變量的定義順序。

  3. 如果開發(fā)者定義了aboutToAppear,則執(zhí)行aboutToAppear方法。

  4. 在首次渲染的時候,執(zhí)行build方法渲染系統(tǒng)組件,如果子組件為自定義組件,則創(chuàng)建自定義組件的實例。在執(zhí)行build()函數(shù)的過程中,框架會觀察每個狀態(tài)變量的讀取狀態(tài),將保存兩個map:

    a. 狀態(tài)變量 -> UI組件(包括ForEach和if)。
    b. UI組件 -> 此組件的更新函數(shù),即一個lambda方法,作為build()函數(shù)的子集,創(chuàng)建對應(yīng)的UI組件并執(zhí)行其屬性方法,示意如下。

build() {
  ...
  this.observeComponentCreation(() => {
    Button.create();
  })

  this.observeComponentCreation(() => {
    Text.create();
  })
  ...
}

當應(yīng)用在后臺啟動時,此時應(yīng)用進程并沒有銷毀,所以僅需要執(zhí)行onPageShow。

自定義組件的刪除

如果if組件的分支改變,或者ForEach循環(huán)渲染中數(shù)組的個數(shù)改變,組件將被刪除:

  1. 在刪除組件之前,將調(diào)用其aboutToDisappear生命周期函數(shù),標記著該節(jié)點將要被銷毀。ArkUI的節(jié)點刪除機制是:后端節(jié)點直接從組件樹上摘下,后端節(jié)點被銷毀,對前端節(jié)點解引用,前端節(jié)點已經(jīng)沒有引用時,將被JS虛擬機垃圾回收。

  2. 自定義組件和它的變量將被刪除,如果其有同步的變量,比如@Link、@Prop@StorageLink,將從同步源上取消注冊。

不建議在生命周期aboutToDisappear內(nèi)使用async await,如果在生命周期的aboutToDisappear使用異步操作(Promise或者回調(diào)方法),自定義組件將被保留在Promise的閉包中,直到回調(diào)方法被執(zhí)行完,這個行為阻止了自定義組件的垃圾回收。

以下示例展示了生命周期的調(diào)用時機:

// Index.ets
import router from '@ohos.router';

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

  // 只有被@Entry裝飾的組件才可以調(diào)用頁面的生命周期
  onPageShow() {
    console.info('Index onPageShow');
  }
  // 只有被@Entry裝飾的組件才可以調(diào)用頁面的生命周期
  onPageHide() {
    console.info('Index onPageHide');
  }

  // 只有被@Entry裝飾的組件才可以調(diào)用頁面的生命周期
  onBackPress() {
    console.info('Index onBackPress');
    this.btnColor ="#FFEE0606"
    return true // 返回true表示頁面自己處理返回邏輯,不進行頁面路由;返回false表示使用默認的路由返回邏輯,不設(shè)置返回值按照false處理
  }

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

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

  build() {
    Column() {
      // this.showChild為true,創(chuàng)建Child子組件,執(zhí)行Child aboutToAppear
      if (this.showChild) {
        Child()
      }
      // this.showChild為false,刪除Child子組件,執(zhí)行Child aboutToDisappear
      Button('delete Child')
      .margin(20)
      .backgroundColor(this.btnColor)
      .onClick(() => {
        this.showChild = false;
      })
      // push到page頁面,執(zhí)行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
@Entry
@Component
struct page {
  @State textColor: Color = Color.Black;
  @State num: number = 0

  onPageShow() {
    this.num = 5
  }

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

  onBackPress() { // 不設(shè)置返回值按照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,也是頁面的入口組件,即頁面的根節(jié)點;一個是Child,是MyComponent的子組件。只有@Entry裝飾的節(jié)點才可以使頁面級別的生命周期方法生效,所以MyComponent中聲明了當前Index頁面的頁面生命周期函數(shù)。MyComponent和其子組件Child也同時聲明了組件的生命周期函數(shù)。

  • 應(yīng)用冷啟動的初始化流程為:MyComponent aboutToAppear --> MyComponent build --> Child aboutToAppear --> Child build --> Child build執(zhí)行完畢 --> MyComponent build執(zhí)行完畢 --> Index onPageShow。
  • 點擊“delete Child”,if綁定的this.showChild變成false,刪除Child組件,會執(zhí)行Child aboutToDisappear方法。
  • 點擊“push to next page”,調(diào)用router.pushUrl接口,跳轉(zhuǎn)到另外一個頁面,當前Index頁面隱藏,執(zhí)行頁面生命周期Index onPageHide。此處調(diào)用的是router.pushUrl接口,Index頁面被隱藏,并沒有銷毀,所以只調(diào)用onPageHide。跳轉(zhuǎn)到新頁面后,執(zhí)行初始化新頁面的生命周期的流程。
  • 如果調(diào)用的是router.replaceUrl,則當前Index頁面被銷毀,執(zhí)行的生命周期流程將變?yōu)椋篒ndex onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已經(jīng)提到,組件的銷毀是從組件樹上直接摘下子樹,所以先調(diào)用父組件的aboutToDisappear,再調(diào)用子組件的aboutToDisappear,然后執(zhí)行初始化新頁面的生命周期流程。
  • 點擊返回按鈕,觸發(fā)頁面生命周期Index onBackPress,且觸發(fā)返回一個頁面后會導(dǎo)致當前Index頁面被銷毀。
  • 最小化應(yīng)用或者應(yīng)用進入后臺,觸發(fā)Index onPageHide。當前Index頁面沒有被銷毀,所以并不會執(zhí)行組件的aboutToDisappear。應(yīng)用回到前臺,執(zhí)行Index onPageShow。
  • 退出應(yīng)用,執(zhí)行Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。

裝飾器

@Builder裝飾器:自定義構(gòu)建函數(shù)

自定義組件內(nèi)部UI結(jié)構(gòu)固定,僅與使用方進行數(shù)據(jù)傳遞。ArkUI還提供了一種更輕量的UI元素復(fù)用機制@Builder,@Builder所裝飾的函數(shù)遵循build()函數(shù)語法規(guī)則,開發(fā)者可以將重復(fù)使用的UI元素抽象成一個方法,在build方法里調(diào)用。
為了簡化語言,我們將@Builder裝飾的函數(shù)也稱為“自定義構(gòu)建函數(shù)”。

裝飾器使用說明

自定義組件內(nèi)自定義構(gòu)建函數(shù)

定義的語法:

@Builder MyBuilderFunction() { ... }

使用方法:

this.MyBuilderFunction()
  • 允許在自定義組件內(nèi)定義一個或多個@Builder方法,該方法被認為是該組件的私有、特殊類型的成員函數(shù)。
  • 自定義構(gòu)建函數(shù)可以在所屬組件的build方法和其他自定義構(gòu)建函數(shù)中調(diào)用,但不允許在組件外調(diào)用。
  • 在自定義函數(shù)體中,this指代當前所屬組件,組件的狀態(tài)變量可以在自定義構(gòu)建函數(shù)內(nèi)訪問。建議通過this訪問自定義組件的狀態(tài)變量而不是參數(shù)傳遞。
全局自定義構(gòu)建函數(shù)

定義的語法:

@Builder function MyGlobalBuilderFunction() { ... }

使用方法:

 MyGlobalBuilderFunction()
  • 全局的自定義構(gòu)建函數(shù)可以被整個應(yīng)用獲取,不允許使用this和bind方法。

  • 如果不涉及組件狀態(tài)變化,建議使用全局的自定義構(gòu)建方法。

參數(shù)傳遞規(guī)則

自定義構(gòu)建函數(shù)的參數(shù)傳遞有按值傳遞按引用傳遞兩種,均需遵守以下規(guī)則:

  • 參數(shù)的類型必須與參數(shù)聲明的類型一致,不允許undefined、null和返回undefined、null的表達式。

  • 在@Builder修飾的函數(shù)內(nèi)部,不允許改變參數(shù)值。

  • @Builder內(nèi)UI語法遵循UI語法規(guī)則。

  • 只有傳入一個參數(shù),且參數(shù)需要直接傳入對象字面量才會按引用傳遞該參數(shù),其余傳遞方式均為按值傳遞。

按引用傳遞參數(shù)

按引用傳遞參數(shù)時,傳遞的參數(shù)可為狀態(tài)變量,且狀態(tài)變量的改變會引起@Builder方法內(nèi)的UI刷新。ArkUI提供$$作為按引用傳遞參數(shù)的范式。

class ABuilderParam {
  paramA1: string = ''
  paramB1: string = ''
}
@Builder function ABuilder($$ : ABuilderParam) {...}
按值傳遞參數(shù)

調(diào)用@Builder裝飾的函數(shù)默認按值傳遞。當傳遞的參數(shù)為狀態(tài)變量時,狀態(tài)變量的改變不會引起@Builder方法內(nèi)的UI刷新。所以當使用狀態(tài)變量的時候,推薦使用按引用傳遞。

不同參數(shù)傳遞方式的示例
class ABuilderParam {
  paramA1: string = ''
}

// 使用$$作為按引用傳遞參數(shù)的范式。當傳遞的參數(shù)為狀態(tài)變量,且狀態(tài)變量的改變會引起@Builder方法內(nèi)的UI刷新
@Builder function BuilderByReference($$: ABuilderParam) {
  Row() {
    Text(`UseStateVarByReference: ${$$.paramA1} `)
  }
  .backgroundColor(Color.Red)
}

// 默認按值傳遞。當傳遞的參數(shù)為狀態(tài)變量時,狀態(tài)變量的改變不會引起@Builder方法內(nèi)的UI刷新
@Builder function BuilderByValue(paramA1: string) {
  Row() {
    Text(`UseStateVarByValue: ${paramA1} `)
  }
  .backgroundColor(Color.Green)
}

@Entry
@Component
struct Parent {
  @State message: string = 'Hello World';

  build() {
    Row() {
      Column({ space: 10 }) {
        // 將this.message引用傳遞給BuilderByReference,message的改變會引起UI變化
        BuilderByReference({ paramA1: this.message })
        // 將this.message按值傳遞給BuilderByValue,message的改變不會引起UI變化
        BuilderByValue(this.message)
        Button('Click me').onClick(() => {
          // 點擊“Click me”后,UI從“Hello World”刷新為“ArkUI”
          this.message = 'ArkUI';
        })
      }
      .width('100%')
    }
    .height('100%')
  }
}
狀態(tài)變量改變時,按引用傳遞參數(shù)的@Builder方法內(nèi)的UI才會刷新

@BuilderParam裝飾器:引用@Builder函數(shù)

當開發(fā)者創(chuàng)建了自定義組件,并想對該組件添加特定功能時,例如在自定義組件中添加一個點擊跳轉(zhuǎn)操作。若直接在組件內(nèi)嵌入事件方法,將會導(dǎo)致所有引入該自定義組件的地方均增加了該功能。為解決此問題,ArkUI引入了@BuilderParam裝飾器,@BuilderParam用來裝飾指向@Builder方法的變量,開發(fā)者可在初始化自定義組件時對此屬性進行賦值,為自定義組件增加特定的功能。該裝飾器用于聲明任意UI描述的一個元素,類似slot占位符。

初始化@BuilderParam裝飾的方法

@BuilderParam裝飾的方法只能被自定義構(gòu)建函數(shù)(@Builder裝飾的方法)初始化

  • 使用所屬自定義組件的自定義構(gòu)建函數(shù)或者全局的自定義構(gòu)建函數(shù),在本地初始化@BuilderParam。
@Builder function GlobalBuilder0() {}

@Component
struct Child {
  @Builder doNothingBuilder() {};

  @BuilderParam aBuilder0: () => void = this.doNothingBuilder;
  @BuilderParam aBuilder1: () => void = GlobalBuilder0;
  build(){}
}
  • 用父組件自定義構(gòu)建函數(shù)初始化子組件@BuilderParam裝飾的方法。
@Component
struct Child {
  @Builder FunABuilder0() {}
  @BuilderParam aBuilder0: () => void = this.FunABuilder0;

  build() {
    Column() {
      this.aBuilder0()
    }
  }
}

@Entry
@Component
struct Parent {
  @Builder componentBuilder() {
    Text(`Parent builder `)
  }

  build() {
    Column() {
      Child({ aBuilder0: this.componentBuilder })
    }
  }
}
  • 需注意this指向正確。
    以下示例中,Parent組件在調(diào)用this.componentBuilder()時,this指向其所屬組件,即“Parent”。@Builder componentBuilder()傳給子組件@BuilderParam aBuilder0,在Child組件中調(diào)用this.aBuilder0()時,this指向在Child的label,即“Child”。對于@BuilderParam aBuilder1,在將this.componentBuilder傳給aBuilder1時,調(diào)用bind綁定了this,因此其this.label指向Parent的label。

說明
開發(fā)者謹慎使用bind改變函數(shù)調(diào)用的上下文,可能會使this指向混亂。

@Component
struct Child {
  label: string = `Child`
  @Builder FunABuilder0() {}
  @Builder FunABuilder1() {}
  @BuilderParam aBuilder0: () => void = this.FunABuilder0;
  @BuilderParam aBuilder1: () => void = this.FunABuilder1;

  build() {
    Column() {
      this.aBuilder0()
      this.aBuilder1()
    }
  }
}

@Entry
@Component
struct Parent {
  label: string = `Parent`

  @Builder componentBuilder() {
    Text(`${this.label}`)
  }

  build() {
    Column() {
      this.componentBuilder()
      Child({ aBuilder0: this.componentBuilder, aBuilder1: ():void=>{this.componentBuilder()} })
    }
  }
}

使用場景

@BuilderParam裝飾的方法可以是有參數(shù)和無參數(shù)的兩種形式,需與指向的@Builder方法類型匹配。@BuilderParam裝飾的方法類型需要和@Builder方法類型一致。

class Tmp{
  label:string = ''
}
@Builder function GlobalBuilder1($$ : Tmp) {
  Text($$.label)
    .width(400)
    .height(50)
    .backgroundColor(Color.Green)
}

@Component
struct Child {
  label: string = 'Child'
  @Builder FunABuilder0() {}
  // 無參數(shù)類,指向的componentBuilder也是無參數(shù)類型
  @BuilderParam aBuilder0: () => void = this.FunABuilder0;
  // 有參數(shù)類型,指向的GlobalBuilder1也是有參數(shù)類型的方法
  @BuilderParam aBuilder1: ($$ : Tmp) => void = GlobalBuilder1;

  build() {
    Column() {
      this.aBuilder0()
      this.aBuilder1({label: 'global Builder label' } )
    }
  }
}

@Entry
@Component
struct Parent {
  label: string = 'Parent'

  @Builder componentBuilder() {
    Text(`${this.label}`)
  }

  build() {
    Column() {
      this.componentBuilder()
      Child({ aBuilder0: this.componentBuilder, aBuilder1: GlobalBuilder1 })
    }
  }
}

尾隨閉包初始化組件

在自定義組件中使用@BuilderParam裝飾的屬性時也可通過尾隨閉包進行初始化。在初始化自定義組件時,組件后緊跟一個大括號“{}”形成尾隨閉包場景。

說明:

  • 此場景下自定義組件內(nèi)有且僅有一個使用@BuilderParam裝飾的屬性。
  • 此場景下自定義組件不支持使用通用屬性。


    多個@BuilderParam裝飾的屬性,編譯報錯

開發(fā)者可以將尾隨閉包內(nèi)的內(nèi)容看做@Builder裝飾的函數(shù)傳給@BuilderParam。示例如下:

// xxx.ets
@Component
struct CustomContainer {
  @Prop header: string = '';
  @Builder CloserFun(){}
  @BuilderParam closer: () => void = this.CloserFun

  build() {
    Column() {
      Text(this.header)
        .fontSize(30)
      this.closer()
    }
  }
}

@Builder function specificParam(label1: string, label2: string) {
  Column() {
    Text(label1)
      .fontSize(30)
    Text(label2)
      .fontSize(30)
  }
}

@Entry
@Component
struct CustomContainerUser {
  @State text: string = 'header';

  build() {
    Column() {
      // 創(chuàng)建CustomContainer,在創(chuàng)建CustomContainer時,通過其后緊跟一個大括號“{}”形成尾隨閉包
      // 作為傳遞給子組件CustomContainer @BuilderParam closer: () => void的參數(shù)
      CustomContainer({ header: this.text }) {
        Column() {
          specificParam('testA', 'testB')
        }.backgroundColor(Color.Yellow)
        .onClick(() => {
          this.text = 'changeHeader';
        })
      }
    }
  }
}

@Styles裝飾器:定義組件重用樣式

如果每個組件的樣式都需要單獨設(shè)置,在開發(fā)過程中會出現(xiàn)大量代碼在進行重復(fù)樣式設(shè)置,雖然可以復(fù)制粘貼,但為了代碼簡潔性和后續(xù)方便維護,我們推出了可以提煉公共樣式進行復(fù)用的裝飾器@Styles。

@Styles裝飾器可以將多條樣式設(shè)置提煉成一個方法,直接在組件聲明的位置調(diào)用。通過@Styles裝飾器可以快速定義并復(fù)用自定義樣式。用于快速定義并復(fù)用自定義樣式。

裝飾器使用說明

@Styles方法不支持參數(shù),反例如下。

// 反例: @Styles不支持參數(shù)
@Styles function globalFancy (value: number) {
  .width(value)
}
  • @Styles可以定義在組件內(nèi)或全局,在全局定義時需在方法名前面添加function關(guān)鍵字,組件內(nèi)定義時則不需要添加function關(guān)鍵字。
// 全局
@Styles function functionName() { ... }

// 在組件內(nèi)
@Component
struct FancyUse {
  @Styles fancy() {
    .height(100)
  }
}
  • 定義在組件內(nèi)的@Styles可以通過this訪問組件的常量和狀態(tài)變量,并可以在@Styles里通過事件來改變狀態(tài)變量的值,示例如下:
@Component
struct FancyUse {
  @State heightValue: number = 100
  @Styles fancy() {
    .height(this.heightValue)
    .backgroundColor(Color.Yellow)
    .onClick(() => {
      this.heightValue = 200
    })
  }
}
  • 組件內(nèi)@Styles的優(yōu)先級高于全局@Styles。
    框架優(yōu)先找當前組件內(nèi)的@Styles,如果找不到,則會全局查找。
使用場景

以下示例中演示了組件內(nèi)@Styles和全局@Styles的用法。

// 定義在全局的@Styles封裝的樣式
@Styles function globalFancy  () {
  .width(150)
  .height(100)
  .backgroundColor(Color.Pink)
}

@Entry
@Component
struct FancyUse {
  @State heightValue: number = 100
  // 定義在組件內(nèi)的@Styles封裝的樣式
  @Styles fancy() {
    .width(200)
    .height(this.heightValue)
    .backgroundColor(Color.Yellow)
    .onClick(() => {
      this.heightValue = 200
    })
  }

  build() {
    Column({ space: 10 }) {
      // 使用全局的@Styles封裝的樣式
      Text('FancyA')
        .globalFancy ()
        .fontSize(30)
      // 使用組件內(nèi)的@Styles封裝的樣式
      Text('FancyB')
        .fancy()
        .fontSize(30)
    }
  }
}

@Extend裝飾器:定義擴展組件樣式

在前文的示例中,可以使用@Styles用于樣式的擴展,在@Styles的基礎(chǔ)上,我們提供了@Extend,用于擴展原生組件樣式。

裝飾器使用說明
  • 語法
@Extend(UIComponentName) function functionName { ... }
  • 和@Styles不同,@Extend僅支持在全局定義,不支持在組件內(nèi)部定義。
  • 和@Styles不同,@Extend支持封裝指定的組件的私有屬性和私有事件,以及預(yù)定義相同組件的@Extend的方法。
// @Extend(Text)可以支持Text的私有屬性fontColor
@Extend(Text) function fancy () {
  .fontColor(Color.Red)
}
// superFancyText可以調(diào)用預(yù)定義的fancy
@Extend(Text) function superFancyText(size:number) {
    .fontSize(size)
    .fancy()
}
  • 和@Styles不同,@Extend裝飾的方法支持參數(shù),開發(fā)者可以在調(diào)用時傳遞參數(shù),調(diào)用遵循TS方法傳值調(diào)用。
// xxx.ets
@Extend(Text) function fancy (fontSize: number) {
  .fontColor(Color.Red)
  .fontSize(fontSize)
}

@Entry
@Component
struct FancyUse {
  build() {
    Row({ space: 10 }) {
      Text('Fancy')
        .fancy(16)
      Text('Fancy')
        .fancy(24)
    }
  }
}
  • @Extend裝飾的方法的參數(shù)可以為function,作為Event事件的句柄。
@Extend(Text) function makeMeClick(onClick: () => void) {
  .backgroundColor(Color.Blue)
  .onClick(onClick)
}

@Entry
@Component
struct FancyUse {
  @State label: string = 'Hello World';

  onClickHandler() {
    this.label = 'Hello ArkUI';
  }

  build() {
    Row({ space: 10 }) {
      Text(`${this.label}`)
        .makeMeClick(() => {this.onClickHandler()})
    }
  }
}
  • @Extend的參數(shù)可以為狀態(tài)變量,當狀態(tài)變量改變時,UI可以正常的被刷新渲染。
@Extend(Text) function fancy (fontSize: number) {
  .fontColor(Color.Red)
  .fontSize(fontSize)
}

@Entry
@Component
struct FancyUse {
  @State fontSizeValue: number = 20
  build() {
    Row({ space: 10 }) {
      Text('Fancy')
        .fancy(this.fontSizeValue)
        .onClick(() => {
          this.fontSizeValue = 30
        })
    }
  }
}
使用場景

以下示例聲明了3個Text組件,每個Text組件均設(shè)置了fontStyle、fontWeight和backgroundColor樣式。

@Entry
@Component
struct FancyUse {
  @State label: string = 'Hello World'

  build() {
    Row({ space: 10 }) {
      Text(`${this.label}`)
        .fontStyle(FontStyle.Italic)
        .fontWeight(100)
        .backgroundColor(Color.Blue)
      Text(`${this.label}`)
        .fontStyle(FontStyle.Italic)
        .fontWeight(200)
        .backgroundColor(Color.Pink)
      Text(`${this.label}`)
        .fontStyle(FontStyle.Italic)
        .fontWeight(300)
        .backgroundColor(Color.Orange)
    }.margin('20%')
  }
}

@Extend將樣式組合復(fù)用,示例如下。

@Extend(Text) function fancyText(weightValue: number, color: Color) {
  .fontStyle(FontStyle.Italic)
  .fontWeight(weightValue)
  .backgroundColor(color)
}

通過@Extend組合樣式后,使得代碼更加簡潔,增強可讀性。

@Entry
@Component
struct FancyUse {
  @State label: string = 'Hello World'

  build() {
    Row({ space: 10 }) {
      Text(`${this.label}`)
        .fancyText(100, Color.Blue)
      Text(`${this.label}`)
        .fancyText(200, Color.Pink)
      Text(`${this.label}`)
        .fancyText(300, Color.Orange)
    }.margin('20%')
  }
}

stateStyles:多態(tài)樣式

@Styles和@Extend僅僅應(yīng)用于靜態(tài)頁面的樣式復(fù)用,stateStyles可以依據(jù)組件的內(nèi)部狀態(tài)的不同,快速設(shè)置不同樣式。這就是我們本章要介紹的內(nèi)容stateStyles(又稱為:多態(tài)樣式)。

概述

stateStyles是屬性方法,可以根據(jù)UI內(nèi)部狀態(tài)來設(shè)置樣式,類似于css偽類,但語法不同。ArkUI提供以下五種狀態(tài):
focused:獲焦態(tài)。
normal:正常態(tài)。
pressed:按壓態(tài)。
disabled:不可用態(tài)。
selected10+:選中態(tài)。

基礎(chǔ)場景

下面的示例展示了stateStyles最基本的使用場景。Button1處于第一個組件,Button2處于第二個組件。按壓時顯示為pressed態(tài)指定的黑色。使用Tab鍵走焦,先是Button1獲焦并顯示為focus態(tài)指定的粉色。當Button2獲焦的時候,Button2顯示為focus態(tài)指定的粉色,Button1失焦顯示normal態(tài)指定的紅色。

@Entry
@Component
struct StateStylesSample {
  build() {
    Column() {
      Button('Button1')
        .stateStyles({
          focused: {
            .backgroundColor(Color.Pink)
          },
          pressed: {
            .backgroundColor(Color.Black)
          },
          normal: {
            .backgroundColor(Color.Red)
          }
        })
        .margin(20)
      Button('Button2')
        .stateStyles({
          focused: {
            .backgroundColor(Color.Pink)
          },
          pressed: {
            .backgroundColor(Color.Black)
          },
          normal: {
            .backgroundColor(Color.Red)
          }
        })
    }.margin('30%')
  }
}
圖1 獲焦態(tài)和按壓態(tài)
@Styles和stateStyles聯(lián)合使用

以下示例通過@Styles指定stateStyles的不同狀態(tài)。

@Entry
@Component
struct MyComponent {
  @Styles normalStyle() {
    .backgroundColor(Color.Gray)
  }

  @Styles pressedStyle() {
    .backgroundColor(Color.Red)
  }

  build() {
    Column() {
      Text('Text1')
        .fontSize(50)
        .fontColor(Color.White)
        .stateStyles({
          normal: this.normalStyle,
          pressed: this.pressedStyle,
        })
    }
  }
}
圖2 正常態(tài)和按壓態(tài)
在stateStyles里使用常規(guī)變量和狀態(tài)變量

stateStyles可以通過this綁定組件內(nèi)的常規(guī)變量和狀態(tài)變量。

@Entry
@Component
struct CompWithInlineStateStyles {
  @State focusedColor: Color = Color.Red;
  normalColor: Color = Color.Green

  build() {
    Column() {
      Button('clickMe').height(100).width(100)
        .stateStyles({
          normal: {
            .backgroundColor(this.normalColor)
          },
          focused: {
            .backgroundColor(this.focusedColor)
          }
        })
        .onClick(() => {
          this.focusedColor = Color.Pink
        })
        .margin('30%')
    }
  }
}

Button默認normal態(tài)顯示綠色,第一次按下Tab鍵讓Button獲焦顯示為focus態(tài)的紅色,點擊事件觸發(fā)后,再次按下Tab鍵讓Button獲焦,focus態(tài)變?yōu)榉凵?/p>

圖3 點擊改變獲焦態(tài)樣式

@AnimatableExtend裝飾器:定義可動畫屬性

@AnimatableExtend裝飾器用于自定義可動畫的屬性方法,在這個屬性方法中修改組件不可動畫的屬性。在動畫執(zhí)行過程時,通過逐幀回調(diào)函數(shù)修改不可動畫屬性值,讓不可動畫屬性也能實現(xiàn)動畫效果。

  • 可動畫屬性:如果一個屬性方法在animation屬性前調(diào)用,改變這個屬性的值可以生效animation屬性的動畫效果,這個屬性稱為可動畫屬性。比如height、width、backgroundColor、translate屬性,Text組件的fontSize屬性等。

  • 不可動畫屬性:如果一個屬性方法在animation屬性前調(diào)用,改變這個屬性的值不能生效animation屬性的動畫效果,這個屬性稱為不可動畫屬性。比如Ployline組件的points屬性等。

語法
@AnimatableExtend(UIComponentName) function functionName(value: typeName) { 
  .propertyName(value)
}
  • @AnimatableExtend僅支持定義在全局,不支持在組件內(nèi)部定義。
  • @AnimatableExtend定義的函數(shù)參數(shù)類型必須為number類型或者實現(xiàn) AnimtableArithmetic<T>接口的自定義類型。
  • @AnimatableExtend定義的函數(shù)體內(nèi)只能調(diào)用@AnimatableExtend括號內(nèi)組件的屬性方法。
AnimtableArithmetic<T>接口說明

對復(fù)雜數(shù)據(jù)類型做動畫,需要實現(xiàn)AnimtableArithmetic<T>接口中加法、減法、乘法和判斷相等函數(shù)。

名稱 入?yún)㈩愋?/th> 返回值類型 說明
plus AnimtableArithmetic<T> AnimtableArithmetic<T> 加法函數(shù)
subtract AnimtableArithmetic<T> AnimtableArithmetic<T> 減法函數(shù)
multiply number AnimtableArithmetic<T> 乘法函數(shù)
equals AnimtableArithmetic<T> boolean 相等判斷函數(shù)
使用場景

以下示例實現(xiàn)字體大小的動畫效果。

@AnimatableExtend(Text) function animatableFontSize(size: number) {
  .fontSize(size)
}

@Entry
@Component
struct AnimatablePropertyExample {
  @State fontSize: number = 20
  build() {
    Column() {
      Text("AnimatableProperty")
        .animatableFontSize(this.fontSize)
        .animation({duration: 1000, curve: "ease"})
      Button("Play")
        .onClick(() => {
          this.fontSize = this.fontSize == 20 ? 36 : 20
        })
    }.width("100%")
    .padding(10)
  }
}

以下示例實現(xiàn)折線的動畫效果。

class Point {
  x: number
  y: number

  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }
  plus(rhs: Point): Point {
    return new Point(this.x + rhs.x, this.y + rhs.y)
  }
  subtract(rhs: Point): Point {
    return new Point(this.x - rhs.x, this.y - rhs.y)
  }
  multiply(scale: number): Point {
    return new Point(this.x * scale, this.y * scale)
  }
  equals(rhs: Point): boolean {
    return this.x === rhs.x && this.y === rhs.y
  }
}

class PointVector extends Array<Point> implements AnimatableArithmetic<PointVector> {
  constructor(value: Array<Point>) {
    super();
    value.forEach(p => this.push(p))
  }
  plus(rhs: PointVector): PointVector {
    let result = new PointVector([])
    const len = Math.min(this.length, rhs.length)
    for (let i = 0; i < len; i++) {
      result.push((this as Array<Point>)[i].plus((rhs as Array<Point>)[I]))
    }
    return result
  }
  subtract(rhs: PointVector): PointVector {
    let result = new PointVector([])
    const len = Math.min(this.length, rhs.length)
    for (let i = 0; i < len; i++) {
      result.push((this as Array<Point>)[i].subtract((rhs as Array<Point>)[I]))
    }
    return result
  }
  multiply(scale: number): PointVector {
    let result = new PointVector([])
    for (let i = 0; i < this.length; i++) {
      result.push((this as Array<Point>)[i].multiply(scale))
    }
    return result
  }
  equals(rhs: PointVector): boolean {
    if (this.length != rhs.length) {
      return false
    }
    for (let i = 0; i < this.length; i++) {
      if (!(this as Array<Point>)[i].equals((rhs as Array<Point>)[i])) {
        return false
      }
    }
    return true
  }
  get(): Array<Object[]> {
    let result: Array<Object[]> = []
    this.forEach(p => result.push([p.x, p.y]))
    return result
  }
}

@AnimatableExtend(Polyline) function animatablePoints(points: PointVector) {
  .points(points.get())
}

@Entry
@Component
struct AnimatablePropertyExample {
  @State points: PointVector = new PointVector([
    new Point(50, Math.random() * 200),
    new Point(100, Math.random() * 200),
    new Point(150, Math.random() * 200),
    new Point(200, Math.random() * 200),
    new Point(250, Math.random() * 200),
  ])
  build() {
    Column() {
      Polyline()
        .animatablePoints(this.points)
        .animation({duration: 1000, curve: "ease"})
        .size({height:220, width:300})
        .fill(Color.Green)
        .stroke(Color.Red)
        .backgroundColor('#eeaacc')
      Button("Play")
        .onClick(() => {
          this.points = new PointVector([
            new Point(50, Math.random() * 200),
            new Point(100, Math.random() * 200),
            new Point(150, Math.random() * 200),
            new Point(200, Math.random() * 200),
            new Point(250, Math.random() * 200),
          ])
        })
    }.width("100%")
    .padding(10)
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容