Skip to content

Harmony 开发

作者:Atom
字数统计:7.7k 字
阅读时长:26 分钟

本文内容源于官网文档,记录在Stage模型下开发HarmonyOS应用的关键知识点,用于巩固学习

ArkTs

ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript(简称TS)生态基础上做了进一步扩展,继承了TS的所有特性,是TS的超集。

它在Typescript的基础上,拓展了声明式UI、状态管理等相应的能力,后续仍会结合应用开发和运行的需求来持续演进。

UIAbility

UIAbility组件是一种包含UI界面的应用组件, 主要用于和用户交互, 可以给应用提供绘制界面的窗口。可以把UIAbility理解为一个对象实例, 它基于Stage模型来完成绘制窗口任务, 至于按钮、表格这些都是属于UIAbility的下一层页面中的内容,UIAbility可以通过多个页面来完成一个功能模块,甚至是一个应用

注意

默认创建项目的时候是一个UIAbility, 这个UIAbility可以管控多个页面, 但是一个UIAbility足够开发一个应用。

声明周期

UIAbility的生命周期包括Create、WindowStageCreate、Foreground、Background、WindowStageDestroy、Destroy六个状态

ts
import UIAbility from '@ohos.app.ability.UIAbility';
import Window from '@ohos.window';

export default class EntryAbility extends UIAbility {
    onCreate(want, launchParam) {
        // 页面初始化
    }
    onWindowStageCreate(windowStage: Window.WindowStage) {
        // 设置WindowStage的事件订阅(获焦/失焦、可见/不可见)

        // 设置UI界面加载
        windowStage.loadContent('pages/Index', (err, data) => {
            // ...
        });
    }
    onForeground() {
        // 申请系统需要的资源,或者重新申请在onBackground中释放的资源
    }

    onBackground() {
        // 释放UI界面不可见时无用的资源,或者在此回调中执行较为耗时的操作
        // 例如状态保存等
    }
    onWindowStageDestroy() {
        // 释放UI界面资源
        // 在UIAbility实例销毁之前,则会先进入onWindowStageDestroy()回调,可以在该回调中释放UI界面资源。
        // 例如在onWindowStageDestroy()中注销获焦/失焦等WindowStage事件。
    }
    onDestroy() {
        // 系统资源的释放、数据的保存等
    }
}

Create

INFO

Create状态为在应用加载过程中,UIAbility实例创建完成时触发,系统会调用onCreate()回调。可以在该回调中进行页面初始化操作,例如变量定义资源加载等,用于后续的UI界面展示

WindowStageCreate

INFO

在onWindowStageCreate()回调中通过loadContent()方法设置应用要加载的页面并根据需要订阅WindowStage的事件(获焦/失焦、可见/不可见)。

Foreground

INFO

Foreground状态在UIAbility实例切换至前台时触发,对应于onForeground()回调。

onForeground()回调,在UIAbility的UI界面可见之前,如UIAbility切换至前台时触发。可以在onForeground()回调中申请系统需要的资源,或者重新申请在onBackground()中释放的资源。

onBackground

INFO

Background状态在UIAbility实例切换至后台时触发,对应于onBackground()回调。

WindowStageDestroy

INFO

在UIAbility实例销毁之前,则会先进入onWindowStageDestroy()回调,可以在该回调中释放UI界面资源。例如在onWindowStageDestroy()中注销获焦/失焦等WindowStage事件。

Destory

INFO

Destroy状态在UIAbility实例销毁时触发。可以在onDestroy()回调中进行系统资源的释放、数据的保存等操作。

声明式UI

容器组件包含: Column、Row、Stack、Grid、List,容器组件均支持子组件配置,可以实现相对复杂的多级嵌套

ts
@Entry
@Component
struct Index {
  public account: string = 'account' // 公有变量, 可以省略public, 表示该属性可被外界(父组件)赋值
  private password: number = '12345' // 私有变量, 只能在当前组件使用

  Column() {
    // 子组件
    Row() {
      // 括号为组件参数
      Image('test1.jpg')
        // 配置属性
        .width(100)
        .height(100)
      Button('click +1')
        // 配置事件
        .onClick(() => {
          console.info('+1 clicked!');
        })
    }
  }
}

自定义组件

在ArkUI中,UI显示的内容均为组件,由框架直接提供的称为系统组件,由开发者定义的称为自定义组件。一般而言, 将UI和部分业务逻辑封装成自定义组件是最常见的行为

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

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

常用快捷键

  • comp: 实例化一个组件结构

  • entry: 创建一个入口组件

组件结构

@Entry

  • @Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。@Entry可以接受一个可选的LocalStorage的参数。

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

@Component

  • @Component装饰器仅能装饰struct关键字声明的数据结构, struct被装饰后具备组件化的能力, 一个struct只能被一个@Component装饰

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

struct

自定义组件是基于struct实现的, 类似于Class, 但不能继承, 也不需要new

build函数

build()函数用于定义自定义组件的声明式UI描述

  • 不允许在build中声明本地变量
  • 不允许在build中的UI描述里直接使用console.info,但允许在方法或者函数里使用
  • 不允许在build中创建本地的作用域, 例如{}
  • 不允许调用没有用@Builder装饰的方法,允许系统组件的参数是TS方法的返回值
  • 不允许switch语法,如果需要使用条件判断,请使用if
  • 不允许使用表达式
ts
@Entry
@Component
export default struct ParentComponent {
  doSomeCalculations() {
  }

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

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

  build() {
    let a: number = 1; // [!code --] ❌ 不允许声明本地变量

    console.info('print debug log'); // [!code --] ❌ 不允许console.info

    { 
      ...// [!code --] ❌ 不允许本地作用域
    } 

    Column() {
      (this.aVar > 10) ? Text('...') : Image('...') // [!code --] ❌ 不允许使用表达式

      switch (expression) { // [!code --] ❌ 不允许使用switch语法
        case 1: 
          Text('case1...') 
          break; 
        default: 
          Text('case2...') 
          break; 
      } 

      this.doSomeCalculations(); // [!code --] ❌ 不能调用没有用@Builder装饰的方法

      this.doSomeRender(); // [!code ++] ✅ 可以调用

      Text(this.calcTextValue()) // [!code ++] ✅ 参数可以为调用TS方法的返回值

      Text('ArkUI message')
      HelloComponent({ message: 'Hello, World!' });
      Divider()
      HelloComponent({ message: '你好!' });
    }
  }
}

组件样式

ArkUI给自定义组件设置样式时,相当于给自定义组件套了一个不可见的容器组件,而这些样式是设置在容器组件上的,而非直接设置给自定义组件的内部组件上

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

@Entry
@Component
struct MyComponent {
  build() {
    Row() {
      // 样式是设置在MyComponent2容器上面的, 而不是设置到内部组件上面的
      MyComponent2()
        .width(200)
        .height(300)
        .backgroundColor(Color.Red)
    }
  }
}

自定义组件成员

  • 自定义组件除了必须要实现build()函数外,还可以实现其他成员函数。自定义组件的成员函数为私有的,且不建议声明成静态函数
  • 自定义组件的成员变量为私有的,且不建议声明成静态变量
  • 自定义组件的成员变量本地初始化有些是可选的,有些是必选的
ts
@Component
struct MyComponent {
  // 成员变量
  private countDownFrom: number = 0;
  private color: Color = Color.Blue;

  build() {
  }
}

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

  build() {
    Column() {
      // 创建MyComponent实例,并将创建MyComponent成员变量countDownFrom初始化为10
      // 将成员变量color初始化为this.someColor
      MyComponent({ countDownFrom: 10, color: this.someColor })
    }
  }
}

组件生命周期

页面生命周期

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

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

组件生命周期

@Component装饰的自定义组件,可以组合多个系统组件实现UI的复用,可以调用组件的生命周期

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

装饰器

装饰器是以@开头的特殊函数, 用于对原有功能等进行扩展、修改、增强等操作

@Builder

@Builder装饰的函数也称为“自定义构建函数”

  • 允许在自定义组件内定义一个或多个@Builder方法,该方法被认为是该组件的私有、特殊类型的成员函数。
  • 自定义构建函数可以在所属组件的build方法和其他自定义构建函数中调用,但不允许在组件外调用。
  • 在自定义函数体中,this指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this访问自定义组件的状态变量而不是参数传递。

语法

ts
// 定义的语法:
@Builder MyBuilderFunction(){ ... }

// 使用方式:
this.MyBuilderFunction()

如果@Builder装饰的自定义函数写在struct之外, 则被认为是全局函数, 可以在任何地方调用, 且不再需要this来读取

ts
MyBuilderFunction()

@Builder装饰的函数的调用参数可以分为两种类型:

按引用传递参数

传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新

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

按值传递参数

当传递的参数为状态变量时,状态变量的改变不会引起@Builder方法内的UI刷新

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

@BuilderParam

@BuilderParam装饰的方法只能被自定义构建函数(@Builder装饰的方法)初始化。

渲染控制

渲染控制语句包括控制组件是否显示的条件渲染语句,基于数组数据快速生成组件的循环渲染语句以及针对大数据量场景的数据懒加载语句

条件渲染

条件渲染语句用于根据条件判断是否渲染组件

ts
@Entry
@Component
struct RenderPage {
  @State isRenderHello:boolean = false
  @State isRenderWorld:boolean = false

  if(isRenderHello) {
    // 条件为真时渲染
    Text('Hello')
  } else if(isRenderWorld) {
    // 条件为真时渲染
    Text('World')
  } else {
    // 条件为假时渲染
    Text('Hello World')
  }
}

循环渲染

循环渲染语句用于根据数组数据快速生成组件

语法

ts
ForEach(
  arr: any[],
  itemGenerator: (item: any, index?: number) => void,
  keyGenerator?: (item: any, index?: number) => string
)
  • arr 需要进行循环渲染的数据源, 必须为数组类型
  • itemGenerator 用于生成每个数组元素对应的组件
  • keyGenerator 用于生成每个数组元素对应的唯一key值, 可选参数

key的作用:

ForEach在数组发生变化(修改数组元素或者向数组增加或删除元素)时,需要重新渲染组件列表,在重新渲染时,它会尽量复用原来的组件对象,而不是重新渲染整个组件列表。key的作用就是辅助ForEach完成组件对象的复用。

具体逻辑如下:

ForEach在进行初次渲染时,会使用keyGenerator为数组中的每个元素生成一个唯一的key,并将key作为组件对象的标识。当数组发生变化导致ForEach需要重新渲染时,ForEach会再次使用keyGenerator为每个元素重新生成一遍key,然后ForEach会检查新生成的key在上次渲染时是否已经存在,若存在,ForEach就会认为这个key对应的数组元素没有发生变化,那它就会直接复用这个key所对应的组件对象;若不存在,ForEach就会 认为这个key对应的元素发生了变化,或者该元素为新增元素,此时,就会为该元素重新创建一个组件对象。

开发者可以通过keyGenerator函数自定义key的生成规则。如果开发者没有定义keyGenerator函数,则系统会使用默认的key生成函数,即

ts
(item: any, index: number) => { return index + '__' + JSON.stringify(item) }

注意

使用默认的key生成函数在某些场景下会导致渲染效率低下, 此时需要通过keyGenerator函数自定义key的生成规则, 但必须要保证每个元素所生成的key是唯一的。

比如在数组的头部插入一个新的元素,这个元素会导致数组中的其他元素的索引发生变化,从而导致key的生成规则发生变化,这样就会导致ForEach无法复用原来的组件对象,而是重新渲染整个组件列表。

状态管理

在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制

管理组件状态

  • @State

用法

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

只能监听到第一层的数据变化,如果发现第二层属性变化,并不会触发UI的更新。此时使用扩展符号...,来进行深层次修改

  • @Prop

用法

@Prop装饰的变量可以和父组件建立单向同步关系,@Prop装饰的变量是可变的,但修改不会同步回父组件。只要被父组件初始化,即使子组件内部的值发生变化,父组件的改变都会同步到子组件。如果未被父组件初始化,状态就只有子组件内部控制,等同于@State。

  • @Link

用法

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

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

从API version 9开始,@Link子组件从父组件初始化@State的语法为Comp({ aLink: this.aState })。同样旧语法Comp({aLink: $aState})也支持。

  • @Provide/@Consume

用法

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

  • @Provide装饰的状态变量自动对其所有后代组件可用,即该变量被“provide”给他的后代组件。由此可见,@Provide的方便之处在于,开发者不需要多次在组件之间传递变量。
  • 后代通过使用@Consume去获取@Provide提供的变量,建立在@Provide和@Consume之间的双向数据同步,与@State/@Link不同的是,前者可以在多层级的父子组件之间传递。
  • @Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定,变量类型必须相同。
ts
// 通过相同的变量名绑定
@Provide a: number = 0;
@Consume a: number;

// 通过相同的变量别名绑定
@Provide('a') b: number = 0;
@Consume('a') c: number;
  • @Observed

用法

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

  • @ObjectLink

用法

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

管理应用状态

  • LocalStorage:页面级UI状态存储,通常用于UIAbility内、页面间的状态共享。
  • AppStorage:特殊的单例LocalStorage对象,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储;
  • PersistentStorage:持久化存储UI状态,通常和AppStorage配合使用,选择AppStorage存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同;
  • Environment:应用程序运行的设备的环境参数,环境参数会同步到AppStorage中,可以和AppStorage搭配使用。

其他状态管理

ArkTS还提供了@Watch$$来为开发者提供更多功能:

  • @Watch:用于监听状态变量的变化。
  • TS使TS

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

@Watch

以下示例是监听父组件传下来的属性的变化, 调用子组件中的方法

ts
// 案例来源: https://www.bilibili.com/video/BV1b1421f78X/?p=13

@Component
export default struct TargetList {
  @State clickIndex: number = CommonConstants.DEFAULT_CLICK_INDEX;

  build() {
    TargetListItem({ clickIndex: this.clickIndex })
  }
}
ts
@Component
export default struct TargetListItem {
  // @Link 与父组件的变量建立双向同步关系
  // @Watch 监听父组件中值的变化后执行自己的回调
  @Link  @Watch('clickIndexChangeCallback') clickIndex: number;
  @State isExpanded: boolean = false
  @State index: boolean

  indexChangeCallback() {
    if(this.clickIndex !== this.index) {
      this.isExpanded = false
    }
  }
}

路由管理

路由管理是指在应用中进行页面跳转的管理。在ArkUI中,路由管理主要通过@ohos.router模块来实现。它提供了页面间的跳转和数据传递的方式

注意路由页面栈的最大容量为32个页面。如果超过这个限制,可以调用router.clear()方法清空历史页面栈,释放内存空间

路由配置

每个新创建的页面需要配置对应的路由

手动创建

创建路径位于: src/main/resources/base/profile/main_pages.json

自动创建

在DevEco Studio编辑器中, 通过UI界面创建页面将会自动创建对应的路由。但是通过UI页面删除页面,并不会自动删除src/main/resources/base/profile/main_pages.json下的配置,此时需要手动删除

创建步骤

在目录上右击 -> New -> Page -> 输入PageName

将自动创建新页面的对应路由配置

跳转模式

Router模块提供了两种实例模式,分别是StandardSingle。这两种模式决定了目标url是否会对应多个实例。

  • Standard:标准实例模式,也是默认情况下的实例模式。每次调用该方法都会新建一个目标页,并压入栈顶。

  • Single:单实例模式。即如果目标页的url在页面栈中已经存在同url页面,则离栈顶最近的同url页面会被移动到栈顶,并重新加载;如果目标页的url在页面栈中不存在同url页面,则按照标准模式跳转。

router.pushUrl

目标页不会替换当前页,而是压入页面栈。这样可以保留当前页的状态,并且可以通过返回键或者调用router.back()方法返回到当前页。

ts
class DataModelInfo {
  age: number;
}

class DataModel {
  id: number;
  info: DataModelInfo;
}

// 在Home页面中
function onJumpClick(): void {
  router.pushUrl({
    url: 'pages/Detail', // 目标url
    params: paramsInfo // 添加params属性,传递自定义参数
  }, router.RouterMode.Standard, (err) => {
    if (err) {
      console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke pushUrl succeeded.');
  });
}
ts
class DataModelInfo {
  age: number;
}

class DataModel {
  id: number;
  info: DataModelInfo;
}

// 在Setting页面中
function onJumpClick(): void {
  // 在Home页面中
  let paramsInfo: DataModel = {
    id: 123,
    info: {
      age: 20
    }
  };

  router.pushUrl({
    url: 'pages/Theme', // 目标url
    params: paramsInfo // 添加params属性,传递自定义参数
  }, router.RouterMode.Single, (err) => {
    if (err) {
      console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke pushUrl succeeded.');
  });
}

router.back

通常需要返回到上一个页面或者指定页面,这就需要用到页面返回功能。在返回的过程中,可能需要将数据传递给目标页,这就需要用到数据传递功能。

ts
router.back();
ts
router.back({
  url: 'pages/Home'
});
ts
router.back({
  url: 'pages/Home',
  params: {
    info: '来自Home页'
  }
});

router.replaceUrl

目标页会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页。

ts
// 在Login页面中
function onJumpClick(): void {
  router.replaceUrl({
    url: 'pages/Profile' // 目标url
  }, router.RouterMode.Standard, (err) => {
    if (err) {
      console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke replaceUrl succeeded.');
  })
}
ts
// 在SearchResult页面中
function onJumpClick(): void {
  router.replaceUrl({
    url: 'pages/SearchDetail' // 目标url
  }, router.RouterMode.Single, (err) => {
    if (err) {
      console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke replaceUrl succeeded.');})
}

路由离开守卫

如果想要在目标界面开启页面返回询问框,需要在调用router.back()方法之前,通过调用router.showAlertBeforeBackPage()方法设置返回询问框的信息。

例如,在支付页面中定义一个返回按钮的点击事件处理函数:

ts
// 定义一个返回按钮的点击事件处理函数
function onBackClick(): void {
  // 调用router.showAlertBeforeBackPage()方法,设置返回询问框的信息
  try {
    router.showAlertBeforeBackPage({
      message: '您还没有完成支付,确定要返回吗?' // 设置询问框的内容
    });
  } catch (err) {
    console.error(`Invoke showAlertBeforeBackPage failed, code is ${err.code}, message is ${err.message}`);
  }

  // 调用router.back()方法,返回上一个页面
  router.back();
}

常用组件

本段落以几个常用的组件为切入点,分析组件的使用方式及参数类型

图片组件

Image为图片组件,常用于在应用中显示图片。Image支持加载stringPixelMapResource类型的数据源,支持pngjpgbmpsvggif类型的图片格式。

参数类型

Image组件的参数类型为string | Resource | media.PixelMap

  • string类型: 用于通过路径的方式引用图片, 包括本地图片和网络图片
ts
// 本地图片
Image('images/demo.jpg')
// 网络图片
Image('https://xxx.jpg')
  • Resource类型: Resource类型的参数用于引入resources目录下的图片。 而resources目录下,用于统一存放应用所需的各种资源, 包括图片、音频、视频、文本等等

resources/base目录、resources/en_US目录、resources/zh_CN目录

resources/baseresources/en_USresources/zh_CN三种目录下,可存在于多种版本以适配不同的环境。例如语言环境(zh_CNen_US)、系统主题(darklight)、设备类型(phonetablet)等等。我们可以为上述每种环境各自准备一套资源文件,每种环境对应resources下的一个目录,例如上述的zh_CNen_US。我们在使用resources下的资源时,无需指定具体的环境版本,系统会根据设备所处的环境自动选择匹配的版本,例如当设备系统语言为中文时,则会使用zh_CN目录下的资源,为英文时,则会使用en_US目录下的资源。若没有与当前所处环境相对应的版本,则使用base目录下资源。

每种语言环境对应的子目录存储的具体资源如下

  • media: 存放媒体资源,包括图片、音频、视频等文件, 不能创建子目录, 不需要携带后缀, 需要使用$r('app.media.<filename>')的方式引用。
  • element: 存放用于描述页面元素的尺寸、颜色、样式等的各种类型的值,每种类型的值都定义在一个相应的JSON文件中。
  • profile: 存放自定义配置文件。

例如图片组件中引用Image($r('app.media.img')), $r()返回值即为Resource类型, 因此可以直接将$r('app.media.img')作为Image组件的参数

resources/rawfile目录

该目录下的资源,可通过$rawfile('path/to/your/file')的方式引用,文件的路径为相对于rawfile的路径。例如路径rawfile/icon.png的图片,须使 用$rawfile('icon.png')引用。需要注意的是,$rawfile()的返回值也是Resource类型,因此其也可以直接作为Image组件的参数,如Image($rawfile('icon.png'))

注意: 该目录下支持创建子目录, 且需要带上文件后缀

  • media.PixelMap: PixelMap指的是图片的像素位图,其通常是一个二维数组,数组中的每个元素对应着图片中的一个像素,其包含了该像素的颜色等信息。像素位图主要用于图片编辑的场景

部分属性

图片尺寸可通过width()方法和height()方法进行设置。以下两个属性的方法可接受的参数类型均为string | number | Resource

ts
Image($r('app.media.img')).width(100).height(100)
  • string类型: string类型的参数可为百分比, 例如'100%'或者具体尺寸, 例如'100px'

尺寸单位

  • px(Pixel)

像素是指屏幕上的一个点,是显示器上最小的显示单位。在不同的设备上,像素的大小是不同的。像素是一个相对单位,它的大小是相对于设备的屏幕而言的。

  • vp(Virtual Pixel)

为了保证一致的观感, 我们可以使用虚拟像素作为单位。虚拟像素是一种可根据屏幕像素密度灵活缩放的单位。1vp相当于像素密度为160ppi的屏幕上的一个像素。

在不同的像素密度的屏幕上, HarmonyOS会根据如下公式将虚拟像素换算为对应的物理像素

sh
px = (ppi / 160) * vp
  • number类型: 该类型的参数, 默认以vp作为单位

  • Resource类型

Resource类型参数用于引用resources下的element目录中定义的数值。引用element目录中的数值,同样需要使用$r()函数。要了解具体语法,需要先熟悉element目录下的文件内容。element目录中可保存各种类型的数值,且每种类型的值分别定义在一个JSON文件中。文件中的内容为键值对(name-value)的形式。

具体内容如下

json
{
  "string": [
    {
      "name": "module desc",
      "value": "模块描述"
    },
    {
      "name": "greeting",
      "value": "你好"
    }
  ]
}
json
{
  "integer": [
    {
      "name": "width",
      "value": 150
    },
    {
      "name": "height",
      "value": 150
    }
  ]
}

我们可以通过name引用相应的value。具体语法为$r('app.<data_type>.<name>')。例如上述的greeting的值,可以通过$r('app.string.greeting')引用,width的值可以通过$r('app.integer.width')

文本组件

文本组件用于在应用中显示文本。文本组件也通过参数类型和属性来进行设置

参数类型

Text组件的参数类型为string|Resource,下面分别对两个参数类型进行介绍:

  • string类型
ts
Text('我是一段文本')
  • Resource类型: Resource类型的参数用于引用resources/*/element目录中定义的字符串,同样需要使用$r()引用。

案例

假如resources/base/element目录中有一个string.json文件,此时我们便可通过如下方式引用并显示greeting的内容。

json
{
  "string": [
    {
      "name": "greeting",
      "value": "你好"
    }
  ]
}
ts
Text(r('app.string.greeting'))

部分属性

字体大小

字体大小可通过fontSize()方法进行设置,该方法的参数类型为string|number|Resource

  • string类型 string类型的参数可用于指定字体大小的具体单位,例如fontSize('1px'),字体大小的单位支持pxfp。其中fp(font pixel)vp类似,具体大小也会随屏幕的像素密度变化而变化。

  • number类型 number类型的参数,默认以fp作为单位

  • Resource类型 Resource类型参数用于引用resources下的element目录中定义的数值。

字体粗细

字体粗细可通过fontWeight()方法进行设置,该方法参数类型为number|FontWeight|string

  • number类型 number类型的取值范围是[10,99],取值间隔为199,默认为499,取值越大,字体越粗。

  • FontWeight类型 FontWeight为枚举类型,可选枚举值如下

名称描述
FontWeight.Lighter字体较细
FontWeight.Normal字体粗细正常
FontWeight.Regular字体粗细正常
FontWeight.Medium字体粗细适中

字体颜色

字体颜色可通过fontColor()方法进行设置,该方法参数类型为Color|string|number|Resource

  • Color类型 Color为枚举类型,其中包含了多种常用颜色,例如Color.Green

  • string类型 string类型的参数可用于设置rgb格式的颜色,具体写法可以为'rgb(6,128,8)'或者'#008000'

  • number类型 number类型的参数用于使用16进制的数字设置rgb格式的颜色,具体写法为0x008000

  • Resource类型 Resource类型的参数用于应用resources下的element目录中定义的值。

文本对齐

文本对齐方向可通过textAlign()方法进行设置,该方法的参数为枚举类型TextAlign,可选的枚举值如下

名称描述
TextAlign.Start首部对齐
TextAlign.Center居中对齐
TextAlign.End尾部对齐

Tab组件

除了使用内置的组件之外,如何构建自定义TabBar组件的样式

ts
struct MainPage {
  @State currentIndex: number = 0;
  private tabsController: TabsController = new TabsController;

  @Builder TabBuilder(
    title: string,
    index: nubmer,
    selectedImg: Resource,
    normalImg: Resource) {
      Column() {
        Image(this.currentIndex === index ? selectedImg : normalImg)
        Text(title)
          .fontColor(this.currentIndex === index ? '#1698CE' : '#6B6B6B')
      }
      .onClick(() => {
        this.currentIndex = index;
        this.tabsController.changeIndex(index);
      })
  }

  build(){
    Tabs({ barPosition: BarPosition.End, controller: this.tabController }) {
      TabContent(){
        Home()
      }
      .tabBar(this.TabBuilder('首页', 0, $r('app.media.home_selected'), $r('app.media.home normal')))

      TabContent(){
        Setting()
      }
      .tabBar(this.TabBuilder('我的', 1, $r('app.media.mine_selected'), $r('app.media.mine normal')))
    }
    .barwidth('100%')
    .barHeight(56)
    .barMode(BarMode.Fixed)
  }
}

参考资料