在module.json5中写入phone、tablet和2in1,只代表应用允许安装到这些设备,并不等于页面已经完成多设备适配。真正的风险通常出现在窗口变化之后:平板竖屏显示正常,横屏却只把手机列表拉得更宽;PC 自由窗口缩小时,“我的”页面底部卡片无法滚动;首页从两列切成三列后,热门实验数量增加到 8 个,但 Grid 仍保持手机高度,第三行内容被裁切;系统字体放大后,固定高度 Banner 内按钮挤出容器。
本文基于化学实验室 1.0.5的真实 HarmonyOS ArkTS 源码展开。工程的entry/src/main/module.json5明确声明:
"deviceTypes": [ "phone", "tablet", "2in1" ]主入口Index.ets始终使用 56vp 底部 Tabs;HomePage.ets和LearningPage.ets通过onAreaChange()读取宽度,在 600vp 与 840vp 处调整 Grid 列数;LabPage.ets始终显示单列实验列表;MinePage.ets使用不可滚动的根 Column。当前实现已经迈出响应式第一步,但各页面对窗口的理解不同,部分固定尺寸没有随内容重算。
本文不按设备名称复制三套页面,而是建立一份可测试的WindowProfile:窗口宽高先映射为 compact、medium、expanded 三种布局能力,再由导航壳、内容网格、列表分栏和输入反馈分别消费。同一应用在手机旋转、平板分屏、PC 自由窗口缩放时,都按实际可用内容宽度重排。
多设备适配不是根据设备名称复制页面,而是把窗口宽度映射成稳定布局能力,再让每个业务区在同一能力下重排、限宽和保留可达操作。
CHEM-LAB-ONE12-MULTIDEVICE-WINDOW-PROFILE-20260730
本文将完成以下内容:
- 核对工程真实设备声明与五个主页面布局。
- 解释设备类型、屏幕尺寸和窗口尺寸的区别。
- 复现首页 medium 宽度下 Grid 高度与行数不匹配的问题。
- 建立统一的 compact、medium、expanded 窗口等级。
- 让首页和学习页共享列数、间距和内容宽度规则。
- 为实验室设计单列到列表详情分栏的渐进布局。
- 让“我的”页面在横屏和小窗口中保持全部操作可达。
- 为 expanded 窗口增加限宽,避免内容无限拉伸。
- 补齐鼠标悬停、键盘焦点和触摸目标验证。
- 给出旋转、分屏、自由窗口和字体放大的测试矩阵。
一、设备声明只说明分发范围
当前module.json5把 phone、tablet 和 2in1 都列入deviceTypes。这意味着发布材料、截图组、交互方式和运行验证都要覆盖三类目标,但页面不会因为这个数组自动从单列变成分栏。
同一设备上还会出现多个窗口状态:
- 手机竖屏与横屏。
- 折叠屏折叠与展开。
- 平板全屏、分屏和悬浮窗。
- PC/2in1 最大化、半屏与自由缩放窗口。
因此布局判断不能只读取“设备是平板还是电脑”,而要以页面真正获得的内容区宽高为依据。
二、当前响应式实现分布在两个页面
HomePage保存本地宽度:
@State screenWidth: number = 360 .onAreaChange((_o, n) => { this.screenWidth = n.width as number })LearningPage也维护一份相同状态和独立断点函数:
private gridCols(): string { if (this.screenWidth > 840) return '1fr 1fr 1fr 1fr' if (this.screenWidth > 600) return '1fr 1fr 1fr' return '1fr 1fr' }这种局部实现短期有效,但会产生三个问题:页面可能在边界值上使用不同规则;每个页面重复处理窗口变化;导航和内容布局不知道彼此的窗口等级。
三、首页的列数规则存在重复分支
HomePage.gridColumns()当前为:
private gridColumns(): string { if (this.screenWidth > 840) return '1fr 1fr 1fr 1fr' if (this.screenWidth > 600) return '1fr 1fr 1fr' return '1fr 1fr 1fr' }600vp 以下和 600 到 840vp 都返回三列,断点没有改变列数。对于 360vp 手机,扣除左右页面边距和两个列间距后,每个实验卡片宽度较窄,长实验名只能依靠省略。
如果产品确实要求手机三列,应删除无效分支并明确理由;如果目标是手机两列、中宽三列、宽屏四列,应让函数真实反映该策略。
private columnsFor(sizeClass: WindowSizeClass): number { if (sizeClass === WindowSizeClass.EXPANDED) return 4 if (sizeClass === WindowSizeClass.MEDIUM) return 3 return 2 }四、一个可以直接复现的 medium 宽度裁切问题
首页展示热门实验数量:
private hotExperiments(): Experiment[] { const count = this.screenWidth > 600 ? 8 : 6 return this.experiments.slice(0, count) }Grid 高度却固定为:
.height(172)单个 GridItem 高 82vp,行间距 8vp。三种窗口下所需高度如下:
| 窗口 | 项目数 | 列数 | 行数 | 所需高度 |
|---|---|---|---|---|
| compact,若两列 | 6 | 2 | 3 | 82×3 + 8×2 = 262vp |
| medium | 8 | 3 | 3 | 82×3 + 8×2 = 262vp |
| expanded | 8 | 4 | 2 | 82×2 + 8 = 172vp |
现有 172vp 只刚好适合 expanded 的两行。尤其在 600 到 840vp 区间,8 个项目排成三行,却仍只分配两行高度。这不是抽象的适配建议,而是可以用 720vp 宽度窗口复现的确定性风险。
五、动态列数必须同步动态行数
可以根据项目数和列数计算高度:
private gridHeight( itemCount: number, columns: number, itemHeight: number, rowGap: number ): number { if (itemCount <= 0 || columns <= 0) { return 0 } const rows = Math.ceil(itemCount / columns) return rows * itemHeight + Math.max(0, rows - 1) * rowGap }使用时保证项目数、列数和高度来自同一份计算:
const items = this.hotExperiments() const columns = this.columnsFor(this.windowProfile.sizeClass) Grid() { // ForEach(items) } .columnsTemplate(this.columnsTemplate(columns)) .height(this.gridHeight(items.length, columns, 82, 8))如果 Grid 在当前容器中能根据内容自然测量,也可以去掉固定高度;但必须用真机验证 Scroll 内嵌 Grid 的测量行为,不能只删除高度后假设正确。
六、建立项目自己的窗口等级
断点不是设备型号表,而是内容在某个宽度开始需要重排的阈值。本文沿用源码已有的 600 与 840 边界,先把它们集中:
export enum WindowSizeClass { COMPACT = 'compact', MEDIUM = 'medium', EXPANDED = 'expanded' } export interface WindowProfile { width: number height: number sizeClass: WindowSizeClass landscape: boolean } export function resolveWindowProfile( width: number, height: number ): WindowProfile { let sizeClass = WindowSizeClass.COMPACT if (width >= 840) { sizeClass = WindowSizeClass.EXPANDED } else if (width >= 600) { sizeClass = WindowSizeClass.MEDIUM } return { width, height, sizeClass, landscape: width > height } }600 与 840 是本项目的初始断点,不应包装成所有 HarmonyOS 应用的通用标准。后续要根据真实内容、字体和窗口测试调整。
七、只在根容器采样一次窗口
当前 Home 和 Learning 各自监听onAreaChange()。更一致的方案是在主壳获取内容区尺寸,再把WindowProfile传给页面:
@State contentWidth: number = 360 @State contentHeight: number = 720 private windowProfile(): WindowProfile { return resolveWindowProfile(this.contentWidth, this.contentHeight) } Stack() { this.MainContent(this.windowProfile()) } .onAreaChange((_oldValue, newValue) => { this.contentWidth = newValue.width as number this.contentHeight = newValue.height as number })页面只消费配置:
@Prop windowProfile: WindowProfile如果某个页面内还有侧栏或内边距,应以它自己的可用内容宽度继续细化,而不是使用物理屏幕宽度。
八、窗口变化要做幂等状态更新
onAreaChange可能频繁触发。不要每次都创建大量对象或写入持久化。只在等级、方向或有效尺寸真正变化时更新:
private updateWindow(width: number, height: number): void { const next = resolveWindowProfile(width, height) if ( next.sizeClass === this.currentProfile.sizeClass && next.landscape === this.currentProfile.landscape && next.width === this.currentProfile.width && next.height === this.currentProfile.height ) { return } this.currentProfile = next }窗口画像属于运行时 UI 状态,不需要写入 Preferences。
九、compact 布局的目标不是塞下所有内容
手机和窄窗口应优先保证核心流程可达:
- 底部四标签保持 56vp 稳定高度。
- 首页热门实验使用两列,名称允许单行省略。
- 快捷入口保持两列,但文字区域必须有
layoutWeight(1)。 - 实验室使用单列列表。
- 学习中心使用两列网格。
- 所有长页面使用 Scroll 或 List。
窄窗口不要为了展示更多内容把字体、图标和触摸目标继续缩小。
十、medium 布局要解决信息密度与高度联动
600 到 840vp 常见于平板分屏、折叠屏展开后的窄列或 PC 半屏。建议:
- 首页热门实验三列、8 项、三行,Grid 高度 262vp。
- 学习分类和工具三列。
- 快捷入口可从两列变为三列或保持两列但限制最大宽度。
- 页面内容区最大宽度可设 720 到 760vp 并居中。
- 实验室仍可单列,但行内容限制最大宽度,避免描述横向拉得太长。
medium 不是“放大后的手机”,它通常需要重新计算内容密度。
十一、expanded 布局要主动使用宽度
840vp 以上的平板横屏和 PC/2in1 窗口,可以:
- 首页采用左右区域,左侧 Banner 与建议,右侧热门实验和快捷工具。
- 学习中心四列,并把知识分类与工具分区并排。
- 实验室采用左侧筛选列表、右侧实验预览或说明。
- “我的”页面使用两列卡片区,而不是把单列菜单拉满全屏。
- 主内容设置最大宽度,超出后留出合理空白。
宽屏价值来自并行信息和更少跳转,而不是每个卡片横向变宽。
十二、统一内容最大宽度
PC 最大化时,如果所有width('100%')都直接拉满,菜单和段落会变得难以扫描。可以建立内容框架:
private contentMaxWidth(profile: WindowProfile): number | string { if (profile.sizeClass === WindowSizeClass.EXPANDED) return 1180 if (profile.sizeClass === WindowSizeClass.MEDIUM) return 760 return '100%' } Column() { // 页面内容 } .width(this.contentMaxWidth(this.windowProfile)) .alignSelf(ItemAlign.Center)最大宽度值要通过实际页面截图和阅读距离验证,不能照搬其他产品。
十三、导航形态也应属于窗口画像
Index.ets当前所有设备都使用底部 Tabs:
Tabs({ barPosition: BarPosition.End, ... }) .barHeight(56)compact 和 medium 可以继续使用底部导航。expanded 窗口则可以切换为左侧导航栏,让内容获得更多垂直空间:
if (profile.sizeClass === WindowSizeClass.EXPANDED) { Row() { this.NavigationRail() this.TabContentHost(profile) } } else { this.BottomTabShell(profile) }两种壳必须共享同一个currentIndex与导航契约,不能分别维护选中状态。
十四、导航切换时要保留业务页面状态
从底栏切换到侧栏发生在窗口跨过断点时。若条件分支直接销毁并重建整个页面树,用户的滚动位置和筛选条件可能丢失。
改造时应验证:
- 当前主标签不变。
- 实验室分类筛选不被重置。
- 学习页滚动位置策略明确。
- 正在编辑的笔记不会因为窗口重排丢失草稿。
- 二级页面返回路径不因主壳变化而改变。
必要时把关键页面状态提升到主壳或 ViewModel,而不是依赖组件实例存活。
十五、首页 Banner 固定高度需要字体测试
首页 Banner 高度固定 168vp,包含标题、副标题和按钮;学习页 Banner 固定 132vp。系统字体放大、文本本地化或按钮文字变化后,固定高度可能不够。
可以使用最小高度与内容撑开:
Column() { // 标题、副标题、按钮 } .constraintSize({ minHeight: 168 }) .padding({ left: 22, right: 18, top: 20, bottom: 20 })如果视觉必须固定高度,则要限制行数并验证最大字体比例,不能让文字覆盖按钮。
十六、快捷入口不应永久写成四个 Row
HomePage当前用四个Row,每行两个 QuickAction。宽屏下仍只能两列,新增入口还要复制一整段 Row。
可以把入口建模为数据并用 Grid 渲染:
interface QuickActionItem { title: string subtitle: string icon: Resource route: string } Grid() { ForEach(this.quickActions, (item: QuickActionItem) => { GridItem() { this.QuickAction(item) } }, (item: QuickActionItem) => item.route) } .columnsTemplate(this.quickActionColumns(profile))compact 两列、medium 三列、expanded 四列,新增入口无需修改页面骨架。
十七、LearningPage 已有好基础但需要去重
学习中心的知识分类和学习工具都使用gridCols(),可以共享窗口规则。问题是页面自己持有screenWidth,断点逻辑与首页重复。
改造后两个 Grid 直接消费统一列数:
private learningColumns(profile: WindowProfile): number { if (profile.sizeClass === WindowSizeClass.EXPANDED) return 4 if (profile.sizeClass === WindowSizeClass.MEDIUM) return 3 return 2 }卡片高度 98vp 和 88vp 可以保留为稳定格式,但要验证长标题、字体放大和英文环境。
十八、LabPage 宽屏仍是单列长行
实验室当前以 List 展示 56vp 图标、标题、描述、分类和收藏按钮。手机上可扫描,PC 宽窗下每行会横向拉得很长,信息密度没有提升。
expanded 可以采用列表详情布局:
左栏 360vp:分类与实验列表 右栏 自适应:实验介绍、参数摘要、开始按钮用户用鼠标选择左侧实验,右侧直接预览;双击或点击开始进入模拟页。这样利用宽度减少跳转,同时保留 compact 下的单列列表。
十九、如果暂不做详情栏,至少限制列表宽度
增量改造可以先让 List 最大宽度 840vp 并居中:
List() { // 原有 ListItem } .width('100%') .constraintSize({ maxWidth: 840 }) .alignSelf(ItemAlign.Center)这不会增加新交互,却能避免描述文字在 PC 上形成过长行。
二十、MinePage 在小窗口中存在可达性风险
MinePage根节点是固定height('100%')的 Column,没有 Scroll。页面包括 88vp 个人区、7 行菜单、分隔线、上下内边距和离线说明卡片。在手机横屏或 PC 窄高窗口中,底部内容可能超出可见区域且无法滚动。
最小修复是把主体放入 Scroll:
Column() { Scroll() { Column() { this.ProfileHeader() this.MenuSection() this.OfflineCard() } .width('100%') } .layoutWeight(1) } .width('100%') .height('100%')所有核心操作在任何窗口高度都必须可达。
二十一、MinePage 的 expanded 版本适合两列
宽屏下可以把菜单与离线信息分成两列:
左侧:实验记录、统计、收藏、笔记 右侧:设置、帮助、关于、离线说明两列卡片各自限制宽度,标题和说明保持合理行长。不要在页面区块外再套装饰卡片,只让菜单组本身成为清晰容器。
二十二、Scroll、List 与 Grid 的职责要分清
- 长顺序记录使用 List,便于复用和滚动。
- 固定数量工具卡使用 Grid。
- 由多个不同区块组成的首页和学习页使用 Scroll。
- 不要在外层 Scroll 中再放一个可独立滚动的长 List,除非明确处理滚动冲突。
- Grid 动态列数时,要同步处理高度或使用可验证的自然测量。
容器选错后,适配问题往往表现为滚动区域截断,而不是简单的宽度错误。
二十三、安全区不能靠设备类型猜
Index.ets从 AppStorage 读取statusBarHeight与bottomBarHeight,并给 Tabs 添加顶部和底部 padding。安全区策略要与窗口模式一致:
- 非沉浸窗口通常已由系统处理部分区域,避免重复避让。
- 沉浸式页面需要读取真实避让区。
- 横屏、分屏和窗口化时避让值可能变化。
- 底部页签或工具栏要确保不进入系统手势区。
不能因为设备是 tablet 就写死另一组安全区数值。
二十四、图片和图标要保持稳定尺寸
当前首页实验图标 44vp、实验室列表图标 56vp、底部标签图标 24vp。不同设备上这些固定视觉尺寸可以保留,不需要跟随窗口无限放大。
响应式不是所有元素等比例缩放,而是:图标保持清晰尺寸,卡片和网格调整排列,内容区控制最大宽度,必要时增加并行区域。
二十五、文字适配要同时检查宽度和高度
源码已经大量使用:
.maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis })这能避免横向溢出,但不能解决纵向固定高度。测试时要覆盖:
- 系统字体 1.0、1.3、1.75。
- 最长实验名和最长分类名。
- compact 窗口两列卡片。
- PC 窗口缩到最小允许宽度。
- 中文与可能的英文资源。
重要操作文字不能只剩不可理解的省略号。
二十六、2in1 不只有尺寸差异
PC/2in1 用户会使用鼠标、触控板和键盘。页面还要补齐:
- 可点击行的 hover 与 pressed 反馈。
- 键盘焦点样式。
- Enter 或 Space 激活主要操作。
- Tab 顺序符合视觉顺序。
- 收藏星标等图标按钮有明确可访问说明。
- 右键和双击仅在有清晰产品需求时使用。
不能只把窗口拉宽就宣称完成 2in1 适配。
二十七、不要按设备复制业务逻辑
compact、medium 和 expanded 可以有不同页面骨架,但实验数据、收藏操作和导航目标必须复用同一业务层。
推荐结构:
ExperimentRepository -> CompactLabLayout -> MediumLabLayout -> ExpandedLabLayout布局组件只负责呈现和事件回调,不能各自再实现一遍收藏和路由逻辑。
二十八、响应式壳的模块结构
模块职责如下:
WindowProfile统一宽高、方向和窗口等级。NavigationShell选择底部导航或侧边导航。ContentLayout决定网格、分栏、最大宽度和间距。InputPolicy处理触控、鼠标和键盘反馈。- 业务页面只消费画像,不重复定义断点。
- 页面状态在骨架切换时保持稳定。
二十九、避免窗口变化时的布局抖动
自由缩放会连续触发尺寸变化。若每一个像素都切换复杂页面树,会增加重建压力。可以:
- 只在跨越断点时切换布局骨架。
- 同一等级内只更新必要的宽高约束。
- 不在
build()中创建大型临时数组。 - ForEach 使用稳定业务键。
- 避免窗口变化时重复读取持久化数据。
布局适配与数据加载是两个不同事件,不应绑定在一起。
三十、断点边界测试必须包含临界值
至少测试:
599vp -> compact 600vp -> medium 601vp -> medium 839vp -> medium 840vp -> expanded 841vp -> expanded如果代码使用>而设计文档使用>=,恰好 600vp 与 840vp 会落在不同等级。本文示例统一使用>=,并通过边界测试固定行为。
三十一、页面测试矩阵
| 页面 | compact | medium | expanded |
|---|---|---|---|
| Index | 底栏可见 | 底栏可见 | 侧栏或受控底栏 |
| Home | 两列、6 项、3 行 | 三列、8 项、3 行 | 四列、8 项、2 行 |
| Lab | 单列列表 | 限宽单列 | 列表详情分栏 |
| Learning | 两列 | 三列 | 四列或分区并排 |
| Mine | 可滚动单列 | 可滚动限宽 | 两列菜单区 |
每个格子都要验证内容、滚动、点击、返回和状态保持。
三十二、窗口场景测试
- 手机竖屏冷启动。
- 手机横屏并滚到页面底部。
- 平板竖屏全屏。
- 平板横屏全屏。
- 平板二分屏和三分屏。
- 2in1 从最大化连续缩到最小窗口。
- 跨越 600vp 与 840vp 时保持当前页签。
- 折叠屏展开与折叠时保持筛选状态。
- 系统字体放大后 Banner 和卡片不重叠。
- 键盘 Tab 遍历与 Enter 激活。
每次测试记录窗口实际宽高,不只记录设备型号。
三十三、视觉验收不能只看截图
静态截图能发现拉伸、空白和截断,但自由窗口还要观察动态过程:
- 跨断点时是否闪白。
- 卡片是否突然跳到错误位置。
- 滚动位置是否被重置。
- 点击区域是否与视觉元素分离。
- 页面是否出现短暂空态。
- 连续缩放时是否明显掉帧。
必要时录屏并在关键宽度暂停检查。
三十四、构建验证
完成共享类型、页面参数和条件布局改造后,运行项目构建:
$env:DEVECO_SDK_HOME='D:\huawei\DevEco Studio\sdk' $env:PATH='D:\huawei\DevEco Studio\jbr\bin;' + $env:PATH & 'D:\huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat' --no-daemon assembleHap构建通过后,至少在 phone、tablet 和 2in1 对应模拟器或真机上完成安装、启动、主页面切换、二级页面、返回和卸载烟测。
三十五、常见故障排查
1. medium 宽度第三行不可见
检查项目数、列数、行数与 Grid 高度是否来自同一计算,确认没有保留固定 172vp。
2. 窗口跨断点后回到首页
检查 compact 与 expanded 是否各自维护一份 currentIndex,或条件分支是否重建了整个主状态。
3. PC 上列表过宽难读
检查内容最大宽度、列表详情分栏和段落行长,不要只增加左右 padding。
4. 横屏底部内容无法操作
检查根页面是否缺少 Scroll/List,确认底部安全区和窗口高度更新。
5. 自由缩放明显卡顿
检查是否每次像素变化都重建复杂数据、重新读取存储或生成新数组。骨架切换只应发生在断点变化时。
三十六、改造前后对比
| 维度 | 改造前 | 改造后 |
|---|---|---|
| 窗口判断 | 页面各自维护宽度 | 根壳统一 WindowProfile |
| 断点 | 逻辑重复且首页分支无效 | compact/medium/expanded 一致 |
| 首页 Grid | 列数变化,高度固定 | 项目、列数、行数同步计算 |
| 快捷入口 | 四个固定双列 Row | 数据驱动响应式 Grid |
| 实验室 | 所有窗口单列拉满 | 单列、限宽、列表详情渐进 |
| 学习中心 | 独立 screenWidth | 共享列数规则 |
| 我的页面 | 固定 Column 不可滚动 | 小窗可滚动,宽屏两列 |
| 导航 | 所有设备底栏 | 窄窗底栏,宽窗可用侧栏 |
| PC 输入 | 只有点击 | 悬停、焦点、键盘激活 |
| 验证 | 只看设备全屏 | 边界、分屏、自由窗口、字体 |
三十七、最小落地顺序
- 新增
WindowProfile与断点边界测试。 - 由 Index 统一采样内容区宽高。
- 修复首页列数和 Grid 动态高度。
- 让 LearningPage 消费统一列数。
- 把 Home 快捷入口改为数据驱动 Grid。
- 给 LabPage 增加内容限宽,再逐步做分栏。
- 给 MinePage 增加 Scroll 与 expanded 两列布局。
- 最后切换宽屏导航壳并补鼠标键盘状态。
先修复确定性裁切,再增加大屏体验,风险最小。
三十八、验收清单
- [ ]
module.json5的目标设备与发布材料一致。 - [ ] 页面依据可用窗口宽度而非设备名称重排。
- [ ] 600vp 和 840vp 边界行为有自动测试。
- [ ] 首页 compact、medium、expanded 列数符合设计。
- [ ] 热门实验 Grid 高度与项目数和列数一致。
- [ ] LearningPage 不再维护重复断点规则。
- [ ] LabPage 在 PC 宽窗中不会无限拉长单行。
- [ ] MinePage 在手机横屏和小窗口中可以滚到底部。
- [ ] expanded 内容有最大宽度或分栏,不无限拉伸。
- [ ] 跨断点后当前页签、筛选和编辑状态不丢失。
- [ ] 系统字体放大后 Banner、按钮和卡片不重叠。
- [ ] 底部导航与系统手势区没有遮挡。
- [ ] 2in1 支持悬停、键盘焦点和 Enter 激活。
- [ ] 自由缩放时没有明显白屏、跳页或重复加载。
- [ ] phone、tablet、2in1 完成构建和核心流程烟测。
三十九、源码事实与建议实现的边界
为了避免把架构建议误读成当前版本已经具备的能力,下面把本文引用的源码事实和建议改造逐项分开。左列可直接在one12工程中复核,右列是本文给出的后续实现方向。
| 维度 | 当前源码事实 | 本文建议实现 |
|---|---|---|
| 目标设备 | module.json5声明phone、tablet、2in1 | 发布材料和运行验证覆盖三个设备类型及其窗口状态 |
| 主导航 | Index.ets始终使用底部Tabs,栏高 56vp | expanded 窗口可评估侧边导航,但不是当前已实现能力 |
| 窗口采样 | HomePage、LearningPage各自通过onAreaChange()保存宽度 | 根壳统一生成WindowProfile,业务页面只消费布局能力 |
| 首页断点 | 840vp 以上四列,600vp 以上三列,其余仍为三列 | compact 两列、medium 三列、expanded 四列,并统一边界语义 |
| 热门实验数量 | 宽度大于 600vp 取 8 项,否则取 6 项 | 项目数、列数、行数和 Grid 高度由同一次计算产生 |
| 首页 Grid 高度 | 固定为 172vp | 根据实际行数计算,或在验证测量行为后取消固定高度 |
| 学习页 | 使用独立screenWidth与两列/三列/四列规则 | 与首页共享断点、间距、最大内容宽度和测试用例 |
| 实验室页 | 分类横向滚动,实验列表始终单列 | medium 先限宽,expanded 再逐步引入列表详情分栏 |
| 我的页面 | 根节点是不可滚动的Column | compact 和低高度窗口增加滚动,expanded 再设计两列菜单区 |
| 页面状态 | Index只维护一份currentIndex | 切换导航骨架时继续复用同一业务状态,不复制页面实例 |
| 鼠标与键盘 | 源码主要定义点击行为 | 2in1 增补悬停、焦点、Enter 激活与可见焦点样式 |
| 构建和真机结果 | 本文未声称改造方案已经构建或上机验证 | 实施后按文中矩阵执行构建、安装、窗口变化和核心流程烟测 |
这条边界很重要:本文对 172vp Grid 的分析来自现有代码,可复核;WindowProfile、侧边导航、列表详情分栏和键盘策略则是建议方案。读者落地时应按最小改造顺序逐项实现,并以构建和设备验证结果为准。
四十、总结
化学实验室已经声明 phone、tablet 和 2in1,也在首页与学习页通过onAreaChange()调整列数。但真正的多设备适配不能停在“宽度大了多放一列”。当前首页 8 个热门实验在 medium 三列下需要三行,固定 172vp 高度却只够两行;实验室和“我的”页面没有宽屏重排;MinePage 缺少滚动,小窗口下存在操作不可达风险。
增量改造的核心是一份统一WindowProfile。根壳读取实际内容区宽高,映射为 compact、medium、expanded;导航、网格、列表、最大宽度和输入反馈再消费同一画像。动态列数必须同步动态行数与高度,宽屏要增加并行信息或限制内容宽度,窄窗要保证滚动和触摸目标。
做到这些以后,同一套 ArkTS 业务逻辑才能在手机旋转、平板分屏和 PC 自由窗口中保持连续。多设备不是三份页面,而是一份稳定业务状态加多种经过验证的布局能力。
参考资料
- Huawei Developer,HarmonyOS 多设备开发最佳实践:
https://developer.huawei.com/consumer/cn/best-practices/multidevice/ - Huawei Developer,HarmonyOS 设计与多设备场景:
https://developer.huawei.com/consumer/cn/design/ - Huawei Developer,ArkUI 概述:
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkui-overview - 项目配置:
entry/src/main/module.json5 - 项目源码:
entry/src/main/ets/pages/Index.ets - 项目源码:
entry/src/main/ets/pages/HomePage.ets - 项目源码:
entry/src/main/ets/pages/LabPage.ets - 项目源码:
entry/src/main/ets/pages/LearningPage.ets - 项目源码:
entry/src/main/ets/pages/MinePage.ets
AI 辅助声明:本文由 AI 辅助完成源码梳理、结构校对与配图设计,技术结论均以文中列出的本地 ArkTS 源码为依据,并由作者复核后发布。