我读了 HarmonyOS 的断点声明:有 5 个档位,高度断点算的其实是宽高比(附 SDK 行号对照)

适用版本:HarmonyOS 7.0.0(API 26)。文中所有结论核对自本机 DevEco Studio 随附 SDK 的 .d.ts 文件,文件名和行号都写在文中。


起因

做多设备适配,我先搜了一圈别人的做法。断点这块,搜出来的文章几乎都是同一个套路:自己写一个 BreakpointSystem 类,用媒体查询监听宽度,然后 if (width >= 840) { ... } 手撕分支。

看多了会以为,鸿蒙的断点就是这么回事:三个档位,一个 840。

我把 SDK 打开翻了一下。事实不是这样。

一、官方断点有 5 个档位,不是 3 个

文件:ets/component/enums.d.ts,第 4741 行:

declare enum WidthBreakpoint {
  WIDTH_XS = 0,   // 第 4751 行
  WIDTH_SM = 1,   // 第 4761 行
  WIDTH_MD = 2,   // 第 4771 行
  WIDTH_LG = 3,   // 第 4781 行
  WIDTH_XL = 4    // 第 4791 行
}

档位和阈值对应如下。

  • WIDTH_XS(0):窗口宽度小于 320 vp
  • WIDTH_SM(1):大于等于 320 vp,小于 600 vp
  • WIDTH_MD(2):大于等于 600 vp,小于 840 vp
  • WIDTH_LG(3):大于等于 840 vp,小于 1440 vp
  • WIDTH_XL(4):大于等于 1440 vp

所以那三个口口相传的 sm、md、lg,只是中间三档。两头还各有一档:320 vp 以下的 XS,和 1440 vp 以上的 XL。

这个差别不是学术问题。手机竖屏基本都落在 SM(320 到 600 vp),折叠屏展开和平板横屏常常落在 LG(840 到 1440 vp),而平板模拟器的宽度可以到 1440 vp,正好踩在 XL 的门槛上。你要是只写了三档,XL 那条分支就是空的。

注意 WIDTH_XS 的取值是 0。用 if (!breakpoint) 这种写法判断,XS 会被当成"没取到值",这是个容易埋雷的地方。

二、高度断点算的是宽高比,不是高度

这是我觉得最容易被搞错的一处。同一个文件,第 4807 行:

declare enum HeightBreakpoint {
  HEIGHT_SM = 0,   // 第 4817 行
  HEIGHT_MD = 1,   // 第 4827 行
  HEIGHT_LG = 2    // 第 4837 行
}

只有三档。但关键在注释的措辞。三档的说明原文分别是:

  • HEIGHT_SM:The window aspect ratio is less than 0.8
  • HEIGHT_MD:The window aspect ratio is greater than or equal to 0.8 and less than 1.2
  • HEIGHT_LG:The window aspect ratio is greater than or equal to 1.2

三处写的都是 aspect ratio,宽高比,不是高度值。

第 4797 行的段落注释把这件事说得更清楚,大意是:下表列出的是典型设备的默认宽高比阈值,供基于窗口宽高比的响应式布局参考。

这个设计有它的道理。折叠屏展开之后,屏幕常常接近正方形;同一个"高度值",在 16:9 的手机和 1:1 的折叠屏上,意味着完全不同的视觉感受。用宽高比判断,判断的是"屏幕形状",比判断绝对高度更贴近布局的真实需求。

但如果你按字面理解成"高度断点就是看高度",写出来的逻辑会完全错。**它是拿宽除以高。**比如一个 1440 x 960 的窗口,宽高比是 1.5,落在 HEIGHT_LG。

三、阈值只是默认值,厂商可以改

这是我认为比档位数量更重要的一条。第 4731 到 4733 行,WidthBreakpoint 的段落注释原文:

The following table lists default width breakpoint thresholds for typical devices, serving as a reference for responsive layout design based on window width breakpoints. Device manufacturers may customize these thresholds through product-specific configurations when needed.

翻译过来:下面列出的是典型设备的默认阈值,作为参考;设备厂商在需要时可以通过产品级配置自定义这些阈值。

HeightBreakpoint 也有同样一句,在第 4798 行。

所以那三个被反复引用的数字(320、600、840),性质是参考默认值,不是恒定契约。

这句话直接否掉了两种常见写法:

  1. 硬编码 840。厂商改了阈值,你的布局逻辑就跟系统的断点判断脱节了。
  2. 把 sm/md/lg 当成跨项目通用的常量。它取决于设备实现。

正确做法是判断枚举值,而不是判断数字。

四、窗口级断点:两个函数加一个事件

ets/api/@ohos.arkui.UIContext.d.ts 第 5269 行和第 5282 行:

getWindowWidthBreakpoint(): WidthBreakpoint;
getWindowHeightBreakpoint(): HeightBreakpoint;

第 5259 行的注释说明了前者的计算依据:按窗口宽度的 vp 值。第 5272 行注释说明后者按窗口宽高比。这跟上一节是同一件事,两处互相印证。

获取变化用事件,第 2142 行:

on(type: 'windowSizeLayoutBreakpointChange',
   callback: Callback<observer.WindowSizeLayoutBreakpointInfo>): void;

回调对象在 ets/api/@ohos.arkui.observer.d.ts 第 730 行:

export class WindowSizeLayoutBreakpointInfo {
  readonly widthBreakpoint: WidthBreakpoint;    // 第 740 行
  readonly heightBreakpoint: HeightBreakpoint;  // 第 750 行
}

一个事件同时给你宽和高的断点,不用分别注册两个监听。注销用第 2158 行的 off。

五、容器级断点:API 26 才出现的那套方案

上面那些都是窗口级断点。它们回答的问题是"当前窗口有多宽"。

但在一多布局里,这个问题常常不够用。左边栏固定占掉 300 vp 之后,右侧内容区能用的宽度,跟窗口宽度是两回事。窗口 1000 vp 的平板上,右栏实际只有 700 vp。你拿窗口断点去控制右侧容器,判断必然偏大。

SDK 里有一套专门解决这个问题的 API,在 ets/api/@ohos.arkui.components.ContainerReader.d.ts:

export interface ContainerReaderInfo {
  size: Size;                                  // 第 43 行,必填
  widthBreakpoint?: WidthBreakpoint;           // 第 55 行
  heightBreakpoint?: HeightBreakpoint;         // 第 67 行
}

export interface ContainerReaderInterface {
  (value: ContainerReaderInfo): ContainerReaderAttribute;  // 第 132 行
}

export declare class ContainerReaderAttribute extends CommonMethod<ContainerReaderAttribute> {
  breakpointConfig(value?: BreakpointOptions): ContainerReaderAttribute;  // 第 159 行
}

export declare const ContainerReader: ContainerReaderInterface;            // 第 173 行
export declare const ContainerReaderInstance: ContainerReaderAttribute;    // 第 186 行

第 163 行的注释写的是它的定位:一个分析容器尺寸、为响应式布局提供断点信息的组件。

这里有个必须注意的版本门槛。这个文件里每一个声明的 @since 都是 26.0.0,包括 ContainerReaderInfo、BreakpointOptions、ContainerReaderAttribute、ContainerReader 本身。

对比一下,WidthBreakpoint 和 HeightBreakpoint 两个枚举的 @since 是 13。

也就是说:**断点枚举在 API 13 就有了,容器级断点方案是 API 26 才加进来的。**你要在低版本上用这套东西,编译期就会找不到。

ContainerReaderInfo 里的 size: Size 是必填项,注释说是"用于布局分析的目标容器尺寸",作为断点计算和布局适配的参考尺寸。也就是说尺寸由你传进去,而不是组件自己去测量。

六、容器级还能自定义阈值数组

breakpointConfig 接收的 BreakpointOptions,定义在第 80 行:

export interface BreakpointOptions {
  width?: Array<number>;    // 第 92 行
  height?: Array<number>;   // 第 104 行
}

两个字段都是数组。

第 150 行的参数注释写得很直接:

@param { BreakpointOptions } [value] - An array of breakpoint values in vp

这里允许你传入自己定义的阈值数组,而不是只能接受默认档位。

这件事和第三节是呼应的。系统默认阈值是"典型设备的参考值",厂商可以改;而当你要在自己的页面里按业务需要划定断点(例如把内容区的分界定在 500 vp 而不是 600 vp),breakpointConfig 就是官方给的自定义入口。

这是我认为整个断点体系里最值得记住的一点:阈值是可配置的,不是常量。

七、一个能自己算的例子

手边只有平板模拟器,型号规格是 2880 x 1920,320 dpi。

320 dpi 意味着 1 vp = 2 px(320 除以基准 160)。换算成 vp:

  • 宽:2880 除以 2 = 1440 vp
  • 高:1920 除以 2 = 960 vp

对照默认阈值:

  • 宽度 1440 vp,正好达到 WIDTH_XL 的下界,落在 WIDTH_XL
  • 宽高比 2880 除以 1920 = 1.5,大于等于 1.2,落在 HEIGHT_LG

所以这个平板在默认配置下,应该报 WIDTH_XL 加 HEIGHT_LG。

需要说明的是,**这是按设备规格做的换算,我没有在模拟器里跑代码验证。**以 1440 vp 正好压线这一点来看,它是个很适合验证边界行为的设备。等你手上有能跑工程的环境,把 getWindowWidthBreakpoint() 的返回值打出来,就能确认。

几处和常见说法不一样的地方

整理一下这篇里几个和常见说法不一样的地方。

  • 宽度断点有 5 档,不是 3 档,两头还有 XS 和 XL
  • 高度断点判断的是宽高比,不是高度值,而且只有 3 档
  • 阈值是默认值,厂商可自定义,不要硬编码数字
  • 容器级断点方案 ContainerReader 是 API 26 新增的,和 API 13 就存在的断点枚举不是一批东西
  • breakpointConfig 允许你传入自己的阈值数组

如果你之前写的是"自己实现一套 BreakpointSystem 监听宽度",那你实现的其实是官方已经提供、而且考虑得更细的东西。差别在于官方的这套知道"容器宽度不等于窗口宽度",也知道阈值可能被厂商改过。

判断响应式布局,先想清楚一件事:**你要判断的是窗口,还是容器?**这两个问题的答案,在 SDK 里对应两套完全不同的 API。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐