我把 HarmonyOS 的折叠屏 API 翻了一遍:10 个折叠状态,你大概只处理了 3 个
起因
做折叠屏适配,我照惯例先去搜别人的做法。搜出来的文章长得都差不多:判断折叠态、配个断点、贴一段 @media,结束。
然后我打开了 SDK 的声明文件。
那些文章讲的,大概只覆盖了真实能力的三分之一。更麻烦的是,其中有几处是错的。
一、FoldStatus 是 10 个状态,不是 3 个
文件:ets/api/@ohos.display.d.ts,enum FoldStatus 在第 708 行。
网上讲折叠屏的文章,状态判断基本都写成三个分支。而 SDK 里的枚举是这样:
FOLD_STATUS_UNKNOWN = 0
FOLD_STATUS_EXPANDED = 1
FOLD_STATUS_FOLDED = 2
FOLD_STATUS_HALF_FOLDED = 3
FOLD_STATUS_EXPANDED_WITH_SECOND_EXPANDED = 11
FOLD_STATUS_FOLDED_WITH_SECOND_EXPANDED = 12
FOLD_STATUS_HALF_FOLDED_WITH_SECOND_EXPANDED = 13
FOLD_STATUS_EXPANDED_WITH_SECOND_HALF_FOLDED = 21
FOLD_STATUS_FOLDED_WITH_SECOND_HALF_FOLDED = 22
FOLD_STATUS_HALF_FOLDED_WITH_SECOND_HALF_FOLDED = 23
后六个带 _WITH_SECOND_ 的,是双折轴设备的状态,也就是三折叠。
SDK 注释交代了两件事。一,只有单折轴的设备,只可能处于 EXPANDED、FOLDED、HALF_FOLDED——所以你以前写三个分支,在单折轴设备上是对的。二,双折轴设备的铰链,在充电口朝下时,从右到左依次称为第一折轴和第二折轴。这个方向定义是所有状态命名的基准。
数字里有一套编码规则
把双折轴的语义按值排开,规律就露出来了。
- 值 1
FOLD_STATUS_EXPANDED:第一折轴全开,第二折轴折叠 - 值 2
FOLD_STATUS_FOLDED:两个折轴都折叠 - 值 3
FOLD_STATUS_HALF_FOLDED:第一折轴半折,第二折轴折叠 - 值 11
EXPANDED_WITH_SECOND_EXPANDED:两个折轴都全开 - 值 12
FOLDED_WITH_SECOND_EXPANDED:第一折轴折叠,第二折轴全开 - 值 13
HALF_FOLDED_WITH_SECOND_EXPANDED:第一折轴半折,第二折轴全开 - 值 21
EXPANDED_WITH_SECOND_HALF_FOLDED:第一折轴全开,第二折轴半折 - 值 22
FOLDED_WITH_SECOND_HALF_FOLDED:第一折轴折叠,第二折轴完全折叠(注释与命名不一致,见下文) - 值 23
HALF_FOLDED_WITH_SECOND_HALF_FOLDED:两个折轴都半折
**个位表示第一折轴,十位表示第二折轴。**个位取值 1 = 全开、2 = 折叠、3 = 半折;十位留空表示第二轴折叠,十位为 1 表示第二轴全开,十位为 2 表示第二轴半折。
拿 12 验一下:个位 2 是"第一轴折叠",十位 1 是"第二轴全开",读作"第一轴折叠、第二轴全开"——和 SDK 注释一模一样。13、21、23 也都对得上。
会真出错的地方
三屏全开的状态是 11,不是 1。
在双折轴设备上,FOLD_STATUS_EXPANDED(1)的语义是"第一折轴全开、第二折轴仍然折叠",也就是只展开两屏。真正的三屏全开是 11。
于是这段看起来很正常的代码会出问题:
if (status === display.FoldStatus.FOLD_STATUS_EXPANDED) {
// 以为是“全展开”,其实是“只展开第一轴”
} else {
// 三屏全开的 11 会掉进这里
}
反过来,FOLD_STATUS_EXPANDED_WITH_SECOND_HALF_FOLDED(21)表示第一轴全开、第二轴半折,这是三折叠特有的一种"折起一角"的形态,很多布局没考虑它。
只处理 1/2/3,至少会漏掉 11 和 21 两种展开态。
有一处注释和命名对不上
FOLD_STATUS_FOLDED_WITH_SECOND_HALF_FOLDED = 22,名字直译是"第一轴折叠、第二轴半折"。但 SDK 注释写的是"第一折轴折叠,第二折轴完全折叠"。
按上面那套编码规则,22 的十位是 2,应该对应"第二轴半折",跟名字一致、跟注释冲突。
我倾向这是文档笔误,但我手上没有三折叠真机,验不了铰链形态,所以不下结论。你要是在真机上测到这个值,以实测为准。
二、还有三个东西被忽略得更彻底
折痕区域
第 425 行:
function getCurrentFoldCreaseRegion(): FoldCreaseRegion;
FoldCreaseRegion(第 974 行)只有两个字段:
readonly displayId: number;
readonly creaseRects: Array<Rect>;
creaseRects 是数组。单折轴设备只有一条折痕,双折轴有两条——又是一处用数组形状来兼容两种设备的例子。
它的用途很实际:折痕是内容禁区,按钮压上去体验就毁了。但折痕位置会随显示模式和屏幕方向变化,写死坐标不行,得实时取。函数名里的 Current 就是在强调这点。
显示模式枚举
第 814 行 enum FoldDisplayMode:
FOLD_DISPLAY_MODE_UNKNOWN = 0
FOLD_DISPLAY_MODE_FULL = 1
FOLD_DISPLAY_MODE_MAIN = 2
FOLD_DISPLAY_MODE_SUB = 3
FOLD_DISPLAY_MODE_COORDINATION
注释区分了两类设备。内屏外屏都能当主屏的机型(大折叠、阔折叠),内屏是 FULL、外屏是 MAIN;外屏只作辅助显示的机型(小折叠),内屏是 MAIN、外屏是 SUB。
这个区分比拿屏幕宽度去猜可靠——宽度是连续量,显示模式是离散状态,不会因为分屏、悬浮窗误判。
至于 COORDINATION,注释没给出足够的行为说明,我不猜它的语义。
折展角度事件
三个监听事件里,第三个几乎没人提:
function on(type: 'foldStatusChange', callback: Callback<FoldStatus>): void; // 246 行
function on(type: 'foldAngleChange', callback: Callback<Array<number>>): void; // 283 行
function on(type: 'foldDisplayModeChange', callback: Callback<FoldDisplayMode>): void; // 397 行
foldAngleChange 的回调参数是 Array<number>,数组。注释说得很直白:双折轴设备的数组包含两个角度,第一个值是第一折轴的折叠角度,第二个值是第二折轴的。
foldStatusChange 只告诉你状态跳变了,foldAngleChange 给你实时角度。做"折到某个角度触发某件事"这类交互,后者才是唯一的入口。
三、断点:官方给的阈值就摆在那儿
这是我这次翻 SDK 收获最大的一段。
网上教鸿蒙多设备适配的文章,讲断点几乎都是同一套:自己写一个 BreakpointSystem 类,用媒体查询听宽度,再 if (width >= 840) 手撕分支。
鸿蒙有现成的断点 API,而且阈值就写在 SDK 里。
文件:ets/component/enums.d.ts,enum WidthBreakpoint 在第 4741 行。
WIDTH_XS(值 0):窗口宽度小于 320 vpWIDTH_SM(值 1):窗口宽度大于等于 320 vp,小于 600 vpWIDTH_MD(值 2):窗口宽度大于等于 600 vp,小于 840 vpWIDTH_LG(值 3):窗口宽度大于等于 840 vp,小于 1440 vpWIDTH_XL(值 4):窗口宽度大于等于 1440 vp
所以那三个口口相传的 sm/md/lg,真实边界是 600 和 840 vp,而完整档位有五个。
但这里有个前提,很少有人说: 第 4732 行的注释明确写着,这些只是典型设备的默认阈值,设备厂商可以自定义。
这句话的含义是:不要把 840 硬编码进你的布局逻辑。用枚举值判断,或者用下面这两个函数拿当前实例的断点:
// ets/api/@ohos.arkui.UIContext.d.ts
getWindowWidthBreakpoint(): WidthBreakpoint; // 5269 行
getWindowHeightBreakpoint(): HeightBreakpoint; // 5282 行
getWindowWidthBreakpoint() 是按窗口宽度的 vp 值算的。而 getWindowHeightBreakpoint() 的注释写着,它按窗口宽高比算,不是按高度值。
这一点挺关键。折叠屏展开后常见的形态是接近正方形,这时候窄高的手机布局会很难看。用宽高比判断比用高度阈值稳。高度断点的阈值同样在第 4807 行。
HEIGHT_SM(值 0):窗口宽高比小于 0.8HEIGHT_MD(值 1):窗口宽高比大于等于 0.8,小于 1.2HEIGHT_LG(值 2):窗口宽高比大于等于 1.2
窗口断点不等于容器断点
还有一个更少人知道的东西。@kit.ArkUI 的导出清单里藏着 ContainerReader 和 BreakpointOptions:
// ets/api/@ohos.arkui.components.ContainerReader.d.ts
interface BreakpointOptions {
widthBreakpoint?: WidthBreakpoint;
heightBreakpoint?: HeightBreakpoint;
}
breakpointConfig(value?: BreakpointOptions): ContainerReaderAttribute; // 159 行
UIContext 那套是窗口级的,ContainerReader 是容器级的。区别在实际项目里很要命:一多布局里左边栏占了 300 vp 之后,右侧内容区剩下的宽度和窗口宽度是两码事。你拿窗口断点去控制右侧容器,平板横屏下必错。
四、折叠 PC 的跨屏
ets/api/@ohos.window.d.ts 第 7406 行:
maximize(presentation?: MaximizePresentation, acrossDisplay?: boolean): Promise<void>;
acrossDisplay 只对具备折叠能力的 2in1 设备生效。注释说,传 true 表示窗口可以直接进入跨屏模式,并在设备半折叠状态下保持跨屏;且只支持主窗口。
折叠 PC 半折时屏幕分成上下两半,窗口怎么摆本来是产品决策,SDK 把它做成了显式参数。
五、一条给所有人提个醒
写这篇之前我搜过资料,搜到过这样的代码:
import { displayFoldStatus } from '@ohos.app.ability.common';
这是编的。
我这套 SDK 的 kits 目录里没有 SceneKit,也没有任何叫 displayFoldStatus 的导出。折叠相关能力全在 @ohos.display 里,这个模块的末尾一行(第 1703 行)写着 export default display;。
这类文章现在很多,特征很明显:接口名看着像那么回事,导入路径也顺,但一编译就报找不到模块。照着抄,先卡编译,再卡排查,时间就没了。
判断方法只有一个:打开你本地 SDK 的 .d.ts 文件看。
<DevEco 安装目录>/sdk/default/openharmony/ets/api/
<DevEco 安装目录>/sdk/default/openharmony/ets/component/
这两个目录是你手上最权威、最不会骗人的文档,离线、可搜索、和你装的 SDK 版本严格对应。网上任何二手信息都不如它。
顺带说一句,ets/component/ 这个目录容易被人忽略——断点枚举就在那儿,不在 api/ 里。
写在最后
这篇没有跑通截图,因为我手上只有模拟器,给不了铰链角度这类物理输入。这里只做一件事:把 SDK 里折叠相关的声明完整摊开。
看完你会发现,折叠屏适配的难点从来不是"判断是不是折叠屏",而是"十个状态你覆盖了几个"。线上的问题,多半出在没想到的那七个上。
另外,我把这篇文章的核对过程反过来用了一遍:写完之后我又回 SDK 对了一次原文,结果改掉了自己两处错误——一处是把 FOLD_STATUS_EXPANDED_WITH_SECOND_EXPANDED 的语义记反了,另一处是折痕函数名写成了不存在的 getLiveCreaseRegion(真名是 getCurrentFoldCreaseRegion)。
**凭记忆写 API,谁都会错。**这也算是这篇文章最想说的那句话。
更多推荐



所有评论(0)