HarmonyOS UI范式-基本語法

參考文檔

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

目錄

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

基本語法概述

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

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

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

  • UI描述:以聲明式的方式來描述UI的結構殿怜,例如build()方法中的代碼塊。

  • 自定義組件:可復用的UI單元曙砂,可組合其他組件头谜,如上述被@Component裝飾的struct Hello。

  • 系統(tǒng)組件:ArkUI框架中默認內置的基礎和容器組件鸠澈,可直接被開發(fā)者調用柱告,比如示例中的Column、Text笑陈、Divider际度、Button。

  • 屬性方法:組件可以通過鏈式調用配置多項屬性涵妥,如fontSize()乖菱、width()、height()蓬网、backgroundColor()等窒所。

  • 事件方法:組件可以通過鏈式調用設置多個事件的響應邏輯,如跟隨在Button后面的onClick()帆锋。

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

聲明式UI描述

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

創(chuàng)建組件

根據組件構造方法的不同盔憨,創(chuàng)建組件包含有參數和無參數兩種方式。

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

配置屬性

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

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

配置事件

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

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

使用組件的成員函數配置組件的事件方法如叼。

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

配置子組件

容器組件均支持子組件配置,可以實現(xiàn)相對復雜的多級嵌套穷劈。
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顯示的內容均為組件追葡,由框架直接提供的稱為系統(tǒng)組件,由開發(fā)者定義的稱為自定義組件奕短。在進行 UI 界面開發(fā)時宜肉,通常不是簡單的將系統(tǒng)組件進行組合使用,而是需要考慮代碼可復用性篡诽、業(yè)務邏輯與UI分離崖飘,后續(xù)版本演進等因素。因此杈女,將UI和部分業(yè)務邏輯封裝成自定義組件是不可或缺的能力。

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

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

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

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

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

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

自定義組件的基本結構
  • struct:自定義組件基于struct實現(xiàn)栓辜,struct + 自定義組件名 + {...}的組合構成自定義組件恋拍,不能有繼承關系。對于struct的實例化藕甩,可以省略new施敢。
  • @Component:@Component裝飾器僅能裝飾struct關鍵字聲明的數據結構。struct被@Component裝飾后具備組件化的能力狭莱,需要實現(xiàn)build方法描述UI僵娃,一個struct只能被一個@Component裝飾。
  • @Entry:@Entry裝飾的自定義組件將作為UI頁面的入口腋妙。在單個UI頁面中默怨,最多可以使用@Entry裝飾一個自定義組件。@Entry可以接受一個可選的LocalStorage的參數骤素。

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

我們可以在build方法或者@Builder裝飾的函數里創(chuàng)建自定義組件匙睹,并傳入參數。

@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()函數

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

  • @Entry裝飾的自定義組件谆棺,其build()函數下的根節(jié)點唯一且必要,且必須為容器組件罕袋,其中ForEach禁止作為根節(jié)點改淑。
    @Component裝飾的自定義組件,其build()函數下的根節(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饥侵,但允許在方法或者函數里使用,反例如下衣屏。
build() {
  // 反例:不允許console.info
  console.info('print debug log');
}
  • 不允許創(chuàng)建本地的作用域躏升,反例如下。
build() {
  // 反例:不允許本地作用域
  {
    ...
  }
}
  • 不允許調用除了被@Builder裝飾以外的方法狼忱,允許系統(tǒng)組件的參數是TS方法的返回值膨疏。
@Component
struct ParentComponent {
  doSomeCalculations() {
  }

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

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

  build() {
    Column() {
      // 反例:不能調用沒有用@Builder裝飾的方法
      this.doSomeCalculations();
      // 正例:可以調用
      this.doSomeRender();
      // 正例:參數可以為調用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('...')
  }
}

自定義組件通用樣式

自定義組件通過“.”鏈式調用的形式設置通用樣式砚偶。

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

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

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

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

  • 自定義組件:@Component裝飾的UI單元洒闸,可以組合多個系統(tǒng)組件實現(xiàn)UI的復用染坯,可以調用組件的生命周期。
  • 頁面:即應用的UI頁面丘逸〉ヂ梗可以由一個或者多個自定義組件組成,@Entry裝飾的自定義組件為頁面的入口組件深纲,即頁面的根節(jié)點仲锄,一個頁面有且僅能有一個@Entry。只有被@Entry裝飾的組件才可以調用頁面的生命周期湃鹊。

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

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

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

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

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

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

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

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

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

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

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

  2. 初始化自定義組件的成員變量:通過本地默認值或者構造方法傳遞參數來初始化自定義組件的成員變量春叫,初始化順序為成員變量的定義順序肩钠。

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

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

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

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

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

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

自定義組件的刪除

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

  1. 在刪除組件之前黍判,將調用其aboutToDisappear生命周期函數豫尽,標記著該節(jié)點將要被銷毀。ArkUI的節(jié)點刪除機制是:后端節(jié)點直接從組件樹上摘下顷帖,后端節(jié)點被銷毀美旧,對前端節(jié)點解引用,前端節(jié)點已經沒有引用時贬墩,將被JS虛擬機垃圾回收榴嗅。

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

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

以下示例展示了生命周期的調用時機:

// 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舅巷,創(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() { // 不設置返回值按照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頁面的頁面生命周期函數椅文。MyComponent和其子組件Child也同時聲明了組件的生命周期函數。

  • 應用冷啟動的初始化流程為: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”,調用router.pushUrl接口锨亏,跳轉到另外一個頁面痴怨,當前Index頁面隱藏,執(zhí)行頁面生命周期Index onPageHide器予。此處調用的是router.pushUrl接口浪藻,Index頁面被隱藏,并沒有銷毀乾翔,所以只調用onPageHide爱葵。跳轉到新頁面后,執(zhí)行初始化新頁面的生命周期的流程。
  • 如果調用的是router.replaceUrl钧惧,則當前Index頁面被銷毀暇韧,執(zhí)行的生命周期流程將變?yōu)椋篒ndex onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已經提到浓瞪,組件的銷毀是從組件樹上直接摘下子樹,所以先調用父組件的aboutToDisappear巧婶,再調用子組件的aboutToDisappear乾颁,然后執(zhí)行初始化新頁面的生命周期流程。
  • 點擊返回按鈕艺栈,觸發(fā)頁面生命周期Index onBackPress英岭,且觸發(fā)返回一個頁面后會導致當前Index頁面被銷毀。
  • 最小化應用或者應用進入后臺湿右,觸發(fā)Index onPageHide诅妹。當前Index頁面沒有被銷毀,所以并不會執(zhí)行組件的aboutToDisappear毅人。應用回到前臺吭狡,執(zhí)行Index onPageShow。
  • 退出應用丈莺,執(zhí)行Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear划煮。

裝飾器

@Builder裝飾器:自定義構建函數

自定義組件內部UI結構固定,僅與使用方進行數據傳遞缔俄。ArkUI還提供了一種更輕量的UI元素復用機制@Builder弛秋,@Builder所裝飾的函數遵循build()函數語法規(guī)則,開發(fā)者可以將重復使用的UI元素抽象成一個方法俐载,在build方法里調用蟹略。
為了簡化語言,我們將@Builder裝飾的函數也稱為“自定義構建函數”遏佣。

裝飾器使用說明

自定義組件內自定義構建函數

定義的語法:

@Builder MyBuilderFunction() { ... }

使用方法:

this.MyBuilderFunction()
  • 允許在自定義組件內定義一個或多個@Builder方法挖炬,該方法被認為是該組件的私有、特殊類型的成員函數贼急。
  • 自定義構建函數可以在所屬組件的build方法和其他自定義構建函數中調用茅茂,但不允許在組件外調用。
  • 在自定義函數體中太抓,this指代當前所屬組件空闲,組件的狀態(tài)變量可以在自定義構建函數內訪問。建議通過this訪問自定義組件的狀態(tài)變量而不是參數傳遞走敌。
全局自定義構建函數

定義的語法:

@Builder function MyGlobalBuilderFunction() { ... }

使用方法:

 MyGlobalBuilderFunction()
  • 全局的自定義構建函數可以被整個應用獲取碴倾,不允許使用this和bind方法。

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

參數傳遞規(guī)則

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

  • 參數的類型必須與參數聲明的類型一致,不允許undefined僧须、null和返回undefined纲刀、null的表達式。

  • 在@Builder修飾的函數內部担平,不允許改變參數值示绊。

  • @Builder內UI語法遵循UI語法規(guī)則

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

按引用傳遞參數

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

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

調用@Builder裝飾的函數默認按值傳遞匪傍。當傳遞的參數為狀態(tài)變量時,狀態(tài)變量的改變不會引起@Builder方法內的UI刷新循榆。所以當使用狀態(tài)變量的時候析恢,推薦使用按引用傳遞

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

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

// 默認按值傳遞。當傳遞的參數為狀態(tài)變量時盗尸,狀態(tài)變量的改變不會引起@Builder方法內的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)變量改變時鞍时,按引用傳遞參數的@Builder方法內的UI才會刷新

@BuilderParam裝飾器:引用@Builder函數

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

初始化@BuilderParam裝飾的方法

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

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

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

  @BuilderParam aBuilder0: () => void = this.doNothingBuilder;
  @BuilderParam aBuilder1: () => void = GlobalBuilder0;
  build(){}
}
  • 用父組件自定義構建函數初始化子組件@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組件在調用this.componentBuilder()時揍异,this指向其所屬組件全陨,即“Parent”。@Builder componentBuilder()傳給子組件@BuilderParam aBuilder0衷掷,在Child組件中調用this.aBuilder0()時,this指向在Child的label柿菩,即“Child”戚嗅。對于@BuilderParam aBuilder1,在將this.componentBuilder傳給aBuilder1時枢舶,調用bind綁定了this懦胞,因此其this.label指向Parent的label。

說明
開發(fā)者謹慎使用bind改變函數調用的上下文凉泄,可能會使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裝飾的方法可以是有參數和無參數的兩種形式,需與指向的@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() {}
  // 無參數類,指向的componentBuilder也是無參數類型
  @BuilderParam aBuilder0: () => void = this.FunABuilder0;
  // 有參數類型蒂誉,指向的GlobalBuilder1也是有參數類型的方法
  @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裝飾的屬性時也可通過尾隨閉包進行初始化教藻。在初始化自定義組件時,組件后緊跟一個大括號“{}”形成尾隨閉包場景右锨。

說明:

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


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

開發(fā)者可以將尾隨閉包內的內容看做@Builder裝飾的函數傳給@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的參數
      CustomContainer({ header: this.text }) {
        Column() {
          specificParam('testA', 'testB')
        }.backgroundColor(Color.Yellow)
        .onClick(() => {
          this.text = 'changeHeader';
        })
      }
    }
  }
}

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

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

@Styles裝飾器可以將多條樣式設置提煉成一個方法,直接在組件聲明的位置調用分唾。通過@Styles裝飾器可以快速定義并復用自定義樣式抗碰。用于快速定義并復用自定義樣式。

裝飾器使用說明

@Styles方法不支持參數弧蝇,反例如下。

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

// 在組件內
@Component
struct FancyUse {
  @Styles fancy() {
    .height(100)
  }
}
  • 定義在組件內的@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
    })
  }
}
  • 組件內@Styles的優(yōu)先級高于全局@Styles。
    框架優(yōu)先找當前組件內的@Styles去枷,如果找不到怖辆,則會全局查找。
使用場景

以下示例中演示了組件內@Styles和全局@Styles的用法删顶。

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

@Entry
@Component
struct FancyUse {
  @State heightValue: number = 100
  // 定義在組件內的@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)
      // 使用組件內的@Styles封裝的樣式
      Text('FancyB')
        .fancy()
        .fontSize(30)
    }
  }
}

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

在前文的示例中竖螃,可以使用@Styles用于樣式的擴展,在@Styles的基礎上逗余,我們提供了@Extend特咆,用于擴展原生組件樣式。

裝飾器使用說明
  • 語法
@Extend(UIComponentName) function functionName { ... }
  • 和@Styles不同录粱,@Extend僅支持在全局定義腻格,不支持在組件內部定義。
  • 和@Styles不同关摇,@Extend支持封裝指定的組件的私有屬性和私有事件荒叶,以及預定義相同組件的@Extend的方法。
// @Extend(Text)可以支持Text的私有屬性fontColor
@Extend(Text) function fancy () {
  .fontColor(Color.Red)
}
// superFancyText可以調用預定義的fancy
@Extend(Text) function superFancyText(size:number) {
    .fontSize(size)
    .fancy()
}
  • 和@Styles不同输虱,@Extend裝飾的方法支持參數些楣,開發(fā)者可以在調用時傳遞參數,調用遵循TS方法傳值調用宪睹。
// 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裝飾的方法的參數可以為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的參數可以為狀態(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組件均設置了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將樣式組合復用,示例如下菠齿。

@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僅僅應用于靜態(tài)頁面的樣式復用芋忿,stateStyles可以依據組件的內部狀態(tài)的不同,快速設置不同樣式疾棵。這就是我們本章要介紹的內容stateStyles(又稱為:多態(tài)樣式)戈钢。

概述

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

基礎場景

下面的示例展示了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綁定組件內的常規(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í)行過程時,通過逐幀回調函數修改不可動畫屬性值庸推,讓不可動畫屬性也能實現(xiàn)動畫效果常侦。

  • 可動畫屬性:如果一個屬性方法在animation屬性前調用浇冰,改變這個屬性的值可以生效animation屬性的動畫效果,這個屬性稱為可動畫屬性聋亡。比如height肘习、width、backgroundColor杀捻、translate屬性井厌,Text組件的fontSize屬性等。

  • 不可動畫屬性:如果一個屬性方法在animation屬性前調用致讥,改變這個屬性的值不能生效animation屬性的動畫效果仅仆,這個屬性稱為不可動畫屬性。比如Ployline組件的points屬性等垢袱。

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

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

名稱 入參類型 返回值類型 說明
plus AnimtableArithmetic<T> AnimtableArithmetic<T> 加法函數
subtract AnimtableArithmetic<T> AnimtableArithmetic<T> 減法函數
multiply number AnimtableArithmetic<T> 乘法函數
equals AnimtableArithmetic<T> boolean 相等判斷函數
使用場景

以下示例實現(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)
  }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市腮考,隨后出現(xiàn)的幾起案子雇毫,更是在濱河造成了極大的恐慌,老刑警劉巖踩蔚,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棚放,死亡現(xiàn)場離奇詭異,居然都是意外死亡馅闽,警方通過查閱死者的電腦和手機飘蚯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捞蛋,“玉大人孝冒,你說我怎么就攤上這事∧馍迹” “怎么了庄涡?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長搬设。 經常有香客問我穴店,道長撕捍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任泣洞,我火速辦了婚禮忧风,結果婚禮上,老公的妹妹穿的比我還像新娘球凰。我一直安慰自己狮腿,他們只是感情好,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布呕诉。 她就那樣靜靜地躺著缘厢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪甩挫。 梳的紋絲不亂的頭發(fā)上贴硫,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音伊者,去河邊找鬼英遭。 笑死,一個胖子當著我的面吹牛亦渗,可吹牛的內容都是我干的挖诸。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼法精,長吁一口氣:“原來是場噩夢啊……” “哼税灌!你這毒婦竟也來了?” 一聲冷哼從身側響起亿虽,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎苞也,沒想到半個月后洛勉,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡如迟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年收毫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片殷勘。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡此再,死狀恐怖,靈堂內的尸體忽然破棺而出玲销,到底是詐尸還是另有隱情输拇,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布贤斜,位于F島的核電站策吠,受9級特大地震影響逛裤,放射性物質發(fā)生泄漏。R本人自食惡果不足惜猴抹,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一带族、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蟀给,春花似錦蝙砌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至薪介,卻和暖如春祠饺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背汁政。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工道偷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人记劈。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓勺鸦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親目木。 傳聞我的和親對象是個殘疾皇子换途,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內容