资讯详情 〖共创稿事节〗HarmonyOS 7 新特性实战(36):一套 ArkUI 代码适配手机、平板和 2in1
📅 2026/10/24 5:38:15
同一个 2in1 模拟器我原以为图鉴一定会显示三列。实际启动后应用处在桌面自由窗口里页面只排了两列——而这恰恰是正确结果。如果代码先按“电脑”处理答案会把三列硬塞进一个尚未展开的窗口如果代码先问“当前窗口还剩多少可用宽度”它就会在窗口变大后自然切到三列。《山海万灵》的多端适配因此没有维护 phone、tablet、2in1 三套页面。窗口尺寸会被转换成一组可测试的布局决策导航放哪、内容有多宽、卡片排几列、个人页是否分栏以及滚动区底部该留多少净空。下文记录这套决策如何在 HarmonyOS 7 API 26 环境下落地并展示一个容易遗漏的边界窗口很宽但高度不足时侧边导航仍不应该出现。一、从窗口状态出发华为官方的响应式布局最佳实践将断点建立在应用窗口宽度和高宽特征上窗口模式变化、折叠状态与显示缩放都可能让同一台设备跨越断点。一次开发多端部署 Codelab也把断点解释为对应用窗口宽度区间的划分设备型号无需进入这张布局表。这带来一个很实用的判断顺序1. 先观察当前根容器宽高 2. 再扣除导航、页面边距等固定占用 3. 最后用“可用内容宽度”决定卡片和分栏 4. 设备类型只决定能力或部署范围不直接决定 UI 列数。《山海万灵》当前采用下面这张决策表。这些数值来自项目侧栏、卡片最小宽度和内容密度不能直接视作通用规范。决策条件结果主导航宽度 ≥ 840vp 且高度 ≥ 520vp左侧导航主导航其他窗口浮动底部导航页面边距宽度 600vp / 600839vp / ≥ 840vp14 / 20 / 24vp个人页页面内宽 ≥ 600vp双栏否则纵向堆叠图鉴内宽容纳 3/2/1 张 420vp 卡片与间距三列 / 双列 / 单列滚动净空侧栏 / 底部浮动栏36 / 112vp二、只在根容器测量一次尺寸状态如果分散在首页、图鉴和个人页里各页面会在同一个窗口下得出不同答案。项目把根容器作为唯一测量点只在有效数值变化时更新状态再把派生结果传给各业务页面。State private screenWidthVp: number 390; State private screenHeightVp: number 844; build() { Stack() { if (this.appLoadState.phase ! CONTENT) { this.AppStatePage(); } else { this.MainShell(); } } .width(100%) .height(100%) .onAreaChange((_oldValue: Area, newValue: Area) { const width: number Math.round(Number(newValue.width)); const height: number Math.round(Number(newValue.height)); if (width 0 width ! this.screenWidthVp) { this.screenWidthVp width; } if (height 0 height ! this.screenHeightVp) { this.screenHeightVp height; } }); }这里同时记录宽和高还有一个实际目的。项目曾遇到“960vp 很宽所以切侧栏”的直觉写法但窗口只有 480vp 高时侧栏五个入口加品牌区会被压缩底部浮动栏反而更稳妥。因此侧栏条件必须是二维门槛const SIDE_NAV_MIN_WIDTH: number 840; const SIDE_NAV_MIN_HEIGHT: number 520; const SIDE_NAV_FOOTPRINT: number 109; const DEFAULT_PAGE_MAX_WIDTH: number 920; const ARCHIVE_PAGE_MAX_WIDTH: number 1360; const CARD_GAP: number 12; static usesSideNavigation(width: number, height: number): boolean { return width SIDE_NAV_MIN_WIDTH height SIDE_NAV_MIN_HEIGHT; } static shellContentWidth(width: number, height: number): number { const navigationWidth: number this.usesSideNavigation(width, height) ? SIDE_NAV_FOOTPRINT : 0; return Math.max(0, width - navigationWidth); } static pageInnerWidth( width: number, height: number, maxWidth: number DEFAULT_PAGE_MAX_WIDTH ): number { const pageWidth: number Math.min( this.shellContentWidth(width, height), maxWidth ); return Math.max(0, pageWidth - this.pageSideInset(width) * 2); } static archiveColumns( width: number, height: number, cardWidth: number 420, gap: number CARD_GAP ): number { const available: number this.pageInnerWidth( width, height, ARCHIVE_PAGE_MAX_WIDTH ); if (available cardWidth * 3 gap * 2) return 3; if (available cardWidth * 2 gap) return 2; return 1; }这段代码应优先关注计算顺序840或420只是当前业务参数窗口宽度 - 侧栏占用仅侧栏启用时 → 壳层内容宽度 → 与页面最大宽度取较小值 - 左右页面边距 → 业务组件可用内宽宽屏页面如果直接使用整个窗口宽度文字行会过长卡片也会被无限放大。因此普通页面封顶 920vp图鉴这种重复卡片页面放宽到 1360vp。两种上限不同但都复用同一条“先扣壳层、再限宽、最后扣边距”的管线。三、让导航、列数和底部净空一起切换只把底栏换成侧栏还不够。浮动底栏覆盖在滚动内容之上如果宽屏切到侧栏后仍保留 112vp 底部空白页面尾部会出现一块无意义留白反过来如果窄屏只留 36vp最后一张卡会被浮动栏压住且无法滚出。所以导航形态与内容净空必须来自同一个判断侧栏只留 36vp 呼吸空间浮动栏则预留“64vp 栏高 32vp 底距 16vp 间隙”合计 112vp。个人页单双栏、推荐卡双列和图鉴列数则继续依据页面内宽独立计算。这样不会出现“已切侧栏但内容仍单列”或“底栏消失后还残留大块空白”的半适配状态。手机上的结果是浮动底栏和单列图鉴平板横屏进入侧栏个人页把资料与进度区并排2in1 截图最能说明为什么不能按设备名写死。设备确实是 2in1但应用处在桌面窗口内当前可用宽度只够双列因此图鉴保持双列当窗口扩展到 1536×960vp 代表尺寸时规则测试才会切为三列。四、把断点变成可回归的契约响应式回归很少表现为编译错误。更常见的情况是某次改了侧栏宽度或页面上限另一个页面悄悄跨错断点。为此验证脚本复用工程中的响应式决策模块对代表窗口和临界值做断言避免复制第二份算法。const cases [ [phone, 390, 844, false, 1, STACK, 112], [tablet-portrait, 800, 1280, false, 1, TWO_PANE, 112], [tablet-landscape, 1280, 800, true, 2, TWO_PANE, 36], [2in1-wide, 1536, 960, true, 3, TWO_PANE, 36], [wide-but-short, 960, 480, false, 2, TWO_PANE, 112] ]; for (const [name, width, height, sideNav, columns, profile, bottom] of cases) { assert.equal(responsive.usesSideNavigation(width, height), sideNav, name); assert.equal(responsive.archiveColumns(width, height), columns, name); assert.equal(responsive.profileLayoutMode(width, height), profile, name); assert.equal(responsive.pageBottomInset(width, height), bottom, name); } assert.equal(responsive.usesSideNavigation(839, 900), false); assert.equal(responsive.usesSideNavigation(840, 519), false); assert.equal(responsive.usesSideNavigation(840, 520), true);本轮验证覆盖 5 组代表窗口与 4 个边界断言全部通过当前 HAP 构建成功并分别在 API 26 的 phone、tablet、2in1 模拟器完成安装、启动和页面取证。准确口径为“在 API 26 运行环境验证”工程 target SDK 尚未升级到 API 26。五、实测结果与一个失败路径环境实际窗口表现导航图鉴其他观察Pura X View / phone1320×2232px 竖屏浮动底栏单列页尾内容不被底栏遮挡MatePad Pro 13 / tablet2880×1920px 横屏侧栏双列个人页双栏MateBook Pro / 2in13120×2080px 桌面、应用窗口化侧栏双列随窗口而非设备名决策规则窗口 1536×960vp宽窗口侧栏三列页面宽度封顶 1360vp规则窗口 960×480vp宽而矮浮动底栏双列高度门槛阻止侧栏挤压2in1 第一次安装曾返回user not exist当时设备仍停留在锁屏用户选择界面截图也是黑屏。选择user100后同一 HAP 安装和启动成功。这个失败不属于布局逻辑却提醒了取证流程的一条底线黑屏、过渡帧或未进入用户会话的画面都不能作为“多端运行成功”的证据。最终有效证据中手机展示单列与底栏平板展示侧栏和双栏2in1 展示自由窗口下的双列。它们没有刻意摆成三个越来越宽的全屏样板而是保留了真实窗口状态这比“按设备类型准备三张静态效果图”更能验证响应式逻辑。六、可以复用的四条经验第一测量窗口不猜设备。平板可以分屏2in1 可以自由缩放折叠屏也会改变可用区域设备名无法稳定代表页面空间。第二宽高要共同参与导航决策。宽而矮的窗口是最容易被单一宽度断点漏掉的反例。第三先算可用内宽再算列数。侧栏、最大内容宽度和页面边距都应在列数判断之前扣除否则不同页面会在相同窗口下错位切换。第四导航切换必须联动滚动净空。布局不仅是“看起来排成几列”还包括最后一项能否完整滚到用户眼前。当这些规则集中成纯决策函数后一套 ArkUI 页面允许各类窗口呈现不同形态同一份业务结构仍能获得合适的导航、密度与阅读节奏。可维护的“一多适配”依赖清晰且可测试的布局输入与边界设备分支无需藏进各个页面。