【口算王|16】HarmonyOS ArkTS 多设备布局实战:适配手机、平板和 PC/2in1 的窗口变化

证据边界:本文依据 D:/huawei/one16-11 当前可读取源码整理;本轮未执行构建、模拟器、真机、折叠展开、旋转、分屏、自由窗口、平板或 PC/2in1 验证。验证矩阵和修复路径属于建议实现,不代表多设备验收已通过。

多设备布局适配封面

多设备适配不是把手机页面等比例拉宽。手机竖屏适合单列内容和底部导航,平板横屏更需要网格与双栏,PC/2in1 还会频繁改变窗口尺寸。如果只依据设备名称决定布局,分屏、小窗、折叠屏展开和桌面拖拽都可能落入错误形态。

口算王已经建立两类窗口信号:BreakpointSystem 用 MediaQuery 把窗口划分为 sm/md/lg,页面再通过 onAreaChange() 记录实际内容宽度。首页、题库列表、挑战页、题库详情和学习统计会在 lg 且局部宽度不小于 700 时切换宽屏结构;分类页则独立按 720 宽度调整卡片比例。源码中也有一个真实缺口:Index 的条件把 sm/md/lg 全部判进底部导航分支,导致已编写的侧边导航 else 永远无法到达。

本文基于口算王项目 D:\huawei\one16-11 的真实源码,复核 EntryAbility.etsBreakpointSystem.etsIndex.etsHomePage.etsBankListPage.etsBankDetailPage.etsCategoryPage.etsExamTab.etsExamResultPage.etsLearningStatsPage.ets。包名 com.jiaweikang.one16 是本文草稿核验使用的唯一标记。项目目标和兼容 SDK 均为 HarmonyOS 6.0 系列,本文讨论的方法适用于 HarmonyOS 5.0 及以上 ArkUI Stage 模型。

本文将完成六件事:

  • 拆解全局断点与局部容器宽度的分工;
  • 验证断点注册、更新和释放的完整生命周期;
  • 分析手机列表、宽屏网格和详情双栏的真实切换;
  • 说明底部安全区如何避免系统手势区遮挡;
  • 找出侧边导航不可达、md 利用不足等当前缺口;
  • 给出手机、平板和 PC/2in1 的可执行验证矩阵。

一、多设备适配首先是窗口问题

HarmonyOS 同一应用可能运行在手机、折叠屏、平板和 2in1 上。即便是同一台设备,旋转、分屏和自由窗口也会改变可用空间。因此布局决策应回答两个问题:

  1. 当前窗口属于哪个宽度档位;
  2. 当前组件实际拿到了多少内容宽度。

口算王没有直接判断设备型号,而是用媒体查询和组件测宽组合。这比“平板就三列”更稳健,因为平板分屏后可能只剩手机宽度,2in1 小窗也不应强行显示三列。

信号范围用途
smwidth <= 600vp手机或窄窗
md600vp < width <= 840vp折叠展开或小平板
lgwidth > 840vp大平板或 2in1
pageWidth页面实际 Area 宽度防止内容区被侧栏等再次压缩

二、BreakpointSystem 集中管理全局窗口档位

BreakpointSystem 位于基础能力库 libraryb,定义了严格的联合类型:

export type BreakpointType = 'sm' | 'md' | 'lg'

注册时创建三个 MediaQuery 监听器:

matchMediaSync('(width<=600vp)')
matchMediaSync('(600vp<width<=840vp)')

matchMediaSync('(840vp<width)')

任何监听结果变为匹配时,系统调用 update(),把当前断点写入:

AppStorage.setOrCreate<string>('currentBreakpoint', bp)

页面通过 @StorageLink('currentBreakpoint') 接收变化,因此窗口跨越阈值时,所有消费页面可以重新计算布局。

三、初始判断不能等待第一次 change

只监听 change 事件还不够。应用首次启动时,窗口可能已经处于平板宽度,如果等待下一次窗口变化,页面会一直保留默认 sm

当前 register 在绑定监听后立即检查:

if (smListener.matches) {
  update('sm')

} else if (mdListener.matches) {

  update('md')
} else {

update('lg')

}

这样首帧前就能获得正确断点。三个区间彼此覆盖且不重叠,合法窗口总能落入一个分支。

四、监听生命周期与 UIAbility 对齐

EntryAbility.onCreate() 调用 BreakpointSystem.register()onDestroy() 调用 unregister()。这使监听器生命周期跟随应用能力,而不是由某个页面临时维护。

onCreate(): void {
  BreakpointSystem.register()

}

onDestroy(): void {
  BreakpointSystem.unregister()

}

集中注册的好处是页面不重复创建 MediaQueryListener。页面只关心断点值,不关心监听器如何产生。

当前 unregister 对三个 listener 调用 off('change'),但没有把静态引用重置为 null。正常单次能力生命周期不会直接造成页面问题;若未来存在重复 register,需要进一步验证是否会产生重复监听。增强方案可以增加幂等保护,并在释放后清空引用。

五、为什么还需要 onAreaChange

全局窗口宽度不等于页面内容宽度。根布局可能增加侧栏、内边距或分栏,导致子页面真正可用空间更小。

HomePage、BankListPage、ExamTab、BankDetailPage 和 LearningStatsPage 都维护:

@State pageWidth: number = 360

.onAreaChange((oldArea: Area, newArea: Area) => {

  const width = Number(newArea.width)
  if (width > 0) this.pageWidth = width

})

它们进入宽屏布局的条件不是只看 lg

return this.currentBp === 'lg' && this.pageWidth >= 700

这是双重门槛:

  • lg 确认整个窗口处于宽屏档;
  • pageWidth >= 700 确认当前页面确实拥有足够内容宽度。

六、首页在宽屏下把题库列表变为三列

HomePage 的题库区域在普通布局中顺序展示卡片,在宽屏条件成立时改用可换行 Flex,每个题库容器宽度为 32%

Flex({
  wrap: FlexWrap.Wrap,

justifyContent: FlexAlign.SpaceBetween

}) {
  ForEach(BANKS, (bank: Bank) => {

Column() {

      BankCard({ bank: bank })
    }

.width('32%')

  })
}

三列不是固定像素,因此窗口继续变宽时卡片会随容器增长。SpaceBetween 负责分配列间空隙,最后一行也能保持稳定起点。

在窄窗中继续使用纵向结构,避免三列卡片被压到无法阅读。

七、题库列表和挑战页复用同一宽屏规则

BankListPage 与 ExamTab 同样使用 lg + pageWidth >= 700,并把卡片宽度设为 32%。这种一致规则减少同一应用不同主 Tab 在窗口变化时的视觉跳跃。

复用的不是一个抽象组件,而是一致的决策契约:

全局断点是 lg
  AND

局部内容宽度 >= 700

  -> 三列网格
否则

-> 单列布局

如果后续调整阈值,最好把规则集中为共享工具,避免三个页面分别修改后产生差异。

八、题库详情不是加列,而是重新组织阅读顺序

BankDetailPage 的宽屏适配更有价值。手机上内容按顺序排列:

  1. 题库封面;
  2. 学习进度;
  3. 题库简介;
  4. 训练重点;
  5. 章节列表。

宽屏时改为左右两块独立 Scroll:

  • 左侧 38%:封面、学习摘要、简介;
  • 右侧剩余空间:训练重点与章节;
  • 两列间距 20;
  • 两边可以独立滚动。
Row({ space: 20 }) {
  Scroll() { /* hero + summary + profile */ }

.width('38%')

  Scroll() { /* focus + chapters */ }
    .layoutWeight(1)

}

这不是把手机长页机械切半,而是按信息职责重排:左侧稳定展示题库身份,右侧承载主要操作内容。

九、宽屏 Hero 高度也随布局调整

题库详情 Hero 在宽屏下高度为 260,普通布局为 210。图片使用 ImageFit.Cover,同时保持圆角和底部渐变层。

高度变化服务于更宽卡片的视觉比例,而不是简单放大所有元素。标题仍限制单行省略,副标题限制两行,避免长文本覆盖标签。

这说明自适应不仅是列数,还包括媒体比例、文本行数和信息密度。

测试这一块时还要检查封面主体是否在 Cover 裁切后仍位于可见区域。手机与宽屏使用同一资源,但容器宽高比不同;如果关键内容靠近图片边缘,260 高度并不能自动保证信息完整。当前代码保证了比例填充和文字可读,具体裁切效果仍需要用真实题库封面逐张目视确认。

十、学习统计同时改变指标与列表结构

LearningStatsPage 在宽屏下做了两处重排:

  • 概览指标由手机上的两行两列变为一行四项;
  • 各年级进度由单列列表变为三列卡片。

同一个 useGridLayout() 控制两处变化,保证页面密度同步提升。手机仍保留更大的单项宽度,避免数字、标签和进度条互相挤压。

DataSummary 的三个统计卡始终使用 layoutWeight(1),能够按剩余空间均分;文字使用 maxLines(1) 与省略,降低窄屏溢出风险。

十一、分类页按局部宽度独立适配

CategoryPage 没有链接全局断点,而是直接依据 pageWidth

private itemWidth(): string {
  return this.pageWidth >= 720 ? '31.5%' : '47.8%'

}

小于 720 时每行两张卡,大于等于 720 时每行三张卡。Flex 使用 Wrap 和 SpaceBetween。

这种写法适合简单局部网格,但阈值与全局断点体系并不完全一致。例如窗口处于 md 的 720 至 840 区间时,分类页已经三列,而其他页面仍保持非宽屏布局。它不是运行错误,却会带来中等窗口下的信息密度不一致。

十二、ExamResultPage 依赖流式与滚动适配

挑战结果页没有读取 currentBreakpoint,也没有显式宽屏分栏。它通过以下方式保证窄屏可达:

  • 根容器 100% 宽高;
  • 主内容放在 Scroll;
  • 答题格使用可换行 Flex;
  • 三个指标通过 layoutWeight 均分;
  • 底部操作栏保留安全区;
  • 长内容不会把按钮永久推出屏幕。

这属于“弹性布局”,不是“断点重排”。在手机上有效,在宽屏上则可能出现内容过宽、阅读行长过大。后续增强可以为结果页增加最大内容宽度或左右分栏,但不能声称当前已实现。

十三、安全区是多设备适配的一部分

多设备页面除了宽度,还要避免系统导航条和手势区遮挡。EntryAbility 获取:

  • TYPE_NAVIGATION_INDICATOR
  • TYPE_SYSTEM

底部取导航指示区和系统底部区域的较大值,写入 navigationIndicatorHeightPx。页面再转换为 vp:

return Math.max(
  Sizes.BOTTOM_NAV_MIN_PADDING,

this.getUIContext().px2vp(this.navigationIndicatorHeightPx)

)

这样底部按钮、空白区和导航栏至少保留项目定义的最小间距,同时尊重设备真实避让区。

十四、避免区监听会随窗口变化更新

EntryAbility 在 WindowStage 创建时取得主窗口,首次调用 updateNavigationIndicatorHeight(),随后监听 avoidAreaChange

当系统导航模式、窗口状态或设备形态改变时,回调重新计算顶部和底部区域。onDestroy 和 onWindowStageDestroy 都会按当前引用解除监听。

异常路径会把高度回退到 0 并记录非敏感警告。页面自己的 Math.max() 仍提供最小底部间距,因此获取系统区域失败时不会完全贴底。

十五、Index 的侧边导航当前实际不可达

Index 已编写两套主导航:

  • BottomNavItem:手机底部五 Tab;
  • SideNavItem:平板或折叠屏侧栏。

但 build 中的判断是:

if (currentBp === 'sm' ||
    currentBp === 'md' ||

currentBp === 'lg') {

  // 底部导航
} else {

// 侧边导航

}

BreakpointType 的所有合法值只有 sm、md、lg,因此 if 永远为真,else 永远不可达。也就是说:

  • 宽屏页面内部的三列和双栏会生效;
  • 根导航仍会保持底部 Tab;
  • 侧边栏代码存在,但当前不会显示。

这一区分必须写清,不能把“代码中有 SideNavItem”描述成“平板已使用侧边栏”。

十六、最小修正是明确导航切换阈值

若产品目标是 lg 使用侧边栏,最小修正为:

if (this.currentBp !== 'lg') {
  this.PhoneLayout()

} else {

  this.WideLayout()
}

但是否让 md 也进入侧栏需要结合折叠屏体验决定。600 至 840vp 的空间未必适合 80vp 侧栏,保留底部导航通常更稳妥。

修正后还要测试内容页的实际宽度。加入 80vp 侧栏后,原本大于 840vp 的窗口内容区可能接近 760vp,仍可通过页面自身 pageWidth >= 700 校验决定是否三列。

十七、md 断点目前主要承担“保持手机结构”

当前多数宽屏页面只在 lg 时切换。md 虽然被识别并广播,但主要继续使用单列或底部导航。

这并非完全无用:md 让系统知道窗口已经离开手机窄屏,只是页面尚未针对它增加专门结构。后续可以按真实测试决定:

  • 首页在 md 使用两列题库;
  • 详情页保持单列但扩大左右边距;
  • 分类页统一为两列或三列策略;
  • 主导航继续底部;
  • 控制最大内容宽度,避免单列无限拉伸。

这些属于增强方向,当前源码没有统一 md 专属布局。

十八、断点与局部阈值需要命名统一

目前出现两组阈值:

  • BreakpointSystem:600、840;
  • 页面局部:700、720。

这种组合有合理性,但魔法数字分散会增加维护成本。建议集中为语义常量:

const BREAKPOINT_MD = 600
const BREAKPOINT_LG = 840

const GRID_MIN_CONTENT_WIDTH = 700

const CATEGORY_THREE_COLUMN_WIDTH = 720

更进一步,可让组件根据最小卡片宽度和间距计算列数,而不是为每个页面手写百分比。

十九、百分比网格要验证极端宽度

三列卡片使用 32% 或 31.5%,配合 SpaceBetween。在常规平板宽度下简单有效,但在非常宽的 2in1 窗口中,卡片可能过宽,文本行长和图片比例也会变化。

可增加最大内容宽度:

Column() {
  this.Content()

}

.constraintSize({ maxWidth: 1280 })
.width('100%')

也可以让外层居中,避免内容无限铺满。当前源码没有统一 maxWidth,发布前需要在大窗口检查视觉密度。

二十、触控与 2in1 输入方式不能只看尺寸

PC/2in1 除了大屏,还可能使用鼠标和键盘。现有卡片主要通过 onClick() 操作,触控目标尺寸总体明确,但源码中没有系统化的键盘焦点、方向键导航、Hover 状态和快捷键设计。

因此“适配 PC/2in1”在当前项目中主要指窗口布局重排和安全区,不应扩大为完整桌面输入体验。后续增强应验证:

  • Tab 键焦点顺序;
  • Enter/Space 激活;
  • 鼠标 Hover 反馈;
  • 滚轮行为;
  • 小窗口拖拽时的连续重排;
  • 键盘出现后底部按钮可达。

二十一、四层职责让适配逻辑可复用

当前实现可以归纳为四层:

EntryAbility

在能力生命周期中注册断点系统、监听窗口避免区,并负责释放资源。

BreakpointSystem

把原始媒体查询结果归一为 sm、md、lg,通过 AppStorage 广播。

Page Area

页面通过 onAreaChange 获取真实内容宽度,防止仅凭全局窗口误判。

Layout Builders

页面根据条件选择单列、三列、双栏或流式结构。Builder 只负责声明 UI,不承担窗口监听细节。

二十二、窗口变化测试要连续,而不是只截三张图

多设备布局最容易在阈值附近出问题。测试不能只用 360、800、1200 三个固定宽度,还要连续拖动窗口。

建议重点观察:

  • 599 -> 601:sm 到 md;
  • 699 -> 701:局部宽屏门槛;
  • 719 -> 721:分类页两列到三列;
  • 839 -> 841:md 到 lg;
  • 加入侧栏后内容宽度是否又跌破 700;
  • 横竖屏切换是否保留当前 Tab;
  • 反复跨阈值是否出现重复监听或状态抖动。

每次切换都要检查文本、图片、按钮、滚动位置和点击区域,而不只是列数。

二十三、手机、平板和 2in1 验证矩阵

场景导航内容布局必查项
手机竖屏底部 Tab单列手势区、长文本、按钮可达
手机横屏当前仍底部 Tab多数单列垂直空间、Scroll
折叠展开/md当前仍底部 Tab多数单列,分类可能三列密度一致性
大平板/lg当前仍底部 Tab三列/双栏已生效侧栏不可达问题
2in1 大窗当前仍底部 Tab三列/双栏最大宽度、鼠标键盘
2in1 小窗随断点回落单列连续拖拽稳定性
系统导航变化同上同上底部避让高度更新

二十四、性能关注布局重建而不是单次判断

MediaQuery 变化频率不高,currentBp === 'lg' 的判断成本可以忽略。真正需要关注的是:

  • onAreaChange 连续触发;
  • 大列表跨阈值时整体重建;
  • 图片在三列与单列间重新布局;
  • 两个独立 Scroll 的状态;
  • 统计方法在每次 build 中重复计算。

可以在调试环境记录断点、局部宽度、布局模式与重建耗时,不要输出用户学习数据。若窗口拖拽出现卡顿,再考虑对宽度状态做去重或只在布局档位变化时更新。

二十五、发布审核前的多设备检查

HarmonyOS 应用上架前应把布局适配视为稳定性门槛:

  • 所选设备类型与 AGC 材料一致;
  • 折叠、旋转、分屏和缩放时不截断;
  • 图片保持比例,不拉伸;
  • 文字有 maxLines、ellipsis 或足够高度;
  • 底部操作不进入系统手势区;
  • 弹窗和长页面最后一项可滚动到;
  • 状态栏和导航栏内容可读;
  • 宽屏不会出现无法利用的大块空白;
  • 手机与宽屏返回路径一致;
  • 修正 Index 后侧栏切换不丢失当前 Tab。

二十六、常见问题排查表

症状真实可能原因检查位置
平板仍显示底部导航Index 条件覆盖 sm/md/lg根布局 if/else
窗口已很宽仍单列未同时满足 lg 与 pageWidth>=700useGridLayout
分类页与其他页密度不同独立使用 720 阈值itemWidth
首次启动断点不对没有初始 matches 判断register
反复进入产生多次回调register 缺少幂等保护listener 生命周期
底部按钮被手势区挡住未消费 avoid areabottomSafePadding
大窗卡片过宽缺少 maxWidth页面内容容器
结果页宽屏过空只有弹性布局,无断点重排ExamResultPage
加侧栏后三列拥挤只看全局宽度pageWidth 二次校验
PC 键盘无法操作只完成窗口适配焦点和输入测试

二十七、总结:用真实空间驱动结构变化

口算王的多设备基础已经能够从源码完整复核:EntryAbility 注册和释放 BreakpointSystem,MediaQuery 将窗口分为 sm、md、lg,AppStorage 向页面广播断点,页面再用 onAreaChange 确认局部可用宽度。首页、题库、挑战、统计的三列网格,以及题库详情的左右双栏,都已经具备真实实现。

当前最重要的修正点同样明确:Index 的条件覆盖全部合法断点,侧边导航分支不可达。修正后还应统一 md 策略、局部阈值和最大内容宽度,并补充 2in1 的鼠标键盘体验。

多设备适配的目标不是为每种设备复制一套页面,而是建立稳定契约:

  • 全局断点描述窗口档位;
  • 局部测宽确认真实空间;
  • 布局 Builder 负责结构重排;
  • 安全区保证关键操作可达;
  • 连续窗口测试验证阈值附近行为。

做到这些,同一份 ArkUI 代码才能在手机、平板和 PC/2in1 上保持信息清楚、操作可达和窗口变化可预测。

本文部分内容由 AI 辅助整理,所有现有行为、代码片段与问题边界均依据上述本地源码复核;Index 条件修正、md 专属布局、最大内容宽度、监听幂等与桌面输入增强部分为基于当前实现的工程建议。

从窗口变化到页面重排的六步链路

多设备自适应布局的职责分层

二十四、修复不可达侧边导航时先明确产品契约

Index 已经写有侧边导航结构,但当前条件把 smmdlg 三种断点全部纳入底部导航分支,因此 else 无法执行。修复不能只把某个判断删掉,因为这会直接改变大屏用户的主导航形态。应先确认产品契约:sm 是否使用底部导航,md 在折叠展开或平板分屏时采用哪种导航,lg 是否始终显示侧栏,以及窗口从 lg 缩到 md 时如何保持当前页和返回栈。

一个较小的候选方案是仅让 smmd 进入底部导航,lg 进入侧边导航。但这只是建议,仍需检查侧栏会占用多少宽度、页面的 pageWidth 是否在扣除侧栏后重新测量,以及 700vp 的局部门槛会不会造成刚进入 lg 时仍保持单列。导航切换过程中还要验证当前 Tab、焦点、滚动位置和无障碍语义是否连续。

二十五、把窗口验证写成可重复矩阵

多设备验证不应只记录“手机正常、平板正常”。可以按宽度临界点建立矩阵,例如 599vp、600vp、601vp、839vp、840vp、841vp,再补充页面局部宽度 699vp、700vp 和 701vp。每个点检查导航形态、列数、文字截断、卡片触控区、滚动可达性和底部安全区。这样能够发现边界符号写错、全局断点与局部阈值相互打架等问题。

还应覆盖窗口连续拖拽,而不只是固定尺寸截图。连续缩放时要观察布局是否频繁抖动、监听回调是否重复注册、卡片是否短暂重叠,以及从三列退回单列后内容顺序是否仍正确。折叠屏展开、横竖屏切换和 PC/2in1 自由窗口本质上都是窗口变化,但系统栏、输入方式和安全区域不同,仍需分别验证。

本文没有执行上述设备和窗口测试,因此这些内容是可执行的验收设计,不是测试通过记录。当前能从源码确认的是断点体系、局部测宽和若干页面重排已经存在,同时侧边导航不可达、中等宽度密度不统一等问题仍待后续代码修复与运行验证。

AI 辅助声明

本文在人工复核口算王 ArkTS 源码、断点系统和页面布局后,使用 AI 辅助整理结构、润色表达并生成配图;未执行的构建、设备与窗口验证均未写成已通过。

Logo

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

更多推荐