HarmonyOS基础(三):ArkUI框架与声明式UI

ArkUI是HarmonyOS官方推荐的声明式UI开发框架,是构建鸿蒙应用界面的核心工具。与传统的命令式UI开发不同,声明式UI让开发者通过描述界面应有的状态来构建UI,而不是通过一步步的操作指令来构建。这种方式使得UI代码更加简洁、可维护性更强,并且能够自动响应数据的变化。

本文将系统讲解ArkUI框架的核心概念与实战技巧。从声明式UI的基本语法出发,深入解析组件化开发、状态管理、布局系统、动画特效等关键内容。通过大量代码示例和实践案例,帮助开发者全面掌握ArkUI的使用方法,能够独立开发出界面美观、交互流畅、多端适配的HarmonyOS应用。

一、ArkUI框架概述

1.1 什么是ArkUI

ArkUI是HarmonyOS生态中核心的UI渲染框架,采用声明式开发范式,支持手机、平板、PC等多种终端设备的统一开发。开发者通过ArkTS语言描述界面,框架负责组件树构建、布局测量、渲染绘制及事件处理。

ArkUI框架包括ArkTS和eTS两种开发语言。ArkTS是基于TypeScript的声明式UI开发语言,它结合了TypeScript的类型系统和ArkUI的组件库,使得开发者能够以更加简洁、高效的方式构建UI。是目前主推且长期演进的开发语言。

ArkUI的核心设计理念可以概括为两点。第一是描述UI的呈现结果,而不关心过程。开发者只需要声明界面应该呈现什么样子,框架会自动处理如何渲染。第二是状态驱动视图更新。界面会随数据更改而自动刷新,开发者只需要关注数据的变化。

1.2 ArkUI的技术架构

ArkUI的技术架构自下而上分为三个层次。

底层是方舟运行时引擎,负责JavaScript/TypeScript代码的执行和内存管理。中间层是UI渲染引擎,包括组件管理、布局计算、渲染管线等核心模块。最上层是开发者接口层,提供声明式UI语法和丰富的API。

当开发者使用ArkUI进行开发时,会使用布局组件和基础组件来描述UI。这些描述形成一棵组件树结构,可以称为应用组件树。基础组件是叶子节点,布局组件是中间节点。当用户进行交互操作时,会触发UI修改。UI修改本质上是触发组件树重新渲染以更新应用UI的过程。

UI更新过程由数据处理和UI更新两部分组成。在数据处理阶段,状态数据被更新,状态数据是指用@State等装饰器定义的数据。当数据发生变化时,更新需要一定时间。与数据关联的组件数量也会影响下一次UI更新的时间。因此,在开发过程中需要避免无效数据更新导致的多余UI更新操作。在UI更新阶段,需要通过Build、Measure、Layout和Render阶段来更新目标元素。Build是创建组件并标记组件为脏的过程。Measure是测量组件宽高的阶段。Layout是将元素放置在屏幕上的阶段。Render是根据测量和布局获得的尺寸和位置等信息提交和绘制组件的过程。

1.3 声明式UI与命令式UI的对比

理解声明式UI与命令式UI的区别是掌握ArkUI的关键。

命令式UI是传统开发方式。开发者需要一步步告诉系统如何构建和更新UI。例如,创建按钮、设置属性、添加事件监听器、在数据变化时手动更新按钮的显示状态。这种方式的优点是控制精细,缺点是代码量大、容易出错、维护困难。

声明式UI是ArkUI采用的开发方式。开发者只需要描述UI应该呈现什么样子。例如,声明一个按钮,绑定一个状态变量。当状态变量变化时,UI会自动更新。开发者无需关心如何更新UI,只需要关注数据的变化。

声明式UI的核心优势体现在以下几个方面。代码更加简洁,同样的界面用声明式UI只需要三分之一的代码量。可维护性更强,数据与UI的绑定关系清晰明确。开发效率更高,不需要手动操作DOM或组件实例。Bug更少,减少了手动更新UI时可能出现的遗漏或错误。

1.4 声明式UI的基本思想

声明式UI的思想主要体现在两个方面。

描述UI的呈现结果,而不关心过程。开发者只需要声明最终界面应该是什么样子,框架会自动处理从状态到UI的映射过程。

状态驱动视图更新。状态是驱动视图动态变化的数据源。视图是UI描述的结果。当状态发生变化时,视图会自动重新渲染以反映最新的状态。

这种思想类似于函数式编程中的纯函数概念。UI是状态的函数,给定相同的状态,总是产生相同的UI。这使得UI行为变得可预测、可测试。

二、声明式UI基本语法

2.1 自定义组件的基本结构

在ArkUI中,通过struct声明组件名,并通过@Component装饰器来构成一个自定义组件。使用@Entry和@Component装饰的自定义组件作为页面的入口,会在页面加载时首先进行渲染。

TypeScript 复制代码
@Entry
@Component
struct ToDoListPage {
  // 组件属性和方法
  build() {
    // UI描述
  }
}

使用@Component装饰的自定义组件可以作为页面的组成部分,被其他组件引用。在自定义组件内需要使用build方法来进行UI描述。build方法内可以容纳内置组件和其他自定义组件。

TypeScript 复制代码
@Component
struct ToDoItem {
  // 组件属性和方法
  build() {
    // UI描述
  }
}

2.2 属性配置方法

ArkTS提供了多种属性配置方式,用于描述界面的样式。

常量传递是最基本的方式。直接在属性方法中传入常量值,例如使用fontSize(50)来配置字体大小。

TypeScript 复制代码
Text('Hello World')
  .fontSize(50)

变量传递允许在组件内定义变量后使用。在组件内部成员变量定义后,可以使用this.size方式使用该变量。

TypeScript 复制代码
@Component
struct MyComponent {
  @State size: number = 50;
  build() {
    Text('Hello World')
      .fontSize(this.size)
  }
}

链式调用是在配置多个属性时,ArkTS提供的链式调用方式。通过点号方式连续配置多个属性。

TypeScript 复制代码
Text('Hello World')
  .fontSize(this.size)
  .width(100)
  .height(100)

表达式传递允许在属性中传入普通表达式以及三目运算表达式。

TypeScript 复制代码
Text('Hello World')
  .fontSize(this.size)
  .width(this.count + 100)
  .height(this.count % 2 === 0 ? 100 : 200)

内置枚举类型支持,ArkTS中提供了Color、FontWeight等内置枚举类型。

TypeScript 复制代码
Text('Hello World')
  .fontColor(Color.Red)
  .fontWeight(FontWeight.Bold)

2.3 容器组件的使用

对于有多种组件需要进行组合时,容器组件描述了这些组件应该如何排列。ArkUI中的布局容器有很多种,在不同场合选择不同的布局容器实现。ArkTS使用容器组件采用花括号语法,内部放置UI描述。

列布局Column是最基础的布局之一,整体从上往下纵向排列。

TypeScript 复制代码
Column() {
  Text('页面标题')
  // 其他组件
}

行布局Row是最基础的布局之一,元素为横向排列。

TypeScript 复制代码
Row() {
  Image($r('app.media.ic_default'))
  Text('内容')
}

2.4 状态变量与数据绑定

声明式UI的特点是UI随数据更改而自动刷新。使用@State装饰器定义的状态变量,框架会建立数据和视图之间的绑定,其值的改变会影响UI的显示。

TypeScript 复制代码
@State isComplete: boolean = false;

当isComplete的值发生变化时,所有依赖该变量的UI组件都会自动更新。

@State装饰器的作用是在框架内建立数据和视图之间的绑定关系。当状态变化时,UI自动刷新,无需手动操作DOM。

2.5 条件渲染与循环渲染

条件渲染用于根据状态切换组件的显示与消失。ArkUI提供了if/else语法来进行条件的判断。

TypeScript 复制代码
if (this.isComplete) {
  this.labelIcon($r('app.media.ic_ok'));
} else {
  this.labelIcon($r('app.media.ic_default'));
}

循环渲染用于渲染列表数据。ForEach基本使用中,需要了解要渲染的数据以及要生成的UI内容两个部分。

TypeScript 复制代码
ForEach(this.totalTasks, (item: string) => {
  ToDoItem({content: item})
}, (item: string) => item)

2.6 @Builder装饰器

当多个组件具有大量重复代码时,ArkTS提供了@Builder装饰器来修饰一个函数,快速生成布局内容,从而避免重复的UI描述内容。

TypeScript 复制代码
@Builder labelIcon(icon: Resource) {
  Image(icon)
    .objectFit(ImageFit.Contain)
    .width($r('app.float.checkbox_width'))
    .height($r('app.float.checkbox_width'))
    .margin($r('app.float.checkbox_margin'))
}

使用时只需要使用@Builder装饰的函数名,即可快速创建布局。

TypeScript 复制代码
if (this.isComplete) {
  this.labelIcon($r('app.media.ic_ok'));
} else {
  this.labelIcon($r('app.media.ic_default'));
}

三、基础组件详解

3.1 Text组件

Text组件用于显示文本内容,是UI开发中最基础的组件之一。

Text组件的核心属性包括fontSize用于设置字体大小,fontColor用于设置字体颜色,fontWeight用于设置字体粗细,textAlign用于设置文本对齐方式。

Text组件支持多种字体粗细级别,包括FontWeight.Lighter、FontWeight.Normal、FontWeight.Bold、FontWeight.Bolder等。

Text组件可以通过textAlign属性设置对齐方式,支持TextAlign.Start、TextAlign.Center、TextAlign.End三种对齐模式。

Text组件支持文本的装饰效果,通过decoration属性可以添加下划线、删除线等效果。

TypeScript 复制代码
Text('Hello World')
  .fontSize(24)
  .fontColor(Color.Black)
  .fontWeight(FontWeight.Bold)
  .textAlign(TextAlign.Center)
  .decoration({ type: TextDecorationType.LineThrough })

3.2 Button组件

Button组件用于触发用户操作,是交互式应用的核心组件。

Button组件的核心属性包括type用于设置按钮类型,fontSize用于设置字体大小,backgroundColor用于设置背景色,enabled用于控制是否可点击。

Button组件支持多种类型,包括ButtonType.Normal普通按钮、ButtonType.Capsule胶囊按钮、ButtonType.Circle圆形按钮等。

Button组件通过onClick事件监听器响应用户点击。

TypeScript 复制代码
Button('点击我')
  .type(ButtonType.Capsule)
  .fontSize(16)
  .backgroundColor(Color.Blue)
  .onClick(() => {
    // 处理点击事件
  })

3.3 Image组件

Image组件用于显示图片,支持本地图片和网络图片两种来源。

Image组件的核心属性包括width和height用于设置图片尺寸,borderRadius用于设置圆角,objectFit用于设置图片缩放模式。

本地图片通过$r('app.media.xxx')引用。网络图片需要配置网络权限,通过url字符串直接引用。

TypeScript 复制代码
// 本地图片
Image($r('app.media.icon'))
  .width(48)
  .height(48)
  .borderRadius(24)

// 网络图片
Image('https://example.com/image.jpg')
  .width(100)
  .height(100)

3.4 TextInput组件

TextInput组件用于接收用户输入的文本,是表单类应用的核心组件。

TextInput组件的核心属性包括placeholder用于设置占位文本,type用于设置输入类型,maxLength用于设置最大长度,value用于设置当前值。

TextInput组件支持多种输入类型,包括InputType.Normal普通文本、InputType.Password密码、InputType.Number数字等。

TextInput组件通过onChange事件监听输入变化,通过onSubmit事件监听提交。

TypeScript 复制代码
TextInput({ placeholder: '请输入用户名' })
  .type(InputType.Normal)
  .maxLength(20)
  .onChange((value: string) => {
    this.username = value;
  })

3.5 Slider组件

Slider组件用于选择范围内的数值,适合音量控制、进度调整等场景。

Slider组件的核心属性包括min和max用于设置范围,value用于设置当前值,step用于设置步长。

TypeScript 复制代码
Slider({
  min: 0,
  max: 100,
  value: this.currentValue,
  step: 1
})
  .onChange((value: number) => {
    this.currentValue = value;
  })

3.6 Scroll组件

Scroll组件用于实现滚动功能,当内容超出容器范围时可以滚动查看。

Scroll组件支持垂直滚动和水平滚动两种方向。可以配合List、Grid等组件实现复杂滚动列表。

TypeScript 复制代码
Scroll() {
  Column() {
    // 滚动内容
  }
}
  .scrollable(ScrollDirection.Vertical)
  .scrollBar(BarState.On)

四、容器组件与布局系统

4.1 布局系统的执行流程

当使用ArkUI进行开发时,开发者使用布局组件和基础组件来描述UI。这些描述形成一棵组件树结构。当用户进行交互时,会触发UI修改。UI更新过程由数据处理和UI更新两部分组成。

首次加载时,所有组件都需要参与页面的渲染。在页面渲染时,可以认为所有组件都需要更新。

UI更新过程涉及标记组件脏和布局。在初始加载时,所有组件经历完整阶段。当UI更新时,不需要重新创建页面上的所有组件对象,只需要更新需要更新的部分。需要更新的内容包含在脏节点数组中。

在UI线程处理过程中,脏节点首先被构建。在Build过程中,组件的属性按照组件ID顺序更新。如果属性发生变化,组件被标记为脏。如果布局属性发生变化,布局被标记为脏。布局边界被找到,子树被更新。非布局属性只影响自身属性,不会用于子树搜索。

4.2 Column和Row布局

Column和Row是最基础的两个布局容器。Column实现垂直方向的线性布局,所有子组件从上到下排列。Row实现水平方向的线性布局,所有子组件从左到右排列。

Column和Row布局支持space属性控制子组件之间的间距,支持alignItems属性控制子组件在交叉轴上的对齐方式。

TypeScript 复制代码
Column({ space: 10 }) {
  Text('第一个元素')
  Text('第二个元素')
  Text('第三个元素')
}
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)

4.3 Flex布局

Flex提供了更灵活的弹性布局能力,支持wrap折行等高级特性。

Flex布局的direction属性控制主轴方向,wrap属性控制是否换行,justifyContent控制主轴对齐方式,alignItems控制交叉轴对齐方式。

TypeScript 复制代码
Flex({
  direction: FlexDirection.Row,
  wrap: FlexWrap.Wrap,
  justifyContent: FlexAlign.SpaceBetween,
  alignItems: ItemAlign.Center
}) {
  // 子组件
}

4.4 Grid布局

Grid网格布局用于实现二维网格排列。Grid组件通过columnsTemplate和rowsTemplate定义网格结构。

TypeScript 复制代码
Grid() {
  ForEach(this.items, (item: string) => {
    GridItem() {
      Text(item)
    }
  })
}
  .columnsTemplate('1fr 1fr 1fr')
  .rowsGap(10)
  .columnsGap(10)

4.5 List布局

List列表布局用于展示垂直滚动列表,支持懒加载优化性能。

TypeScript 复制代码
List() {
  ForEach(this.items, (item: string) => {
    ListItem() {
      Text(item)
    }
  })
}
  .width('100%')
  .height('100%')

4.6 Stack布局

Stack重叠布局让子组件可以重叠排列,适合实现悬浮效果。

TypeScript 复制代码
Stack() {
  Image($r('app.media.background'))
  Text('前置文字')
    .backgroundColor(Color.White)
}
  .width('100%')
  .height(200)

4.7 布局性能优化

布局性能优化是构建高性能应用的重要环节。影响UI更新过程的一个主要因素是参与更新的节点数量。因此,在开发过程中需要尽量减少组件节点数量。

减少节点的主要方法包括移除冗余节点和扁平化布局以减少节点数量。当组件布局发生变化时,整个树重新布局的成本很高。因此,需要减少重新布局整个树所带来的开销。在某些特定场景下,只有某些组件在发生变化时需要重新布局。标记组件为脏的目的是确定布局边界内的最小影响范围,以减少重新布局整个树的成本。

通常,如果组件有固定的宽度和高度,该组件就是布局边界。内部组件布局的变化不影响布局边界外部的布局。因此,在搜索过程中只需要确定在布局边界内哪些组件布局受到影响。这可以避免在整个树结构中搜索的过程。

在开发过程中,有三种主要方法可以实现扁平化布局。RelativeContainer通过相对布局实现扁平化。绝对定位通过锚点定位实现扁平化。Grid通过二维布局实现扁平化。

4.8 布局边界的使用

当组件的宽度和高度不需要自适应时,建议在UI描述中指定组件的宽度和高度。当组件外部容器的大小发生变化时,如果组件的宽度和高度是固定的,理论上该组件不参与布局阶段的Measure过程。相应的尺寸数据存储在节点上。如果组件内容较多,性能会大幅提升,因为避免了组件的整体计算过程。

使用布局边界可以减少布局计算量。当组件更新时,布局边界可以限制子树更新的范围,减少布局计算。因此,布局边界的合理使用可以显著提升应用的布局性能。

五、状态管理机制

5.1 状态管理概述

状态管理是ArkUI声明式UI的核心机制。状态是驱动视图动态变化的数据源,视图是UI描述的结果。当状态变化时,视图自动更新。

状态管理可以分为组件状态共享和应用状态共享两个层面。组件状态共享用于组件内部和组件之间的状态传递。应用状态共享用于整个应用范围内的状态管理。

5.2 @State装饰器

@State是最基本的状态装饰器,用于组件内部状态管理。当@State装饰的变量发生变化时,UI会自动刷新。

TypeScript 复制代码
@Component
struct Counter {
  @State count: number = 0;
  
  build() {
    Column() {
      Text(`当前计数: ${this.count}`)
      Button('增加')
        .onClick(() => {
          this.count++;
        })
    }
  }
}

@State装饰的变量必须是组件内部的成员变量,不能是静态变量或全局变量。

5.3 @Prop装饰器

@Prop用于父组件向子组件传递数据,建立单向数据流。父组件的数据变化会同步到子组件,子组件的数据变化不会影响父组件。

TypeScript 复制代码
@Component
struct Child {
  @Prop count: number = 0;
  
  build() {
    Text(`子组件计数: ${this.count}`)
  }
}

@Component
struct Parent {
  @State count: number = 0;
  
  build() {
    Column() {
      Child({ count: this.count })
      Button('增加')
        .onClick(() => {
          this.count++;
        })
    }
  }
}

5.4 @Link装饰器

@Link用于双向数据绑定,父组件和子组件共享同一数据源。子组件修改数据时,父组件同步更新。

TypeScript 复制代码
@Component
struct Child {
  @Link count: number;
  
  build() {
    Button(`子组件计数: ${this.count}`)
      .onClick(() => {
        this.count++;
      })
  }
}

@Component
struct Parent {
  @State count: number = 0;
  
  build() {
    Column() {
      Child({ count: $count })
      Text(`父组件计数: ${this.count}`)
    }
  }
}

5.5 @Provide和@Consume装饰器

@Provide和@Consume用于跨层级组件通信,避免中间组件传递数据的麻烦。

TypeScript 复制代码
@Component
struct GrandParent {
  @Provide('theme') theme: string = 'dark';
  
  build() {
    Parent()
  }
}

@Component
struct Child {
  @Consume('theme') theme: string;
  
  build() {
    Text(`当前主题: ${this.theme}`)
  }
}

5.6 @Observed和@ObjectLink装饰器

@Observed和@ObjectLink用于对象级别的状态管理,当对象的属性变化时触发UI更新。

TypeScript 复制代码
@Observed
class User {
  name: string;
  age: number;
  
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

@Component
struct UserCard {
  @ObjectLink user: User;
  
  build() {
    Column() {
      Text(`姓名: ${this.user.name}`)
      Text(`年龄: ${this.user.age}`)
    }
  }
}

5.7 应用状态管理

LocalStorage用于页面级别的状态管理,适用于UIAbility内部的状态共享。

AppStorage用于应用级别的状态管理,适用于整个应用范围内的状态共享。

PersistentStorage用于持久化状态管理,数据会持久化到磁盘,应用重启后恢复。

TypeScript 复制代码
// 使用AppStorage
AppStorage.setOrCreate('theme', 'light');
let theme = AppStorage.get('theme');

// 使用PersistentStorage
PersistentStorage.persistProp('userName', '');

六、条件渲染与循环渲染

6.1 条件渲染语句

条件渲染用于根据状态控制组件的显示和隐藏。ArkUI提供了if/else语法来实现条件渲染。

TypeScript 复制代码
@Component
struct ConditionalRender {
  @State isVisible: boolean = true;
  
  build() {
    Column() {
      if (this.isVisible) {
        Text('可见的文本')
      } else {
        Text('隐藏的文本')
      }
      Button('切换')
        .onClick(() => {
          this.isVisible = !this.isVisible;
        })
    }
  }
}

条件渲染中,if分支和else分支中的组件是互斥的。当条件发生变化时,前一个分支的组件被销毁,后一个分支的组件被创建。

6.2 循环渲染语句

循环渲染用于渲染列表数据。ForEach基本使用中,需要了解要渲染的数据以及要生成的UI内容两个部分。

TypeScript 复制代码
@Entry
@Component
struct ToDoList {
  @State items: string[] = ['任务1', '任务2', '任务3'];
  
  build() {
    Column() {
      ForEach(this.items, (item: string) => {
        Text(item)
          .fontSize(20)
          .padding(10)
      }, (item: string) => item)
    }
  }
}

ForEach的参数包括要渲染的数据数组、生成UI内容的函数、以及用于生成唯一键的函数。第三个参数对于列表更新性能至关重要,可以帮助框架识别哪些项发生了变化。

6.3 LazyForEach懒加载

LazyForEach是懒加载版本的循环渲染,适合数据量大的列表场景。在滚动过程中进行数据读取和加载,LazyForEach仅渲染可视区域项,避免一次性数据加载过多。

TypeScript 复制代码
class MyDataSource implements IDataSource {
  private dataArray: string[] = [];
  
  constructor(data: string[]) {
    this.dataArray = data;
  }
  
  totalCount(): number {
    return this.dataArray.length;
  }
  
  getData(index: number): string {
    return this.dataArray[index];
  }
  
  registerDataChangeListener(listener: DataChangeListener): void {}
  unregisterDataChangeListener(listener: DataChangeListener): void {}
}

@Entry
@Component
struct LazyList {
  private dataSource: MyDataSource = new MyDataSource(
    Array.from({ length: 1000 }, (_, i) => `第${i}项`)
  );
  
  build() {
    List() {
      LazyForEach(this.dataSource, (item: string) => {
        ListItem() {
          Text(item)
            .width('100%')
            .height(50)
        }
      }, (item: string) => item)
    }
  }
}

七、自定义组件与组件复用

7.1 自定义组件的创建

自定义组件是将多个基础组件组合成一个可复用的单元。通过@Component装饰器声明自定义组件。

TypeScript 复制代码
@Component
struct CustomButton {
  @Prop text: string = '';
  @Prop onClick: () => void = () => {};
  
  build() {
    Button(this.text)
      .width('100%')
      .height(50)
      .backgroundColor(Color.Blue)
      .fontColor(Color.White)
      .borderRadius(8)
      .onClick(this.onClick)
  }
}

7.2 组件导出与导入

自定义组件可以被导出和导入,实现跨文件的组件复用。

TypeScript 复制代码
// MyComponent.ets
@Component
export struct MyComponent {
  build() {
    Text('导出的组件')
  }
}

// 在其他文件中导入
import { MyComponent } from './MyComponent';

7.3 @Styles装饰器

@Styles装饰器用于定义可复用的样式函数,减少重复的样式代码。

TypeScript 复制代码
@Styles
function commonStyle() {
  .width('100%')
  .height(50)
  .borderRadius(8)
}

@Component
struct MyComponent {
  build() {
    Column() {
      Button('按钮1')
        .commonStyle()
      Button('按钮2')
        .commonStyle()
    }
  }
}

7.4 @Extend装饰器

@Extend装饰器用于扩展系统组件的能力,添加自定义样式和属性。

TypeScript 复制代码
@Extend(Text)
function titleStyle(size: number) {
  .fontSize(size)
  .fontWeight(FontWeight.Bold)
  .fontColor(Color.Blue)
}

@Component
struct MyComponent {
  build() {
    Text('标题')
      .titleStyle(24)
  }
}

7.5 @BuilderParam装饰器

@BuilderParam用于在自定义组件中注入自定义布局内容,实现更灵活的组件定制。

TypeScript 复制代码
@Component
struct Card {
  @BuilderParam content: () => void = this.defaultContent;
  
  @Builder defaultContent() {
    Text('默认内容')
  }
  
  build() {
    Column() {
      this.content()
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(8)
  }
}

@Component
struct Parent {
  @Builder customContent() {
    Text('自定义内容')
      .fontSize(20)
      .fontColor(Color.Red)
  }
  
  build() {
    Card({ content: this.customContent })
  }
}

7.6 尾随闭包语法

当自定义组件的最后一个参数是@BuilderParam时,可以使用尾随闭包语法简化调用。

TypeScript 复制代码
Card() {
  Text('尾随闭包内容')
    .fontSize(20)
}

八、页面路由与组件导航

8.1 页面跳转

页面跳转是应用开发中的常见需求。HarmonyOS提供了多种页面跳转方式。

使用router.pushUrl进行页面跳转。

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

router.pushUrl({
  url: 'pages/DetailPage',
  params: {
    id: 123,
    name: '详情'
  }
})

使用router.replaceUrl替换当前页面。

TypeScript 复制代码
router.replaceUrl({
  url: 'pages/NewPage'
})

8.2 页面返回

页面返回是跳转的逆操作。使用router.back返回上一页。

TypeScript 复制代码
router.back();

带参数返回。

TypeScript 复制代码
router.back({
  params: {
    result: 'success'
  }
});

Navigation组件是HarmonyOS推荐的导航方案,提供了更丰富的导航能力。

TypeScript 复制代码
Navigation() {
  Column() {
    // 页面内容
  }
}
  .title('导航标题')
  .mode(NavigationMode.Stack)
  .hideToolBar(false)

8.4 路由表配置

路由表用于集中管理页面路由,支持跨模块的页面跳转。

TypeScript 复制代码
// router_map.json
{
  "home": "pages/HomePage",
  "detail": "pages/DetailPage"
}
TypeScript 复制代码
router.pushNamedRoute({
  name: 'detail',
  params: { id: 123 }
})

8.5 拦截器

拦截器可以在页面跳转前后执行自定义逻辑,用于权限验证、日志记录等场景。

TypeScript 复制代码
router.enableInterception({
  onIntercept: (options: router.RouterOptions) => {
    // 检查权限
    if (!this.hasPermission) {
      return false; // 阻止跳转
    }
    return true; // 允许跳转
  }
})

九、动画系统

9.1 属性动画

属性动画通过改变组件的属性实现平滑过渡效果。ArkUI提供了animateTo函数来实现属性动画。

TypeScript 复制代码
@State width: number = 100;
@State height: number = 100;

animateTo({
  duration: 500,
  curve: Curve.EaseOut,
  onFinish: () => {
    console.log('动画完成');
  }
}, () => {
  this.width = 200;
  this.height = 200;
})

属性动画支持多种曲线,包括Curve.Ease、Curve.EaseIn、Curve.EaseOut、Curve.EaseInOut等。

9.2 转场动画

转场动画在组件出现或消失时执行,增强页面切换的视觉体验。

TypeScript 复制代码
@State isVisible: boolean = false;

if (this.isVisible) {
  Text('Hello')
    .transition({
      type: TransitionType.Insert,
      opacity: 0,
      translate: { x: 0, y: -100 }
    })
}

转场动画支持多种动画类型,包括透明度变化、位置变化、缩放变化等。

9.3 图像帧动画

图像帧动画通过连续播放多张图片实现动画效果。

TypeScript 复制代码
ImageAnimator({
  images: [
    { src: $r('app.media.frame1'), duration: 100 },
    { src: $r('app.media.frame2'), duration: 100 },
    { src: $r('app.media.frame3'), duration: 100 }
  ],
  duration: 300,
  state: ImageAnimatorState.Playing,
  loop: true
})
  .width(100)
  .height(100)

十、手势交互与事件处理

10.1 点击事件

点击事件是最基础的交互方式,通过onClick方法处理。

TypeScript 复制代码
Button('点击我')
  .onClick(() => {
    // 处理点击
  })

10.2 触摸事件

触摸事件提供了更丰富的交互能力,包括触摸开始、触摸移动、触摸结束等阶段。

TypeScript 复制代码
Column() {
  Text('触摸区域')
}
  .width('100%')
  .height(200)
  .backgroundColor(Color.Gray)
  .onTouch((event: TouchEvent) => {
    switch (event.type) {
      case TouchType.Down:
        // 触摸开始
        break;
      case TouchType.Move:
        // 触摸移动
        break;
      case TouchType.Up:
        // 触摸结束
        break;
    }
  })

10.3 拖拽事件

拖拽事件用于实现拖拽交互,支持源端和目标端的双向通信。

TypeScript 复制代码
Column() {
  Text('可拖拽')
}
  .draggable(true)
  .onDragStart(() => {
    return { data: '拖拽数据' };
  })

10.4 手势系统

ArkUI提供了丰富的手势系统,包括长按手势、滑动手势、捏合手势等。

TypeScript 复制代码
Column() {
  Text('支持手势')
}
  .gesture(
    LongPressGesture()
      .onAction(() => {
        // 长按触发
      })
  )
  .gesture(
    PanGesture()
      .onActionStart(() => {
        // 滑动开始
      })
  )

十一、性能优化策略

11.1 减少组件节点数量

在布局阶段,组件位置和尺寸通过递归遍历所有节点来计算。如果嵌套层级过深,会产生更多中间节点。在布局计算阶段,额外的节点会导致更多计算过程,从而影响性能。

模拟测试数据显示,当Row容器嵌套10层、100层、500层和1000层时,第一帧绘制时间从3.2ms增长到32ms,Measure阶段从1.88ms增长到10.46ms,Layout阶段从0.38ms增长到10.88ms。当行数相同时,嵌套模式和并列模式的性能略有差异,但整体趋势相同。随着组件数量增加,性能呈线性恶化。

因此,在布局过程中应尽量减少节点数量,以缓解布局性能恶化。减少节点的方法包括移除冗余节点和扁平化布局。

移除冗余节点的典型场景是,Row容器中可能包含子组件也是Row容器。这种嵌套实际上是冗余的,会导致不必要的布局开销。如果视图复杂,在布局渲染过程中会产生不必要的计算。

扁平化布局是在没有冗余节点的情况下,通过切换到完全不同的布局类型来减少层级。一些高级组件如RelativeContainer和Grid可以用来在平面上展开元素。这种布局模式可以有效减少线性布局导致的嵌套深度,并优化用于描述布局的容器节点,从而减少节点数量。

11.2 使用布局边界

如果组件的宽度和高度不需要自适应,建议在UI描述中指定组件的宽度和高度。当组件外部容器的大小发生变化时,如果组件的宽度和高度是固定的,该组件不参与布局阶段的Measure过程。

对比测试显示,当容器固定宽高时,UI渲染、测量和布局的耗时都比不固定宽高时更短。

因此,建议在开发过程中,对固定尺寸的组件设置具体宽高,限制布局影响范围。优先使用无状态组件@Builder替代@Component,减少状态依赖。

11.3 使用组件复用机制

复用替代重建是重要的性能优化手段。利用组件复用机制,减少滑动过程中组件创建和布局开销,提升帧率。

在List和Grid等滚动容器中,组件复用可以显著减少滑动时的卡顿。

TypeScript 复制代码
List() {
  LazyForEach(this.dataSource, (item: string) => {
    ListItem() {
      Text(item)
    }
    .reusable(true)
  })
}

11.4 使用状态管理V2

状态管理V2相对于状态管理V1优化了更新方式,由V1的对象级观察优化为属性级观察,可以降低状态更新时带来的开销。

V2版本的状态管理更精确地追踪哪些属性发生了变化,只更新受影响的UI组件,避免不必要的重新渲染。

11.5 分帧渲染

高负载场景下,可以采用分帧渲染技术。将本来一帧内加载的数据分成多帧加载,避免单帧负载过重导致的卡顿。

分帧渲染需要开发者计算每帧中加载多少数据,操作相对复杂,仅在必要的情况下推荐使用。

十二、多端适配与一多部署

12.1 一多的设计目标

一次开发多端部署是HarmonyOS面向多终端的核心能力。一套代码工程,一次开发上架,多端按需部署。

实现一多需要解决三个基础问题:页面如何适配不同屏幕尺寸,功能如何兼容不同设备能力,工程如何组织以实现一套代码部署多种设备。

12.2 自适应布局

自适应布局是指当外部容器大小发生变化时,元素可以根据相对关系自动变化。HarmonyOS提供了七种自适应布局能力:拉伸能力、均分能力、占比能力、缩放能力、延伸能力、隐藏能力、折行能力。

TypeScript 复制代码
Row() {
  Text('标题')
    .flexGrow(1) // 拉伸能力
  Text('内容')
    .flexShrink(0) // 不收缩
}

12.3 响应式布局

响应式布局是指元素可以根据断点、媒体查询或栅格布局自动变化。断点将窗口宽度划分为不同范围,当断点改变时调整页面布局。

TypeScript 复制代码
@Entry
@Component
struct ResponsivePage {
  @State currentBreakpoint: string = 'sm';
  
  build() {
    Column() {
      if (this.currentBreakpoint === 'sm') {
        // 小屏布局
      } else if (this.currentBreakpoint === 'md') {
        // 中屏布局
      } else {
        // 大屏布局
      }
    }
  }
}

12.4 媒体查询

媒体查询支持监听窗口宽度、横竖屏、深浅色等多种媒体特征。

TypeScript 复制代码
let mediaQuery = matchMedia('(max-width: 600px)');
mediaQuery.on('change', (result: MediaQueryResult) => {
  if (result.matches) {
    // 小屏设备
  } else {
    // 大屏设备
  }
});

12.5 系统能力兼容

HarmonyOS定义了系统能力的概念,每个系统能力对应多个API。开发者可以通过canIUse API判断设备是否支持特定系统能力。

TypeScript 复制代码
if (canIUse('SystemCapability.XXXX')) {
  // 设备支持该能力
} else {
  // 设备不支持,提供降级方案
}

12.6 资源限定符

资源限定符允许为不同设备配置不同的资源,实现自动适配。

复制代码
resources/
├── base/
│   └── element/
│       └── string.json
├── en_US/
│   └── element/
│       └── string.json
└── zh_CN/
    └── element/
        └── string.json

总结

本文系统讲解了ArkUI框架与声明式UI开发的完整知识体系。从ArkUI框架概述到声明式UI基本语法,从基础组件详解到容器组件与布局系统,从状态管理机制到条件渲染与循环渲染,从自定义组件与组件复用到页面路由与组件导航,从动画系统到手势交互与事件处理,从性能优化策略到多端适配与一多部署,本卷覆盖了HarmonyOS UI开发的各个方面。

通过本文的学习,希望你能够理解ArkUI框架的架构设计和运行原理,掌握声明式UI的基本语法和编程范式,熟练使用各种基础组件和容器组件构建UI界面,理解状态管理机制并能够在实际开发中正确使用各种状态装饰器,掌握条件渲染和循环渲染的使用方法,能够创建和复用自定义组件,掌握页面路由和组件导航的实现方法,了解动画系统和手势交互的基本用法,掌握性能优化的基本策略,理解多端适配和一多部署的实现原理。

ArkUI是HarmonyOS应用开发的核心框架,掌握ArkUI是开发高质量鸿蒙应用的基础。第四篇将深入讲解一次开发多端部署的完整技术方案,包括界面级一多、功能级一多、工程级一多的具体实现方法。