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

多设备适配不是把手机页面的宽度改成 100% 就结束。手机适合底部主导航,平板和 PC/2in1 更适合侧边导航;分类卡片要随着可用宽度增加列数,题库详情在宽窗口中可以拆成双栏;窗口缩小时,内容必须重新回到单列,同时保持按钮、长文本和底部安全区可达。

本文基于知律项目 D:\huawei\one19-11、包名 com.jiaweikang.one19 的真实源码,重点核对 brief 指向的 Index.etsBankDetailPage.etsCategoryPage.etsExamResultPage.etsHakkaBankPage.ets,并追踪断点来源 BreakpointSystem.ets 和窗口安全区来源 EntryAbility.ets。项目面向 HarmonyOS 5.0 及以上版本,已经具备媒体查询、AppStorage 断点分发、局部宽度测量、双栏详情页和底部避让等基础,但主导航还存在一个会让宽屏分支失效的真实条件错误。

本文只描述源码可复核的布局能力,不把尚未运行的设备测试说成通过,也不把结果页硬编码的排名数字当作真实平台数据。

一、先区分设备断点和容器宽度

知律的适配有两类输入。

第一类是全局窗口断点。BreakpointSystem 使用 mediaquery.matchMediaSync 建立三段规则:

ts 复制代码
sm: width <= 600vp
md: 600vp < width <= 840vp
lg: 840vp < width

第二类是页面或组件真正拿到的容器宽度。页面通过 onAreaChange 读取 newArea.width

ts 复制代码
.onAreaChange((oldArea: Area, newArea: Area) => {
  const width = Number(newArea.width)
  if (width > 0) this.pageWidth = width
})

两者不能互相替代。窗口已经属于 lg,但主壳侧边栏、页面边距和分栏间距会继续消耗宽度;某个内容区域拿到的宽度可能没有窗口那么大。反过来,如果只看局部宽度,页面又不知道主导航应该采用底部还是侧边结构。

稳妥的职责是:

text 复制代码
全局断点:决定导航形态和页面级结构
局部宽度:决定卡片列数、内容分栏和组件细节

二、BreakpointSystem 已经建立响应式信号源

BreakpointSystem.register() 创建三个监听器:

ts 复制代码
BreakpointSystem.smListener =
  mediaquery.matchMediaSync('(width<=600vp)')
BreakpointSystem.mdListener =
  mediaquery.matchMediaSync('(600vp<width<=840vp)')
BreakpointSystem.lgListener =
  mediaquery.matchMediaSync('(840vp<width)')

每个监听器在命中时调用:

ts 复制代码
private static update(bp: BreakpointType): void {
  BreakpointSystem.currentBp = bp
  AppStorage.setOrCreate<string>('currentBreakpoint', bp)
}

EntryAbility.onCreate 注册断点系统,页面通过:

ts 复制代码
@StorageLink('currentBreakpoint') currentBp: string = 'sm'

接收变化。onDestroy 再调用 BreakpointSystem.unregister() 移除监听器,生命周期边界完整。这样做的价值是,窗口拖动或折叠状态变化时不需要每个页面各自注册三套媒体查询。

不过 currentBp 在页面中仍被声明为普通 string。更严格的写法应直接复用 BreakpointType,让拼写错误在编译期暴露:

ts 复制代码
@StorageLink('currentBreakpoint')
currentBp: BreakpointType = 'sm'

三、Index 的宽屏侧边导航现在不可达

Index 已经分别实现了 BottomNavItemSideNavItem。从组件结构看,设计意图很明确:小窗口使用底部五 Tab,大窗口使用 80vp 侧边栏和弹性内容区。

但实际判断是:

ts 复制代码
if (this.currentBp === 'sm' ||
    this.currentBp === 'md' ||
    this.currentBp === 'lg') {
  // 手机模式:底部导航
} else {
  // 平板/折叠模式:侧边导航 + 内容区
}

BreakpointType 只有 smmdlg 三种值,条件把所有合法值都包含了,所以 else 永远不会执行。即使窗口超过 840vp,应用仍会进入底部导航布局。

这不是风格偏好,而是可以直接由源码证明的逻辑问题。最小修正应先明确产品规则。例如手机与中等窗口保留底部导航、宽屏才切侧边栏:

ts 复制代码
if (this.currentBp === 'sm' || this.currentBp === 'md') {
  this.PhoneShell()
} else {
  this.WideShell()
}

如果希望折叠屏展开后的 md 也使用侧边栏,则判断应只保留 sm。关键是规则必须与注释、设计稿和测试矩阵一致。

四、导航切换后还要处理内容可用宽度

宽屏分支中的侧边栏固定为 80vp:

ts 复制代码
Column({ space: 8 }) {
  // logo 和五个 SideNavItem
}
.width(80)
.height('100%')

内容区使用:

ts 复制代码
Stack() {
  this.PageContent()
}
.layoutWeight(1)
.height('100%')

这种组合方向正确:导航拥有稳定宽度,内容区占据剩余空间。窗口从 1200vp 缩到 900vp 时,内容区不会被写死为某个桌面宽度。

但宽窗口中的内容也不宜无限拉伸。法律说明、题目正文和设置项在超宽行上可读性会下降。可以在内容区内部增加居中最大宽度:

ts 复制代码
Column() {
  this.PageContent()
}
.width('100%')
.constraintSize({ maxWidth: 1280 })

这属于改进建议,当前 Index 源码尚未设置最大内容宽度。

五、CategoryPage 展示了局部宽度驱动列数

分类页不依赖设备名称,而是测量当前页面宽度:

ts 复制代码
@State pageWidth: number = 360

private itemWidth(): string {
  return this.pageWidth >= 720 ? '24%' : '48%'
}

卡片放在可换行的 Flex 中:

ts 复制代码
Flex({
  wrap: FlexWrap.Wrap,
  justifyContent: FlexAlign.SpaceBetween
}) {
  ForEach(CATEGORIES, (cat: Category) => {
    this.CategoryCard(cat)
  })
}

小窗口一行两张,每张约 48%;达到 720vp 后一行四张,每张约 24%。由于还要给 SpaceBetween 留间距,百分比没有写成 50% 和 25%,这是一个实用细节。

不过这里只有两档。手机横屏、窄平板或 PC 小窗口可能更适合三列。可以把列数和间距集中计算,而不是不断叠加百分比条件:

ts 复制代码
private columnCount(): number {
  if (this.pageWidth >= 960) return 4
  if (this.pageWidth >= 600) return 3
  return 2
}

进一步可用 Grid 和稳定的列模板表达,减少卡片内容、百分比和 SpaceBetween 共同作用时的边缘误差。

六、BankDetailPage 同时使用全局断点与真实宽度

题库详情页的宽屏判断是:

ts 复制代码
private useWideLayout(): boolean {
  return this.currentBp === 'lg' && this.pageWidth >= 700
}

这里同时检查全局 lg 和内容区宽度。虽然 lg 对应窗口宽度超过 840vp,但如果外层已经分配侧边栏,页面自身宽度仍应再次确认。双条件可以避免"窗口很宽但页面被放进狭窄分栏"时强行套双栏。

宽屏结构把信息拆成两个独立滚动区:

ts 复制代码
Row({ space: 20 }) {
  Scroll() {
    Column({ space: 16 }) {
      this.HeroCard()
      this.BankSummaryCard()
      this.ProfileCard()
    }
  }
  .width('38%')

  Scroll() {
    Column({ space: 16 }) {
      this.FocusCard()
      this.ChapterSection()
    }
  }
  .layoutWeight(1)
}

左侧负责封面、摘要和题库介绍,右侧负责重点标签与章节列表。单列模式则按相同信息顺序放进一个 Scroll。这种"内容不变、布局重组"比维护两套业务页面更稳。

双滚动区也带来交互问题:鼠标滚轮焦点、触控滑动位置和键盘滚动可能让用户不确定当前滚动的是哪一栏。宽屏验收时要实际测试两栏滚动,而不能只看静态截图。

七、HakkaBankPage 证明详情组件可以复用

HakkaBankPage 没有复制整套详情 UI,而是包裹同一个组件:

ts 复制代码
Column() {
  BankDetailContent({ fixedBankId: 'b_hakka' })
}
.width('100%')
.height('100%')

这意味着 BankDetailContent 的宽度测量、单双栏切换和底部安全区逻辑会自然复用于固定题库入口。一个组件同时支持路由参数和固定 bankId,减少了不同设备上两个详情页面逐渐分叉的风险。

需要注意的是,复用布局不等于完成多设备验证。固定题库的标题、标签数量、章节名称长度可能与其他题库不同,仍要在窄窗口和宽窗口中检查文本截断与按钮可达。

八、ExamResultPage 目前仍是手机优先单列

结果页使用根 Column、中间 Scroll 和底部按钮栏,整体可以在小屏中滚动:

ts 复制代码
Column() {
  TopBar({ title: '考试结果' })
  Scroll() {
    Column({ space: 16 }) {
      // 分数、指标、答题网格
    }
  }
  .layoutWeight(1)

  Row({ space: 12 }) {
    // 错题解析、再考一次
  }
}

答题序号使用 FlexWrap.Wrap,窗口变窄时会自动换行,这部分具备弹性。但页面没有监听 currentBreakpoint,分数卡、三项指标和操作区在大屏仍保持手机单列并拉满宽度。

尤其要关注三个细节:

  • 分数字号固定为 56,印章固定为 80x80 并绝对定位;
  • 指标 Row 始终展示三项,极窄窗口下长用时文本可能挤压;
  • 底部两个按钮始终横排,长文本或系统字体放大时可能不足。

宽屏可以将分数摘要与答题网格并排,窄屏则保留现有单列;底部操作可根据宽度从横排切为纵排。实现前应先测真实最小窗口,而不是仅按设备类型猜测。

结果页中的:

ts 复制代码
this.MetricItem(`${236}/${1258}`, '排名')

是硬编码展示值,源码没有真实排名数据来源。多设备改造时不应把它写入验收指标或发布材料,更不能描述为真实用户排名。

九、安全区不是固定底部留白

EntryAbility 获取导航指示区和系统避让区,并把像素高度写入 AppStorage

ts 复制代码
const navigationArea =
  this.mainWindow.getWindowAvoidArea(
    window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR
  )
const systemArea =
  this.mainWindow.getWindowAvoidArea(
    window.AvoidAreaType.TYPE_SYSTEM
  )

页面再将 px 转成 vp:

ts 复制代码
private bottomSafePadding(): number {
  return Math.max(
    Sizes.BOTTOM_NAV_MIN_PADDING,
    this.getUIContext().px2vp(this.navigationIndicatorHeightPx)
  )
}

Index 用它计算底部导航总高度,BankDetailPageExamResultPage 用它抬高底部操作区。相比固定写 16vp,这种做法可以适应手势导航和不同窗口形态。

需要避免重复叠加。ExamResultPage 当前使用:

ts 复制代码
bottom: 16 + this.bottomSafePadding()

bottomSafePadding() 本身已经保证最小值。如果设计目标是"基础 16vp 加系统避让",这行合理;如果 BOTTOM_NAV_MIN_PADDING 已包含设计基础间距,就可能产生过大的底部空白。安全区工具函数的语义应统一成"纯系统 inset"或"最终设计 padding",不要在不同页面混用。

十、宽度变化应触发布局,不应重置业务状态

窗口变化时,currentBppageWidth 会更新,ArkUI 重新执行对应布局分支。业务状态如当前 Tab、题库进度、章节进度都来自 AppStorage 或页面状态,不应因为从单列切双栏而重新初始化。

这要求响应式分支满足两个约束:

  1. 两个布局分支使用同一份数据和事件方法;
  2. 不在布局分支内部执行有副作用的初始化。

BankDetailPage 的单双栏都调用 HeroCardProfileCardChapterSection 等 Builder,方向正确。Index 的两种导航也都写同一个 currentIndex。修复不可达条件后,应重点测试旋转或缩放过程中当前 Tab 是否保持、章节滚动位置是否可接受、按钮是否重复触发。

十一、建议把断点判断收口成语义方法

当前页面散落着:

ts 复制代码
this.currentBp === 'lg'
this.pageWidth >= 700
this.pageWidth >= 720

这些数字分别有上下文,但长期会形成边界不一致。可以保留全局设备断点,同时为页面定义语义化规则:

ts 复制代码
private useSideNavigation(): boolean {
  return this.currentBp === 'lg'
}

private useDetailColumns(): boolean {
  return this.currentBp === 'lg' && this.pageWidth >= 700
}

private categoryColumns(): number {
  if (this.pageWidth >= 960) return 4
  if (this.pageWidth >= 600) return 3
  return 2
}

方法名直接表达设计意图,阈值调整时也有唯一入口。不要把"平板""PC"直接等同于某个像素宽度,因为 PC/2in1 支持自由窗口,设备没变,窗口却可以持续变化。

十二、多设备验收矩阵必须覆盖窗口过程

只在三个固定分辨率截图远远不够。针对知律当前页面,建议至少验证:

场景 主导航 内容布局 重点检查
手机竖屏 底部 Tab 单列 安全区、长文本、按钮
手机横屏 按产品规则 单列或紧凑双栏 高度不足、横向拥挤
折叠屏展开 底部或侧栏 2-3 列 折叠前后状态保持
平板竖屏 侧边栏 多列/双栏 可读行宽、滚动焦点
平板横屏 侧边栏 双栏 两栏比例、空白控制
PC/2in1 宽窗口 侧边栏 双栏 最大宽度、鼠标滚轮
PC/2in1 窄窗口 自动回退 单列 连续拖动、无闪烁
系统字体放大 结构不变 内容增高 截断、重叠、按钮换行

每个场景都应走首页切 Tab、进入分类、打开题库详情、滚动章节、进入考试结果、点击底部操作并返回。还要观察窗口跨越 600vp、840vp 和页面 700/720vp 阈值时,布局是否只切换一次、是否出现中间空白或状态丢失。

十三、针对现有源码的落地顺序

最小可验证的改造顺序是:

  1. 修复 Index 覆盖三种断点的条件,让侧边导航真实可达;
  2. 明确 md 到底使用底部还是侧边导航,并同步注释与测试;
  3. 为主内容增加合理最大宽度,避免 2in1 超宽拉伸;
  4. 把分类页从两档百分比扩展为明确的 2/3/4 列策略;
  5. ExamResultPage 增加局部宽度测量和宽屏布局;
  6. 统一安全区方法语义,检查是否存在重复 padding;
  7. 在真实设备或模拟器上执行跨阈值拖动、旋转和字体放大测试。

每一步都能单独截图和验证,不需要一次性重写所有页面。

十四、结语

知律已经搭起多设备适配的关键链路:BreakpointSystem 监听窗口媒体查询,通过 AppStorage 分发断点;页面用 onAreaChange 获取真实容器宽度;分类页按宽度换列,题库详情按断点和内容宽度切换单双栏;EntryAbility 统一提供系统安全区数据。

当前最关键的问题是 Index 的合法断点被同一个条件全部吞掉,导致已经写好的侧边导航无法执行。修复这个入口后,再逐步补齐结果页宽屏结构、内容最大宽度、三档网格和跨窗口测试,才能让"手机、平板、PC/2in1"从代码中的注释变成可复核的运行行为。

本文由 AI 辅助整理,所有技术结论均基于项目真实源码复核;未虚构设备测试通过结果、平台数据、排名、PV、点赞、收藏或推荐结果。

相关推荐
小雨青年3 小时前
【HarmonyOS 7 沉浸光感深度实战】10 通用组件封装与现有项目接入
华为·harmonyos
YM52e4 小时前
蒙特卡洛模拟器:鸿蒙 Canvas 上的大数定律与中心极限定理实证
华为·harmonyos
Dovis(誓平步青云)5 小时前
DevEco Studio 6.1.1 Windows 安装实录:从下载校验到首次启动
android·开发语言·数据库·人工智能·windows·harmonyos
2501_919749035 小时前
华为鸿蒙免费排班APP—小羊排班
华为·harmonyos·鸿蒙
大雷神6 小时前
相册照片不用上传:HarmonyOS Core Vision 端侧识别动物与植物
harmonyos
OH_TPC6 小时前
HarmonyOS APP开发---"刷刷"短视频App,需要用到这个库
harmonyos
超爱西西鸭7 小时前
思维导图编辑体验:触摸交互与节点操作(ArkTS)
学习·华为·harmonyos
特立独行的猫A8 小时前
cc-switch 鸿蒙 PC移植:使用Tauri官方 feat/open-harmony 分支最新版本移植实录
harmonyos
世人万千丶8 小时前
HarmonyOS 股票 K 线图完整源码与金融图表模板
学习·华为·金融·harmonyos·鸿蒙