前言

在 UI 开发里,有一类需求特别常见却又特别棘手:在渲染之前,就知道一段文字会占多大的空间。比如聊天气泡要根据文字长度自适应宽度、卡片标题超过两行要显示"展开"按钮、横向标签栏要判断能塞下几个标签、Canvas 上绘制文字要预留精确的背景框……这些场景都有一个共同点——你需要在文字真正上屏之前,拿到它的宽度和高度。

声明式 UI 的组件尺寸是布局阶段才确定的,等你能读到组件的实际尺寸时,界面往往已经渲染完了,再调整就会闪一下。HarmonyOS NEXT 为此提供了 @ohos.measure 模块,让你可以在任意时刻、脱离组件树,直接"离线"计算一段文本在给定字体样式下的精确尺寸。

本文用一个可交互的"文本测量实验室"页面,把 measure 的两个核心方法讲透:实时测量文本的单行宽高、限定宽度后测量换行高度,并用一个"实际渲染对照框"验证测量值与真实渲染完全一致。全文含完整可运行代码,适合中级开发者掌握这项自适应布局的关键能力。


一、为什么需要文本测量

1.1 布局阶段的"先有鸡还是先有蛋"

设想一个聊天气泡:气泡宽度要贴合文字,文字短就窄、文字长就宽,但最宽不超过屏幕的 70%。如果直接让 Text 自适应,大多数情况没问题;可一旦你需要基于文字尺寸做决策——例如"当文字宽度不足 50vp 时用圆形气泡,否则用圆角矩形"——就必须在布局前拿到宽度。

组件的 onAreaChange 能读到尺寸,但那是渲染之后的回调,用它来反向调整布局会导致二次布局甚至闪烁。measure 把测量从渲染中解耦出来:给它文本内容和字体参数,它立刻返回尺寸,你据此提前把布局算好。

1.2 典型使用场景

  • 自适应气泡/标签:根据文字宽度决定容器尺寸与形状;
  • 文字截断预判:测量出总高度超过 N 行的高度阈值,提前决定是否显示"展开";
  • 横向排布容量计算:累加多个标签的测量宽度,判断一行能放几个、何时换行;
  • Canvas 绘制:在画布上绘制文字前,用测量值预留精确的背景矩形和内边距;
  • 动态字号适配:在固定容器里反复测量、逐步缩小字号,直到文字恰好放下。

二、measure 模块的两个核心方法

引入方式(注意 measure 从 @ohos.measure 直接默认导入,而不是从 @kit.ArkUI 具名导入):

import measure from '@ohos.measure';

易错点:很多人习惯写 import { measure } from '@kit.ArkUI',但这会报错 'measure' is not exported from Kit '@kit.ArkUI'。measure 是一个默认导出的命名空间,必须用 import measure from '@ohos.measure'

它提供两个方法:

2.1 measureText:只测宽度

const width: number = measure.measureText({
  textContent: '鸿蒙',
  fontSize: 16
});

返回单行文本的宽度(单位 px)。适合只关心宽度的简单场景。

2.2 measureTextSize:测宽 + 高

const size: SizeOptions = measure.measureTextSize({
  textContent: '鸿蒙 HarmonyOS',
  fontSize: 16,
  fontWeight: FontWeight.Bold,
  constraintWidth: 200
});
// size.width / size.height —— 单位 px

返回一个 SizeOptions,同时给出宽度和高度。这是更常用的方法,本文实验室主要用它。

2.3 MeasureOptions 参数

两个方法都接收一个配置对象(MeasureOptions),关键字段:

字段 说明
textContent 要测量的文本(必填)
fontSize 字号(数字默认按 fp)
fontWeight 字重,可用 FontWeight 枚举
fontFamily 字体族
fontStyle 正常 / 斜体
letterSpacing 字间距
lineHeight 行高
maxLines 最大行数
constraintWidth 约束宽度,超过则换行
textAlign / overflow / wordBreak 对齐、溢出、断词策略

其中 constraintWidth 是产生"换行"的关键:不传它,文本按单行测量,宽度可能很大;传了它,文本在该宽度内折行,高度随行数增加。


三、实战:文本测量实验室

页面把测量参数做成可实时调节的控件,一边调一边看数值变化,再用一个真实渲染的文本框做对照。

3.1 状态设计

import { router } from '@kit.ArkUI';
import measure from '@ohos.measure';
import { FontSize, Spacing } from '../common/Constants';

interface WeightOption {
  label: string;
  value: FontWeight;
}

@Entry
@Component
struct TextMeasureLabPage {
  @State text: string = '鸿蒙 HarmonyOS 文本测量可精确计算控件尺寸,助力自适应布局';
  @State fontSize: number = 16;
  @State weightIndex: number = 0;
  @State constraintWidth: number = 200;

  @State singleW: number = 0;   // 单行宽度
  @State singleH: number = 0;   // 单行高度
  @State wrapW: number = 0;     // 换行后宽度
  @State wrapH: number = 0;     // 换行后高度
  @State lineCount: number = 0; // 估算行数

  private weights: WeightOption[] = [
    { label: '常规', value: FontWeight.Normal },
    { label: '中等', value: FontWeight.Medium },
    { label: '加粗', value: FontWeight.Bold }
  ];
}

3.2 核心:两次测量

页面同时展示两种测量结果——不加约束的"单行尺寸"和加了约束宽度的"换行尺寸",用它们的差异直观说明 constraintWidth 的作用:

// 分别测量“无约束单行”与“限定宽度换行”两种情况
private measureAll(): void {
  const weight: FontWeight = this.weights[this.weightIndex].value;

  const single: SizeOptions = measure.measureTextSize({
    textContent: this.text,
    fontSize: this.fontSize,
    fontWeight: weight
  });
  this.singleW = Math.round(px2vp(Number(single.width)));
  this.singleH = Math.round(px2vp(Number(single.height)));

  const wrapped: SizeOptions = measure.measureTextSize({
    textContent: this.text,
    fontSize: this.fontSize,
    fontWeight: weight,
    constraintWidth: this.constraintWidth
  });
  this.wrapW = Math.round(px2vp(Number(wrapped.width)));
  this.wrapH = Math.round(px2vp(Number(wrapped.height)));

  // 用换行总高度 ÷ 单行高度估算行数
  if (this.singleH > 0) {
    this.lineCount = Math.round(this.wrapH / this.singleH);
  } else {
    this.lineCount = 0;
  }
}

这里有两个必须注意的技术细节:

其一,返回值要显式标注类型。 ArkTS 严格模式禁止隐式的 any/unknown。直接写 const single = measure.measureTextSize(...) 会报错 arkts-no-any-unknown,必须写成 const single: SizeOptions = ...

其二,measure 返回的是 px,UI 用的是 vp,要转换。 measureTextSize 返回的宽高单位是物理像素 px,而 ArkUI 布局用的是虚拟像素 vp。两者不能直接混用,必须用全局函数 px2vp() 把测量结果转成 vp,才能和界面上的尺寸对齐:

this.singleW = Math.round(px2vp(Number(single.width)));

single.width 的类型是 Length(可能是 number 或 string),用 Number() 兜底转成数字再传给 px2vp

3.3 行数估算

measure 没有直接返回行数,但可以用一个巧妙的方法推算:换行后的总高度 ÷ 单行高度 ≈ 行数。因为单行高度就是一行文字占的高度,总高度是它的整数倍(约等于),相除四舍五入即得行数。这在判断"是否需要展开按钮"时很实用——比如行数 > 2 就显示"展开全文"。

3.4 参数调节控件

三个可调参数各用一个交互控件。字号和约束宽度用滑块,复用一个 @Builder:

@Builder
sliderRow(label: string, valueText: string, value: number, min: number, max: number,
          step: number, onChange: (v: number) => void) {
  Column() {
    Row() {
      Text(label).fontColor('#78716C')
      Blank()
      Text(valueText).fontColor('#EA580C').fontWeight(FontWeight.Bold)
    }
    Slider({ value: value, min: min, max: max, step: step })
      .selectedColor('#EA580C')
      .onChange((v: number) => { onChange(v); })
  }
}

在 build 里这样使用,每次滑动都重新测量:

this.sliderRow('字号', this.fontSize + ' fp', this.fontSize, 10, 40, 1,
  (v: number) => { this.fontSize = Math.round(v); this.measureAll(); })

this.sliderRow('约束宽度', this.constraintWidth + ' vp', this.constraintWidth, 80, 340, 10,
  (v: number) => { this.constraintWidth = Math.round(v); this.measureAll(); })

字重用三个互斥的按钮切换:

Row() {
  ForEach(this.weights, (w: WeightOption, index: number) => {
    Text(w.label)
      .fontColor(this.weightIndex === index ? '#FFFFFF' : '#C2410C')
      .backgroundColor(this.weightIndex === index ? '#EA580C' : '#FFEDD5')
      .layoutWeight(1)
      .onClick(() => { this.weightIndex = index; this.measureAll(); })
  })
}

文本内容改变同样触发重新测量:

TextArea({ text: this.text })
  .onChange((v: string) => { this.text = v; this.measureAll(); })

页面在 aboutToAppear 里先测一次,保证打开即有数据:

aboutToAppear(): void {
  this.measureAll();
}

3.5 实际渲染对照

这是整个实验室最有说服力的一块:把一个真实的 Text 组件用完全相同的参数渲染出来,外面套一个虚线框,直观验证"测量值 = 实际占用"。

Row() {
  Text(this.text)
    .fontSize(this.fontSize)
    .fontWeight(this.weights[this.weightIndex].value)
    .constraintSize({ maxWidth: this.constraintWidth })
    .padding(6)
    .border({ width: 1, color: '#EA580C', style: BorderStyle.Dashed })
}

关键在于:Text 的 fontSizefontWeight 与测量时传入的参数一致,constraintSize({ maxWidth }) 对应测量的 constraintWidth。这样虚线框的实际大小,就应当等于上方 wrapW × wrapH 的测量值。调整任意参数,你会看到测量数值和虚线框同步变化,两者始终吻合——这正是 measure 可靠性的直接证明。


在这里插入图片描述
在这里插入图片描述

四、几个实战要点与坑

4.1 导入路径:@ohos.measure

再次强调:import measure from '@ohos.measure',默认导入,不是从 @kit.ArkUI 具名导入。这是最容易踩的坑。

4.2 单位:测量返回 px,布局用 vp

measureText / measureTextSize 返回的是物理像素 px,务必用 px2vp() 转成 vp 再参与布局或显示。忘记转换会让尺寸在不同 DPI 的设备上全部错位。反过来,如果你要把 vp 的约束传进测量,measure 的 constraintWidth 接收的是 vp 数值,这一侧不用转。

4.3 测量参数要与渲染参数完全一致

测量的前提是"同样的字体设置"。如果测量时用 fontSize: 16fontWeight: Bold,而实际 Text 渲染用的是 14 号常规,测量值自然对不上。凡是影响文字尺寸的属性(字号、字重、字体、字间距、行高)都要在测量与渲染两侧保持一致。

4.4 返回值显式类型

ArkTS 严格模式下,measureTextSize 的返回必须显式标注 SizeOptions,measureText 的返回标注 number,否则触发 arkts-no-any-unknown 编译错误。

4.5 频繁测量的性能

measure 是同步计算,单次开销很小,但如果在滑动、拖拽等高频回调里对超长文本反复测量,累积起来仍可能影响流畅度。高频场景可考虑防抖,或缓存"内容+参数"到"尺寸"的映射(正好可以配合 LRUCache)。


五、进阶:用测量做动态字号适配

一个很实用的组合技:在固定尺寸的容器里,让文字自动缩放到恰好放下。思路是从最大字号开始,不断测量,直到高度不超过容器:

private fitFontSize(text: string, boxWidth: number, boxHeight: number): number {
  let size = 24;
  while (size > 10) {
    const s: SizeOptions = measure.measureTextSize({
      textContent: text,
      fontSize: size,
      constraintWidth: boxWidth
    });
    if (px2vp(Number(s.height)) <= boxHeight) {
      break;
    }
    size -= 1;
  }
  return size;
}

这段逻辑常见于海报标题、名片、封面等"文字必须放进固定框"的设计场景,用 measure 就能优雅实现,而不必依赖多次渲染试错。


六、小结

本文以"文本测量实验室"为载体,系统讲解了 HarmonyOS NEXT 中 @ohos.measure 的用法:

  • 两个核心方法:measureText(只测宽度)与 measureTextSize(测宽高,返回 SizeOptions);
  • 关键参数:textContent / fontSize / fontWeight / constraintWidth,其中 constraintWidth 决定是否换行;
  • 两处必知细节:measure 从 @ohos.measure 默认导入、返回值单位是 px 需用 px2vp 转 vp;
  • 实用技巧:用"总高 ÷ 单行高"估算行数、用循环测量实现动态字号适配、测量参数须与渲染参数一致;
  • 可靠性验证:通过"实际渲染对照框"证明测量值与真实占用完全吻合。

文本测量是自适应布局的底层能力。掌握 measure,你就能在渲染之前精确掌控每一段文字的尺寸,把气泡、标签、截断、字号适配这些"看文字大小行事"的需求做得又稳又准。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐