Skip to content

Latest commit

 

History

History
1329 lines (954 loc) · 41.7 KB

5.ArkTS声明式UI入门.md

File metadata and controls

1329 lines (954 loc) · 41.7 KB

5.ArkTS声明式UI入门

方舟开发框架(ArkUI)概述

方舟开发框架(简称ArkUI)为OpenHarmony应用的UI开发提供了完整的基础设施,包括简洁的UI语法、丰富的UI功能(组件、布局、动画以及交互事件),以及实时界面预览工具等,可以支持开发者进行可视化界面开发。

基本概念

  • UI: 即用户界面。开发者可以将应用的用户界面设计为多个功能页面,每个页面进行单独的文件管理,并通过页面路由API完成页面间的调度管理如跳转、回退等操作,以实现应用内的功能解耦。

  • 组件: 页面搭建与显示的最小单位,如列表、网格、按钮、单选框、进度条、文本等。开发者通过多种组件的组合,构建出满足自身应用诉求的完整界面。

声明式UI描述

ArkTS以声明方式组合和扩展组件来描述应用程序的UI,同时还提供了基本的属性、事件和子组件配置方法,帮助开发者实现应用交互逻辑。

创建组件

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

说明:

创建组件时不需要new运算符。

无参数

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

Column() {
  Text('item 1')
  Divider()
  Text('item 2')
}
有参数

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

组件分类

组件根据功能可以分为以下五大类:

  • 基础组件
  • 容器组件
  • 媒体组件
  • 绘制组件
  • 画布组件

基础组件

  • Text
  • Button
  • Image
  • TextInput

容器组件

  • Column
  • Row
  • Stack
  • List

图片显示

Image组件用来渲染展示图片,支持gif、svg。

Image(src: string|PixelMap|Resource

  • string格式,通常用来加载网络图片,需要申请网络访问权限
Image('https://xxx.png')

权限的配置为app-entry-src-main目录中的module.json5文件:

    "requestPermissions": [
      {
        "name" : "ohos.permission.INTERNET",
      }
    ]
  • PixelMap格式,可以加载像素图,常用在图片编辑中
Image(pixelMapObject)
  • Resource格式,加载本地图片
// 加载app项目resource目录下media文件夹中的图片
Image($r('app.media.xxx'))
// 加载resource目录下rawfile总的图片,注意要带后缀
Image($rawfile('xxx.png'))

缩放类型

为了使图片在页面中有更好的显示效果,有时候需要对图片进行缩放处理。您可以使用objectFit属性设置图片的缩放类型,objectFit的参数类型为ImageFit。

image

ImageFit包含以下几种类型:

  • Contain:保持宽高比进行缩小或者放大,使得图片完全显示在显示边界内。
  • Cover(默认值):保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界。
  • Auto:自适应显示。
  • Fill:不保持宽高比进行放大缩小,使得图片充满显示边界。
  • ScaleDown:保持宽高比显示,图片缩小或者保持不变。
  • None:保持原有尺寸显示。

网络图片

加载网络图片时,默认网络超时是5分钟,建议使用alt配置加载时的占位图。如果需要更灵活的网络配置,可以使用SDK中提供的HTTP工具包发送网络请求,接着将返回的数据解码为Image组件中的PixelMap

// @ts-nocheck import http from '@ohos.net.http' import ResponseCode from '@ohos.net.http' import image from '@ohos.multimedia.image'

@Entry @Component struct Index {

// 先创建一个PixelMap状态变量用于接收网络图片 @State image: PixelMap = undefined

build() { Column({space: 10}) { Button("获取网络图片") .onClick(() => { this.httpRequest() }) Image(this.image).height(100).width(100) } .width('100%') .height('100%') .padding(10) }

// 网络图片请求方法 private httpRequest() { let httpRequest = http.createHttp()

httpRequest.request(
  "https://www.example.com/xxx.png",   // 请填写一个具体的网络图片地址
  (error, data) => {
    if(error) {
      console.log("error code: " + error.code + ", msg: " + error.message)
    } else {
      let code = data.responseCode
      if(ResponseCode.ResponseCode.OK == code) {
        let imageSource = image.createImageSource(data.result)
        let options = {alphaType: 0,                     // 透明度
                       editable: false,                  // 是否可编辑
                       pixelFormat: 3,                   // 像素格式
                       scaleMode: 1,                     // 缩略值
                       size: {height: 100, width: 100}}  // 创建图片大小
        imageSource.createPixelMap(options).then((pixelMap) => {
          this.image = pixelMap
        })
      } else {
        console.log("response code: " + code)
      }
    }
  }
)

} }

文本显示

Text组件用于在界面上展示一段文本信息,可以包含子组件Span。

// 使用字符
Text('xxx')

// 使用string.json文件中的配置
Text($r('app.string.xxx'))

当文本内容较多超出了Text组件范围的时候,您可以使用textOverflow设置文本截取方式,需配合maxLines使用,单独设置不生效,maxLines用于设置文本显示最大行数。下面的示例代码将textOverflow设置为Ellipsis ,它将显示不下的文本用 “...” 表示:

Text('This is the text content of Text Component This is the text content of Text Component') .fontSize(16) .maxLines(1) .textOverflow({overflow:TextOverflow.Ellipsis}) .backgroundColor(0xE6F2FD)

文本输入框

TextInput(placeHolder: '占位字符', text: '当前内容')
.type(InputType.Password)
.onChange((value: string) => {

})

设置光标位置 可以使用TextInputController动态设置光位置,下面的示例代码使用TextInputController的caretPosition方法,将光标移动到了第二个字符后。

@Entry @Component struct TextInputDemo { controller: TextInputController = new TextInputController()

build() { Column() { TextInput({ controller: this.controller }) Button('设置光标位置') .onClick(() => { this.controller.caretPosition(2) }) } .height('100%') .backgroundColor(0xE6F2FD) } }

占位组件

Blank() 可以将布局内剩余的空间给占满

按钮组件

// 添加按钮,以响应用户点击
Button('戳我')
  .type(ButtonType.Capsule) // 按钮风格: 胶囊型按钮(圆角默认为高度的一半)
  .margin({
    // 距离上一个的高度
    top: 20
  })
  .onClick(() => {
    router.pushUrl({ url: 'pages/Second' }).then(() => {
      console.info('start second page')
    })
  })
  .backgroundColor('#0D9FFB')
  .width('40%')
  .height('5%')

Button组件可以包含子组件,让您可以开发出更丰富多样的Button,下面的示例代码中Button组件包含了一个Image组件:

Button({ type: ButtonType.Circle, stateEffect: true }) {
  Image($r('app.media.icon_delete'))
    .width(30)
    .height(30)
}
.width(55)
.height(55)
.backgroundColor(0x317aff)

滑动条属性

 Slider({
      min: 0,
      max: 100,
      value: 30,
      step: 10,
      style: SliderStyle.OutSet, // 滑块在进度条内还是上
      direction: Axis.Horizontal,
      reverse: false
    }).showTips(true) // 滑动时气泡显示进度
      .showSteps(true) // 显示步长
      .blockColor('#f00')
      .trackThickness(8) // 滑动条的粗细
      .width('100%')
      .onChange((value: number, mode: SliderChangeMode) => {

      })

LoadingProgress

LoadingProgress组件用于显示加载进展,比如应用的登录界面,当我们点击登录的时候,显示的“正在登录”的进度条状态。LoadingProgress的使用非常简单,只需要设置颜色和宽高就可以了。

LoadingProgress()
  .color(Color.Blue)
  .height(60)
  .width(60)

日期选择组件

DatePicker(options?: {start?: Date, end?: Date, selected?: Date})

onChange(callback: (value: DatePickerResult) => void)

线性布局(Row/Column)

线性布局(LinearLayout)是开发中最常用的布局,通过线性容器Row和Column构建。
线性布局是其他布局的基础,其子元素在线性方向上(水平方向和垂直方向)依次排列。
线性布局的排列方向由所选容器组件决定,Column容器内子元素按照垂直方向排列,Row容器内子元素按照水平方向排列。

在布局容器内,可以通过space属性设置排列方向上子元素的间距,使各子元素在排列方向上有等间距效果。 就是Android的LinearLayout

Column({ space: 20 }) {
  Text('space: 20').fontSize(15).fontColor(Color.Gray).width('90%')
  Row().width('90%').height(50).backgroundColor(0xF5DEB3)
  Row().width('90%').height(50).backgroundColor(0xD2B48C)
  Row().width('90%').height(50).backgroundColor(0xF5DEB3)
}.width('100%')

image

  • 主轴: 当前空间排列方向的轴
  • 交叉轴: 与主轴垂直的轴

对齐方式:

  • justifyContent: 设置子元素在主轴方向的对齐方式, 参数是FlexAlign的枚举
  • alignItems: 设置子元素在交叉轴方向的对齐方式, Row容器使用VerticalAlign枚举,Column容器使用HorizontalAlign枚举

justifyContent

在布局容器内,可以通过justifyContent属性设置子元素在容器主轴上的排列方式。可以从主轴起始位置开始排布,也可以从主轴结束位置开始排布,或者均匀分割主轴的空间。

image

Row和Column的方向不同,方式都是一样的

alignItems

image

间距

和Android的基本一样。

  • 外边距: margin
  • 内边距: padding

层叠布局(Stack)

层叠布局(StackLayout)用于在屏幕上预留一块区域来显示组件中的元素,提供元素可以重叠的布局。

层叠布局通过Stack容器组件实现位置的固定定位与层叠,容器中的子元素依次入栈,后一个子元素覆盖前一个子元素,子元素可以叠加,也可以设置位置。

层叠布局具有较强的页面层叠、位置定位能力,其使用场景有广告、卡片层叠效果等。 这有点类似Android中的FrameLayout

弹性布局(Flex)

弹性布局是与线性布局类似的布局方式。区别在于弹性布局默认能够使子组件压缩或拉伸。在子组件需要计算拉伸或压缩比例时优先使用此布局,可使得多个容器内子组件能有更好的视觉上的填充容器效果。

弹性布局(Flex)提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。常用于页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等。

相对布局(RelativeContainer)

RelativeContainer为采用相对布局的容器,支持容器内部的子元素设置相对位置关系。子元素支持指定兄弟元素作为锚点,也支持指定父容器作为锚点,基于锚点做相对位置布局。

@Entry
@Component
struct Index {
  build() {
    Row() {
      RelativeContainer() {
        Row()
          .width(100)
          .height(100)
          .backgroundColor('#FF3333')
          .alignRules({
            top: { anchor: '__container__', align: VerticalAlign.Top },  //以父容器为锚点,竖直方向顶头对齐
            middle: { anchor: '__container__', align: HorizontalAlign.Center }  //以父容器为锚点,水平方向居中对齐
          })
          .id('row1')  //设置锚点为row1

        Row() {
          Image($r('app.media.icon'))
        }
        .height(100).width(100)
        .alignRules({
          top: { anchor: 'row1', align: VerticalAlign.Bottom },  //以row1组件为锚点,竖直方向低端对齐
          left: { anchor: 'row1', align: HorizontalAlign.Start }  //以row1组件为锚点,水平方向开头对齐
        })
        .id('row2')  //设置锚点为row2

        Row()
          .width(100)
          .height(100)
          .backgroundColor('#FFCC00')
          .alignRules({
            top: { anchor: 'row2', align: VerticalAlign.Top }
          })
          .id('row3')  //设置锚点为row3

        Row()
          .width(100)
          .height(100)
          .backgroundColor('#FF9966')
          .alignRules({
            top: { anchor: 'row2', align: VerticalAlign.Top },
            left: { anchor: 'row2', align: HorizontalAlign.End },
          })
          .id('row4')  //设置锚点为row4

        Row()
          .width(100)
          .height(100)
          .backgroundColor('#FF66FF')
          .alignRules({
            top: { anchor: 'row2', align: VerticalAlign.Bottom },
            middle: { anchor: 'row2', align: HorizontalAlign.Center }
          })
          .id('row5')  //设置锚点为row5
      }
      .width(300).height(300)
      .border({ width: 2, color: '#6699FF' })
    }
    .height('100%').margin({ left: 30 })
  }
}

栅格布局(GridRow/GridCol)

栅格布局是一种通用的辅助定位工具,对移动设备的界面设计有较好的借鉴作用。主要优势包括:

提供可循的规律:栅格布局可以为布局提供规律性的结构,解决多尺寸多设备的动态布局问题。通过将页面划分为等宽的列数和行数,可以方便地对页面元素进行定位和排版。

统一的定位标注:栅格布局可以为系统提供一种统一的定位标注,保证不同设备上各个模块的布局一致性。这可以减少设计和开发的复杂度,提高工作效率。

灵活的间距调整方法:栅格布局可以提供一种灵活的间距调整方法,满足特殊场景布局调整的需求。通过调整列与列之间和行与行之间的间距,可以控制整个页面的排版效果。

自动换行和自适应:栅格布局可以完成一对多布局的自动换行和自适应。当页面元素的数量超出了一行或一列的容量时,他们会自动换到下一行或下一列,并且在不同的设备上自适应排版,使得页面布局更加灵活和适应性强。

GridRow为栅格容器组件,需与栅格子组件GridCol在栅格布局场景中联合使用。

创建列表(List)

List是很常用的滚动类容器组件,一般和子组件ListItem一起使用,List列表中的每一个列表项对应一个ListItem组件。 List组件里面的列表项默认是按垂直方向排列的,如果您想让列表沿水平方向排列,您可以将List组件的listDirection属性设置为Axis.Horizontal。

列表是一种复杂的容器,当列表项达到一定数量,内容超过屏幕大小时,可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集,例如图片和文本。

在列表中显示数据集合是许多应用程序中的常见要求(如通讯录、音乐列表、购物清单等)。

使用列表可以轻松高效地显示结构化、可滚动的信息。

通过在List组件中按垂直或者水平方向线性排列子组件ListItemGroup或ListItem,为列表中的行或列提供单个视图,或使用循环渲染迭代一组行或列,或混合任意数量的单个视图和ForEach结构,构建一个列表。

List组件支持使用条件渲染、循环渲染、懒加载等渲染控制方式生成子组件。

类似Android的ListView。

List({space: 10}) {
  ForEach([1, 2, 3, 4], item => {
    ListItem() {
      // 列表项内容,只能包含一个根组件
      Text('item')
    }
  })
}
.width('100%')
// 设置item的对齐方式
.alignListItem(ListItemAlign.Center)
// 设置几列
.lanes(2)

设置列表分割线

List组件子组件ListItem之间默认是没有分割线的,部分场景子组件ListItem间需要设置分割线,这时候您可以使用List组件的divider属性。divider属性包含四个参数:

  • strokeWidth: 分割线的线宽。
  • color: 分割线的颜色。
  • startMargin:分割线距离列表侧边起始端的距离。
  • endMargin: 分割线距离列表侧边结束端的距离。

List组件提供了一系列事件方法用来监听列表的滚动,您可以根据需要,监听这些事件来做一些操作:

  • onScroll:列表滑动时触发,返回值scrollOffset为滑动偏移量,scrollState为当前滑动状态。
  • onScrollIndex:列表滑动时触发,返回值分别为滑动起始位置索引值与滑动结束位置索引值。
  • onReachStart:列表到达起始位置时触发。
  • onReachEnd:列表到底末尾位置时触发。
  • onScrollStop:列表滑动停止时触发。

循环渲染

ForEach,这里的渲染循环并不是前面语法说的for循环

ForEach (
    arr: Array,        // 1.要遍历的数据数组
    (item: any, index?: number) => {   // 2.页面组件生成函数

      Row() {
        Image(item.image)
        Column() {
          Text(item.name)
          Text(item.price)
        }
      }

    }),
    keyGenerator?: (item: any, index?: number): string => {  // 3.键生成函数,为数组每一项生成一个唯一标识,组件是否重新渲染的判断标准

    }
)

ForEach接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在ForEach父容器组件中的子组件。例如,ListItem组件要求ForEach的父容器组件必须为List组件。

在ForEach循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。

ForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: any, index: number) => { return index + '__' + JSON.stringify(item); }。

在初始化渲染时,ForEach会加载数据源的所有数据,并为每个数据项创建对应的组件,然后将其挂载到渲染树上。如果数据源非常大或有特定的性能需求,建议使用LazyForEach组件。

创建网格(Grid/GridItem)

Grid组件为网格容器,是一种网格列表,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。Grid组件一般和子组件GridItem一起使用,Grid列表中的每一个条目对应一个GridItem组件。

ArkUI提供了Grid容器组件和子组件GridItem,用于构建网格布局。Grid用于设置网格布局相关参数,GridItem定义子组件相关特征。Grid组件支持使用条件渲染、循环渲染、懒加载等方式生成子组件。

@Entry
@Component
struct GridExample {
  // 定义一个长度为16的数组
  private arr: string[] = new Array(16).fill('').map((_, index) => `item ${index}`);

  build() {
    Column() {
      Grid() {
        ForEach(this.arr, (item: string) => {
          GridItem() {
            Text(item)
              .fontSize(16)
              .fontColor(Color.White)
              .backgroundColor(0x007DFF)
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
        }, item => item)
      }
      .columnsTemplate('1fr 1fr 1fr 1fr')
      .rowsTemplate('1fr 1fr 1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .height(300)
    }
    .width('100%')
    .padding(12)
    .backgroundColor(0xF1F3F5)
  }
}
  • 设置columnsTemplate的值为'1fr 1fr 1fr 1fr',表示这个网格为4列,将Grid允许的宽分为4等分,每列占1份;
  • rowsTemplate的值为'1fr 1fr 1fr 1fr',表示这个网格为4行,将Grid允许的高分为4等分,每行占1份。

这样就构成了一个4行4列的网格列表,然后使用columnsGap设置列间距为10vp,使用rowsGap设置行间距也为10vp。

上面构建的网格布局使用了固定的行数和列数,所以构建出的网格是不可滚动的。然而有时候因为内容较多,我们通过滚动的方式来显示更多的内容,就需要一个可以滚动的网格布局。我们只需要设置rowsTemplate和columnsTemplate中的一个即可。

LazyForEach

数据懒加载

LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。

LazyForEach(
    dataSource: IDataSource,             // 需要进行数据迭代的数据源
    itemGenerator: (item: Object) => void,  // 子组件生成函数
    keyGenerator?: (item: Object): string => string // 键值生成函数
): void

使用限制

LazyForEach必须在容器组件内使用,仅有List、Grid、Swiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。

LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。

生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。

允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。

键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件被框架忽略,从而无法在父容器内显示。

LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。

为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。

性能提升的推荐方法

开发者在使用长列表时,如果直接采用循环渲染方式,如下所示,会一次性加载所有的列表元素,一方面会导致页面启动时间过长,影响用户体验,另一方面也会增加服务器的压力和流量,加重系统负担。

我们希望从数据源中按需迭代加载数据并创建相应组件,因此需要使用数据懒加载,如下所示:

使用数据懒加载

class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = []

  public totalCount(): number {
    return 0
  }

  public getData(index: number): any {
    return undefined
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener')
      this.listeners.push(listener)
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      console.info('remove listener')
      this.listeners.splice(pos, 1)
    }
  }

  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded()
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index)
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index)
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index)
    })
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to)
    })
  }
}

class MyDataSource extends BasicDataSource {
  private dataArray: string[] = ['item value: 0', 'item value: 1', 'item value: 2']

  public totalCount(): number {
    return this.dataArray.length
  }

  public getData(index: number): any {
    return this.dataArray[index]
  }

  public addData(index: number, data: string): void {
    this.dataArray.splice(index, 0, data)
    this.notifyDataAdd(index)
  }

  public pushData(data: string): void {
    this.dataArray.push(data)
    this.notifyDataAdd(this.dataArray.length - 1)
  }
}

@Entry
@Component
struct MyComponent {
  private data: MyDataSource = new MyDataSource()

  build() {
    List() {
      LazyForEach(this.data, (item: string) => {
        ListItem() {
          Row() {
            Text(item).fontSize(20).margin({ left: 10 })
          }
        }
        .onClick(() => {
          this.data.pushData('item value: ' + this.data.totalCount())
        })
      }, item => item)
    }
  }
}

设置List组件的宽高

在使用Scroll容器组件嵌套List组件加载长列表时,若不指定List的宽高尺寸,则默认全部加载。

说明 Scroll嵌套List时:

List没有设置宽高,会布局List的所有子组件。 List设置宽高,会布局List显示区域内的子组件。 List使用ForEach加载子组件时,无论是否设置List的宽高,都会加载所有子组件。 List使用LazyForEach加载子组件时,没有设置List的宽高,会加载所有子组件,设置了List的宽高,会加载List显示区域内的子组件。

减少应用滑动白块

应用通过增大List/Grid控件的cachedCount参数,调整UI的加载范围。cachedCount表示屏幕外List/Grid预加载item的个数。

如果需要请求网络图片,可以在item滑动到屏幕显示之前,提前下载好内容,从而减少滑动白块。

创建轮播(Swiper)

Swiper组件提供滑动轮播显示的能力。Swiper本身是一个容器组件,当设置了多个子组件后,可以对这些子组件进行轮播显示。通常,在一些应用首页显示推荐的内容时,需要用到轮播显示的能力。

布局与约束

Swiper作为一个容器组件,在自身尺寸属性未被设置时,会自动根据子组件的大小设置自身的尺寸。如果开发者对Swiper组件设置了固定的尺寸,则在轮播显示过程中均以该尺寸生效;否则,在轮播过程中,会根据子组件的大小自动调整自身的尺寸。

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

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

自定义组件:@Component装饰的UI单元,可以组合多个系统组件实现UI的复用。

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

image

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

  • onPageShow:页面每次显示时触发。

  • onPageHide:页面每次隐藏时触发一次。

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

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

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

  • aboutToDisappear:在自定义组件即将析构销毁时执行。

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

image

@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)
    .onClick(() => {
      this.title = 'Hello ArkUI';
    })
  }
}

自定义组件

方式1:@Component装饰器进行自定义组件抽取

1.1当前组件内抽取
@Entry
@Component
struct Second {
  // 注意string是小写,不是String
  @State message: string = "Hello HarmonyOS"

  build() {
    Row() {
      Column() {
        Header({content: 'haha'})
        Text(this.message)
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

@Component
struct Header {
  content: string
  build() {
    Text(this.content)
      .fontSize(15)
  }
}
1.2单独文件抽取

在ets目录下新加conponents文件夹,里面新加Header.ets

@Component
// 想要输出的模块,必须加上export
export struct Header {
  content: string
  build() {
    Text(this.content)
      .fontSize(15)
  }
}

使用:

// 需要先导入组件
import {Header} from '../components/Header'
@Entry
@Component
struct Second {
  // 注意string是小写,不是String
  @State message: string = "Hello HarmonyOS"

  build() {
    Row() {
      Column() {
        Header({content: 'haha'})
        Text(this.message)
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

方式2:@Builder装饰器进行自定义构建函数

前面章节介绍了如何创建一个自定义组件。该自定义组件内部UI结构固定,仅与使用方进行数据传递。ArkUI还提供了一种更轻量的UI元素复用机制@Builder,@Builder所装饰的函数遵循build()函数语法规则,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。

为了简化语言,我们将@Builder装饰的函数也称为“自定义构建函数”。

组件状态管理

ArkUI框架提供了多种管理状态的装饰器来修饰变量,使用这些装饰器修饰的变量即称为状态变量。

在组件范围传递的状态管理常见的场景如下: image

  • @State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。

  • @Prop与@State有相同的语义,但初始化方式不同。@Prop装饰的变量必须使用其父组件提供的@State变量进行初始化,允许组件内部修改@Prop变量,但更改不会通知给父组件,即@Prop属于单向数据绑定。

  • @Link装饰的变量可以和父组件的@State变量建立双向数据绑定,需要注意的是:@Link变量不能在组件内部进行初始化。

@Link装饰的变量可以和父组件的@State变量建立双向数据绑定,任何一方所做的修改都会反应给另一方:

子组件可以通过参数传递父组件中的@State值,而且可以通过$操作符创建“引用”,达到双向绑定的作用:

@Entry
@Component RankPage {
    @State isSwitchDataSource: boolean = true

    build() {
      Column() {
        // 对子组件的@Link成员变量进行初始化,必须使用$操作符创建引用达到双向绑定的目的
        TileComponent({isRefreshData: $isSwitchDataSource}) {
          ....
        }
      }
    }
}

@Component {
  export struct TileComponent {
    // @Link修饰的变量不能初始化,要父组件进行初始化
    @Link isRefreshData: boolean

    build() {
      Row() {
        Button('click')
        .onClick() => {
          // 会引起父组件的刷新
          this.isRefreshData = !this.isRefreshData
        }
      }
    }
  }
}

管理组件拥有的状态,即图中Components级别的状态管理:

  • @State:@State装饰的变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。当其数值改变时,会引起相关组件的渲染刷新。

  • @Prop:@Prop装饰的变量可以和父组件建立单向同步关系,@Prop装饰的变量是可变的,但修改不会同步回父组件。

  • @Link:@Link装饰的变量和父组件构建双向同步关系的状态变量,父组件会接受来自@Link装饰的变量的修改的同步,父组件的更新也会同步给@Link装饰的变量。

  • @Provide/@Consume:@Provide/@Consume装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定。

  • @Observed:@Observed装饰class,需要观察多层嵌套场景的class需要被@Observed装饰。单独使用@Observed没有任何作用,需要和@ObjectLink、@Prop连用。

  • @ObjectLink:@ObjectLink装饰的变量接收@Observed装饰的class的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。

2.1自定义组件内自定义构建函数

在外组件struct内部定义: 定义的语法:

// 组件内的构建函数不用加function
@Builder MyBuilderFunction() { ... }

使用方法:

// 组件内的构建函数需要使用this
this.MyBuilderFunction() { ... }

允许在自定义组件内定义一个或多个@Builder方法,该方法被认为是该组件的私有、特殊类型的成员函数。

自定义构建函数可以在所属组件的build方法和其他自定义构建函数中调用,但不允许在组件外调用。

在自定义函数体中,this指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this访问自定义组件的状态变量而不是参数传递。

2.2全局自定义构建函数

定义在当前组件文件内,但是在当前组件struct之外 定义的语法:

// 全局构建函数需要用function
@Builder function MyGlobalBuilderFunction() { ... }

使用方法:

// 直接调用,不用this
MyGlobalBuilderFunction()
  • 全局的自定义构建函数可以被整个应用获取,不允许使用this和bind方法。

  • 如果不涉及组件状态变化,建议使用全局的自定义构建方法。

自定义构建函数的参数传递有按值传递和按引用传递两种,均需遵守以下规则:

  • 参数的类型必须与参数声明的类型一致,不允许undefined、null和返回undefined、null的表达式。

  • 在@Builder修饰的函数内部,不允许改变参数值。

  • @Builder内UI语法遵循UI语法规则。

按引用传递参数

按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新。ArkUI提供$$作为按引用传递参数的范式。

class ABuilderParam {
  paramA1: string = ''
  paramB1: string = ''
}

@Builder function ABuilder($$ : ABuilderParam) {...}
class ABuilderParam {
  paramA1: string = ''
}

@Builder function ABuilder($$: ABuilderParam) {
  Row() {
    Text(`UseStateVarByReference: ${$$.paramA1} `)
  }
}
@Entry
@Component
struct Parent {
  @State label: string = 'Hello';
  build() {
    Column() {
      // 在Parent组件中调用ABuilder的时候,将this.label引用传递给ABuilder
      ABuilder({ paramA1: this.label })
      Button('Click me').onClick(() => {
        // 点击“Click me”后,UI从“Hello”刷新为“ArkUI”
        this.label = 'ArkUI';
      })
    }
  }
}

$$运算符:给内置组件提供TS变量的引用,使得TS变量和内置组件的内部状态保持同步。

按值传递参数

调用@Builder装饰的函数默认按值传递。当传递的参数为状态变量时,状态变量的改变不会引起@Builder方法内的UI刷新。所以当使用状态变量的时候,推荐使用按引用传递。

@Builder function ABuilder(paramA1: string) {
  Row() {
    Text(`UseStateVarByValue: ${paramA1} `)
  }
}
@Entry
@Component
struct Parent {
  label: string = 'Hello';
  build() {
    Column() {
      ABuilder(this.label)
    }
  }
}

自定义组件通用信息

外部自定义样式:

@Entry
@Component
struct Second {
  // 注意string是小写,不是String
  @State message: string = "Hello HarmonyOS"

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fillScreen();
      }
      .width('100%')
    }
    .height('100%')
  }
}

// 全局公共样式函数
@Styles function fillScreen() {
  .width('100%')
  .height('100%')
  .backgroundColor('#f00')
  .padding('20')
}

内部自定义样式函数,同样是去掉function:

@Entry
@Component
struct Second {
  // 注意string是小写,不是String
  @State message: string = "Hello HarmonyOS"

  @Styles fillScreen() {
    .width('100%')
    .height('100%')
    .backgroundColor('#f00')
    .padding('20')
  }

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fillScreen()
      }
      .width('100%')
    }
    .height('100%')
  }
}

上面的属性都是所有组件通用的属性,但是如果我想对Text里面的fontSize、fontColor进行抽取呢? 这个时候因为不是通用的,用@Style就不行了。那就要用@Extend来抽取。

@Extend装饰器:自定义扩展组件样式

类似Kotlin的扩展函数

@Extend,用于扩展原生组件信息(非通用组件信息)。

使用规则 和@Styles不同,@Extend仅支持在全局定义,不支持在组件内部定义。

和@Styles不同,@Extend支持封装指定的组件的私有属性和私有事件,以及预定义相同组件的@Extend的方法。

注意: @Extend只能写在全局,不能写在组件内部

// @Extend(Text)可以支持Text的私有属性fontColor
@Extend(Text) function fancy () {
  .fontColor(Color.Red)
}
// superFancyText可以调用预定义的fancy
@Extend(Text) function superFancyText(size:number) {
    .fontSize(size)
    .fancy()
}

组件通用信息

所有组件都有的通用属性或事件,分为三类:

  • 通用事件
  • 通用属性
  • 手势处理

鼠标在组件上右键,菜单会显示Show in Api Reference。 image

点击进去可以看到组件的文档,里面有通用属性的介绍 image

  • 点击事件
onClick(event: (event?: ClickEvent) => void)

stateStyles:多态样式

@Styles和@Extend仅仅应用于静态页面的样式复用,stateStyles可以依据组件的内部状态的不同,快速设置不同样式。

stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。ArkUI提供以下几种状态:

  • focused:获焦态。

  • normal:正常态。

  • pressed:按压态。

  • disabled:不可用态。

  • selected:选中态。

 Button('Click me')
  .stateStyles({
    focused: {
      .backgroundColor(Color.Pink)
    },
    pressed: {
      .backgroundColor(Color.Black)
    },
    normal: {
      .backgroundColor(Color.Yellow)
    }
  })

使用资源引用类型

Resource是资源引用类型,用于设置组件属性的值。推荐大家优先使用Resource类型,将资源文件(字符串、图片、音频等)统一存放于resources目录下,便于开发者统一维护。同时系统可以根据当前配置加载合适的资源,例如,开发者可以根据屏幕尺寸呈现不同的布局效果,或根据语言设置提供不同的字符串。

在string.json中定义Button显示的文本。

{
  "string": [
    {
      "name": "login_text",
      "value": "登录"
    }
  ]
} 
在float.json中定义Button的宽高和字体大小。

{
  "float": [
    {
      "name": "button_width",
      "value": "300vp"
    },
    {
      "name": "button_height",
      "value": "40vp"
    },
    {
      "name": "login_fontSize",
      "value": "18fp"
    }
  ]
}

在color.json中定义Button的背景颜色。

{
  "color": [
    {
      "name": "button_color",
      "value": "#1890ff"
    }
  ]
}

然后在Button组件通过“$r('app.type.name')”的形式引用应用资源。app代表应用内resources目录中定义的资源;type代表资源类型(或资源的存放位置),可以取“color”、“float”、“string”、“plural”、“media”;name代表资源命名,由开发者定义资源时确定。

build函数

所有声明在build()函数的语言,我们统称为UI描述,UI描述需要遵循以下规则:

  • @Entry装饰的自定义组件,其build()函数下的根节点唯一且必要,且必须为容器组件,其中ForEach禁止作为根节点。

  • @Component装饰的自定义组件,其build()函数下的根节点唯一且必要,可以为非容器组件,其中ForEach禁止作为根节点。

  • 不允许声明本地变量。

  • 不允许在UI描述里直接使用console.info,但允许在方法或者函数里使用。

  • 不允许使用switch语法,如果需要使用条件判断,请使用if。

  • 不允许直接改变状态变量。 不能在自定义组件的build()或@Builder方法里直接改变状态变量,这可能会造成循环渲染的风险。