本文基于 HarmonyOS 6.1.0(Release) 官方文档与 MultiWeather Codelab 整理,以"跟着官方示例实操 + V哥分析出的易错点"方式呈现。代码为说明问题自写的完整示例,不是官方示例搬运;API 名称、枚举取值与版本号等事实信息均标注官方出处。文中未做任何真机实测数据编造,运行表现请以真机为准。

折叠屏与鸿蒙电脑:窗口一变布局就崩?

引子:折叠一开,界面就散了

V哥第一次把应用装到折叠屏上,没改任何一行布局代码,展开的那一下,界面直接"炸"了:卡片挤成一团,侧边栏浮在内容上面遮了一半,底部 Tab 被顶出了屏幕。

第一反应是代码写得不对。后来照着 MultiWeather 把断点接上,问题就没了。这事儿让V哥记住一句铁律——

折叠态切换出问题,八成是断点没定义,不是代码写错。

下面把这条判断拆开讲,顺便把官方给的几把"钥匙"(窗口尺寸回调、SideBarContainerTypeGridRow/GridColNavigation 模式、PC/2in1 键鼠)一条条过一遍。


一、为什么折叠态/展开态切换最容易出事

折叠屏最特殊的不是"两块屏",是窗口宽度在瞬间跳变一个量级。折叠态近似一台直板机(SM 断点),展开态往往直接跨进 MD 甚至 LG。如果你的布局只按"当前这一屏"写死,那跳变瞬间没有第二套规则兜底,散架是必然。

官方其实把这件事说得很直白:

不推荐使用折叠状态监听接口实现页面布局的响应式布局和接续,避免在窗口变化但折叠状态未改变的场景下布局未能及时调整,出现页面异常。(多设备适配屏幕差异

这句话点破一个常见误区:很多人第一反应是去监听 display.on('foldStatusChange')(折叠状态变化)。但折叠状态和窗口尺寸不是一一对应的——窗口可以缩放、分屏、旋转,而折叠状态没变。真正驱动布局的应该是窗口尺寸,也就是断点。 折叠状态监听留给悬停态、相机预览这类功能适配用,别拿它做布局主逻辑。

所以V哥的判断是:先问"V哥有没有断点",再问"V哥代码写错了没"。 九成的"一折就崩",卡在前者。

折叠态与展开态:同一套代码,两套版式


二、断点不是玄学:WidthBreakpoint + 窗口尺寸回调怎么落

HarmonyOS 横向断点按窗口宽度(vp)划分,官方推荐区间:

断点窗口宽度典型形态
XS(0, 320)穿戴 / 折叠收起
SM[320, 600)直板机
MD[600, 840)折叠展开 / 横屏手机
LG[840, 1440)平板 / 大折叠
XL[1440, +∞)电脑 / 2in1

拿到断点有两种姿势,且要配合用:主动获取 + 被动监听

  • 主动获取:UIContext.getWindowWidthBreakpoint() 返回当前 WidthBreakpoint 枚举值,在 Ability/页面初始化时调一次校准。
  • 被动监听:window.on('windowSizeChange') 在窗口尺寸变化时回调,在回调里重新取断点。官方对双折叠开合的原话是:

双折叠开合状态变化时会伴随着窗口尺寸的变化,可通过注册 on(‘windowSizeChange’) 事件监听器来捕获窗口尺寸变化,在回调函数中重新计算当前断点,通过断点变化更新 UI 布局。(多设备适配屏幕差异

下面是V哥自写的断点监听封装,把"取断点"和"监听回调"收口到一处,业务页面只认 state.widthBp

// BreakpointUtil.ets —— V哥自己的断点监听封装
import { window } from '@kit.ArkUI';
import { UIContext } from '@kit.ArkUI';

// 把官方推荐的横向断点阈值记成常量,避免散落在业务里到处写魔法数字
export const BP = {
  XS_MAX: 320,
  SM_MAX: 600,
  MD_MAX: 840,
  LG_MAX: 1440
} as const;

@Observed
export class WindowState {
  // 当前横向断点,UI 只订阅它
  public widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_SM;
}

export class BreakpointUtil {
  private uiContext: UIContext | null = null;
  public state: WindowState = new WindowState();

  // 在拿到 UIContext 后调一次:主动校准
  init(uiContext: UIContext): void {
    this.uiContext = uiContext;
    this.refresh();
  }

  // 取当前断点:官方 UIContext 提供的入口
  refresh(): void {
    if (this.uiContext === null) {
      return;
    }
    this.state.widthBp = this.uiContext.getWindowWidthBreakpoint();
  }

  // 窗口尺寸变化回调:把"重新取断点"收口到这一处
  onSizeChange: (size: window.Size) => void = (_size: window.Size) => {
    this.refresh();
  };
}

记住一个V哥的小判断:回调里只做"刷新断点",别在回调里堆业务布局逻辑。 让 ArkUI 的声明式绑定去重算 UI,你只负责喂准断点这一个状态。


三、SideBarContainerType 怎么选:Embed / Overlay / AUTO / DISPLACE

侧边栏是最常见的"一多"分水岭——窄屏要收起、宽屏要常驻。SideBarContainer 的构造参数 type 决定侧边栏"怎么摆",枚举V哥整理成一张带判断的表:

取值含义V哥的判断:什么时候用
Embed(0)侧边栏嵌入内容区旁,并列显示,会挤占内容宽度宽屏常驻首选,MD/LG/XL 下让导航一直可见
Overlay(1)侧边栏浮在内容区上,不影响内容大小窄屏 SM 临时唤出,点完就收,不抢空间
AUTO(2)尺寸 ≥ minSideBarWidth+minContentWidth 用 Embed,否则 Overlay想偷懒做响应式时直接用,省去手写判断
DISPLACE(3)并列显示且内容区超出的部分移出,展开时内容区盖蒙层26.0.0 起;需要"强打断"式聚焦场景才考虑

官方对前两个的定义是:

Embed:侧边栏嵌入到组件内,和内容区并列显示。Overlay:侧边栏浮在内容区上面,不会影响内容区的大小。(SideBarContainer

V哥的口诀是:窄屏用 Overlay(浮起来不占地),宽屏用 Embed(并排常驻更顺手)。 MultiWeather 的写法就是拿 WidthBreakpoint.WIDTH_SM 当分界:是 SM 就 Overlay,否则 Embed。你也可以直接用 AUTO,把 minSideBarWidth / minContentWidth 配好,让系统替你切。

下面这段是接在第二节 BreakpointUtil 上的页面骨架(V哥自写的命名与注释,不抄官方):

// DashboardPage.ets —— 一份代码吃下折叠屏与鸿蒙电脑
import { BreakpointUtil } from '../utils/BreakpointUtil';
import { window } from '@kit.ArkUI';

@Entry
@Component
struct DashboardPage {
  private bpUtil: BreakpointUtil = new BreakpointUtil();
  @State isSideBarOpen: boolean = true;

  aboutToAppear(): void {
    this.bpUtil.init(this.getUIContext());
    // 关键:注册窗口尺寸变化监听,断点跟着窗口走
    window.getLastWindow().then((win: window.Window) => {
      win.on('windowSizeChange', this.bpUtil.onSizeChange);
    }).catch(() => {});
  }

  aboutToDisappear(): void {
    // 注销监听,别留内存尾巴
    window.getLastWindow().then((win: window.Window) => {
      win.off('windowSizeChange', this.bpUtil.onSizeChange);
    }).catch(() => {});
  }

  build() {
    // 窄屏 SM 用悬浮,更宽用嵌入
    SideBarContainer(
      this.bpUtil.state.widthBp === WidthBreakpoint.WIDTH_SM
        ? SideBarContainerType.Overlay
        : SideBarContainerType.Embed
    ) {
      // 第一个子组件:侧边栏
      Column() { /* 导航 / 城市管理列表 */ }.width('100%')
      // 第二个子组件:内容区(栅格多列见下一节)
      this.contentArea()
    }
    .sideBarWidth(280)
    .minSideBarWidth(200)
    .minContentWidth(360)
    .showSideBar(this.isSideBarOpen)
  }

  @Builder
  contentArea() {
    // 占位:真实内容见 GridRow/GridCol 示例
    Column() {}
  }
}

四、GridRow/GridCol 栅格多列:把"信息密度"交给断点

侧边栏解决"导航在哪",栅格解决"内容铺几列"。GridRow 是容器、GridCol 是子项,两者用 columnsspan 按断点分别配置,系统自动落列数——这就是"响应式"而不是"自适应":跨断点直接换结构,不是小幅伸缩。

GridRow({
  // 不同断点不同列数:窄屏 1 列,平板 2 列,大屏 3 列,电脑 4 列
  columns: { sm: 1, md: 2, lg: 3, xl: 4 },
  breakpoints: { value: ['320vp', '600vp', '840vp', '1440vp'] }
}) {
  ForEach(this.cards, (item: Card) => {
    GridCol({ span: 1 }) {
      // 一张卡片:图文组合,跨断点只是"挤几列"变,结构不变
      this.cardItem(item)
    }
  }, (item: Card) => item.id)
}
.onBreakpointChange((bp: string) => {
  // 这里只做日志/埋点,列数已由 columns 自动接管
  console.info('current breakpoint: ' + bp);
})

V哥的判断:栅格的 span 别写死,按断点给阶梯。 窄屏让一张卡片占满整行(span 取满列数),宽屏让它只占一格,信息密度自然就上去了。真机表现请以真机实测为准——不同设备密度下观感会有出入。


五、Navigation 单双栏:Auto 模式与 600vp 那条隐线

列表进详情这类"导航 + 内容"结构,Navigation 比手写分栏省心。它有个 mode

取值含义V哥的用法
Stack单栏(导航页覆盖内容区)强制手机版
Split双栏(左右分栏)强制平板/电脑版
Auto(默认)按组件宽度自适配单/双栏多端通吃首选

官方对 Auto 的断点计算:

NavigationMode.Auto:基于组件宽度自适应单栏和双栏。Auto 模式断点计算:默认 600vp(minNavBarWidth 240vp + minContentWidth 360vp)。(Navigation

也就是说,Navigation 宽度 < 600vp 自动单栏,≥ 600vp 自动双栏。想改这条线,调 navBarWidthminContentWidth 即可,不用自己写 if/else。接法很简单:

Navigation() {
  // NavDestination 列表与详情
}
.mode(NavigationMode.Auto)   // 默认即此值,可省略
.navBarWidth(280)            // 影响断点计算中的 minNavBarWidth
.minContentWidth(360)

V哥的口诀:能 Auto 就别手写双栏切换,那条 600vp 隐线是系统帮你踩好的,自己重写反而容易在边界宽度上抖一下。


六、PC/2in1:鼠标悬停与拖拽,别把触屏界面直接搬过来

到了鸿蒙电脑 / 2in1,窗口是自由窗——用户能拖大拖小、能并排多窗。这时候除了断点,还要补两件事:

第一,悬停态(hover)。 触屏只有"按下",鼠标有"划过"这个中间态。PC 上划过列表项、按钮没有反馈,用户会觉得"死的"。官方对 PC/2in1 设备的特性明确写着:

PC/2in1 设备支持全屏或自由窗口显示应用,支持键鼠。(一次开发多端部署概览

落到代码就是 onHover 事件 + hoverEffect 属性:

Button('打开')
  .onHover((isHover: boolean) => {
    // 鼠标划过变亮,离开复原
    this.hoverBg = isHover ? '#EAF3FF' : '#FFFFFF';
  })
  .hoverEffect(HoverEffect.Scale)   // 悬浮轻微放大,桌面感就出来了
  .backgroundColor(this.hoverBg)

第二,自由窗口断点。 用户在 PC 上把一个窗口拖窄,它就该从双栏退回单栏——这条线还是靠 on('windowSizeChange') 接住。窗口尺寸回调没接住,用户一拖窗口就白屏,这正是悬崖所在。

V哥的判断:手机能跑 ≠ 电脑好用。 键鼠适配(hover、右键、快捷键、焦点环)是另一层功夫,本文只点题,不展开,避免编造未验证的实测细节。

PC/2in1:自由窗口 + 鼠标悬停态


七、上线自检清单

  • 布局是否先定义了断点?没断点的"一折就崩"优先查这里,而不是改业务代码
  • 是否在初始化时主动调 getWindowWidthBreakpoint() 校准过一次?
  • 是否注册了 window.on('windowSizeChange') 并在回调里刷新断点?
  • 页面/Ability 销毁时是否 off('windowSizeChange') 注销了监听?
  • 有没有误用 foldStatusChange 做布局主逻辑?(它只建议用于悬停态/相机等功能适配)
  • SideBarContainer 窄屏是否用了 Overlay、宽屏是否用了 Embed(或直接 AUTO)?
  • GridRow/GridColcolumns/span 是否按断点给了阶梯,而非写死?
  • Navigation 多端是否用 Auto 模式吃下 600vp 那条单双栏隐线?
  • PC/2in1 上的交互组件是否补了 onHover / hoverEffect 悬停反馈?
  • 自由窗口拖窄时,双栏能否正确退回单栏(断点回调是否真的接住)?
  • 是否只用真机/模拟器覆盖了 折叠态(SM) → 展开态(MD/LG) → 电脑(XL) 三档回归?

参考与出处

本文涉及的事实性信息(API 名称、枚举取值、版本号、官方约束)来自以下官方文档,文中的结构、代码示例、决策流程与自检清单为本人整理编写:


最后一句话:折叠屏和鸿蒙电脑最反直觉的地方在于——你以为难的是"多写几套布局",其实难的是"先承认窗口会变、再定义断点"。断点定义好了,代码反而是最省心的那部分。

Logo

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

更多推荐