〖共创稿事节〗HarmonyOS 7 新特性实战(60):宽屏首页一行四卡——262×68 零拉伸与 840vp 判定链
# 〖共创稿事节〗HarmonyOS 7 新特性实战(60):宽屏首页一行四卡——262×68 零拉伸与 840vp 判定链
上一篇写了 EasyGo 从生效到下线,其中比例标定定稿 2|1:左栏 960vp 过 840vp 阈值,保住壳层的竖排导航。这条链还有个下游没讲——分栏把首页容器拉宽到 874vp 之后,首页自己也得回答「宽了怎么办」。
当时的首页是竖屏结构直接铺满:页头、同步横幅、三张进度卡、一张全宽的「已收录」知识卡。三张进度卡被拉宽后数字孤零零悬在半空,「已收录」卡更是灾难——wide_card 底图按 3.86:1 设计,全宽 874vp 意味着横向拉伸 4.9 倍,中间纸纹糊成一条,四角云纹也跟着变形。给平板单独设计一版首页?工程量和维护成本都不值。最后的答案是一条 Row:三张进度卡占左,「已收录」占右,一行四卡。
一、宽屏判定:不新增断点,复用壳层那条链
改版前先回答「谁是宽屏」。项目里已经有一条阈值:壳层竖排导航的 840vp 判定(EasyGo 比例标定就是围着它转的)。首页不再发明第二个断点——断点体系应当收敛而不是发散(官方「一多」断点适配指南同样是按窗口宽度划档),直接挂在同一条链上:
// common/layout/ShanhaiResponsive.ets —— 宽屏判定与壳层形态同源
static isWideScreen(windowWidthVp: number, windowHeightVp: number = 800): boolean {
return ShanhaiResponsive.usesSideNavigation(windowWidthVp, windowHeightVp);
}
// pages/Index.ets —— 同一个窗口状态函数喂给所有页面
private isWideScreen(): boolean {
return ShanhaiResponsive.isWideScreen(this.screenWidthVp, this.screenHeightVp);
}
判定链的效果是一体的:平板 2|1 分栏 → 左栏 960vp 过 840 阈值 → 壳层竖排导航成立 → isWideScreen 为 true → 首页切宽行。壳层形态和内容密度由同一个布尔值驱动,不会出现「壳层已经竖排了、首页还在铺竖屏结构」的错位。手机上阈值不过,isWideScreen 为 false,什么都不变。
这条链有个此前埋下的好处:840 阈值在 EasyGo 时代被三档比例反复验证过(720vp 底部 tab、960vp 竖排导航),它的两侧行为都是实测过的,不是拍脑袋划的一条线。
二、一行四卡:空间从哪来
宽屏分支的全部结构:
if (this.isWideScreen) {
Row({ space: 10 }) {
ShanhaiStatisticCard({ title: '已发现', value: this.discoveredCount.toString(),
note: '神兽图谱', isHome: true, isDarkMode: this.isDarkMode, isWideScreen: this.isWideScreen })
ShanhaiStatisticCard({ title: '护照印章', value: this.passportCount.toString().padStart(2, '0'),
note: '地域通行', isHome: true, isDarkMode: this.isDarkMode, isWideScreen: this.isWideScreen })
ShanhaiStatisticCard({ title: '已读篇章',
value: shanhaiReadChapterCountOf(this.readChaptersRaw).toString() + '/' + shanhaiChapterTotal().toString(),
note: '古籍原文', isHome: true, isDarkMode: this.isDarkMode, isWideScreen: this.isWideScreen })
ShanhaiKnowledgeStructureCard({
isDarkMode: this.isDarkMode,
isWideRow: this.isWideScreen, // 宽行里固定 262×68
onOpenLayer: (action: string) => {
this.onOpenKnowledgeLayer(action);
}
})
}
.width('100%')
.padding({ top: 8, left: this.horizontalInset, right: this.horizontalInset })
// 展开后这一行会被撑高,三张进度卡必须顶对齐,否则被居中到很高 Row 的中间
.alignItems(VerticalAlign.Top)
}
窄屏分支保持原堆叠结构,两种形态共享同一批卡片组件与同一个 isWideScreen 入参:
} else {
Column({ space: 8 }) {
Row({ space: 10 }) {
ShanhaiStatisticCard({ title: '已发现', /* … */ isWideScreen: this.isWideScreen })
ShanhaiStatisticCard({ title: '护照印章', /* … */ isWideScreen: this.isWideScreen })
ShanhaiStatisticCard({ title: '已读篇章', /* … */ isWideScreen: this.isWideScreen })
}
.width('100%')
.padding({ top: 8 })
ShanhaiKnowledgeStructureCard({ isDarkMode: this.isDarkMode,
onOpenLayer: (action: string) => {
this.onOpenKnowledgeLayer(action);
}
})
}
.width('100%')
.padding({ left: this.horizontalInset, right: this.horizontalInset })
.alignItems(HorizontalAlign.Start)
}
进度卡侧的适配只是微调字号与最小高度,没有结构性改动:
// ShanhaiStatisticCard.ets —— 宽屏仅两处取值变化
.fontSize(!this.isHome && this.isWideScreen ? 24 : 22)
.constraintSize({ minHeight: this.isHome ? 68 : (this.isWideScreen ? 82 : 76) })
宽度账很简单:左栏内容区 874vp,减掉左右内边距和三个 10vp 间距,四张卡各分到约 262vp。三张进度卡本来就是这个宽度量级(手机上三卡并排各约 105vp,宽屏各 262vp 是它们的舒适区),所以进度卡侧只需要把 isWideScreen 透传下去微调内距,没有结构性改动。
真正要解决的是第四张卡——「已收录」知识卡。它在窄屏是全宽卡,进宽行之前必须回答「我以什么尺寸站进这一行」。
三、262×68:一个数字的两重身份
知识卡头部是宽卡底图,3.86:1。它进宽行的解法是固定宽度 262:
// 宽屏并排时的固定宽度 262(高度由 Header 的 aspectRatio 3.86 自动算出 ≈68),底图零拉伸。
private wideRowCardWidth: number = 262;
// 收起态 Header 宽度分支只有这一处:
.width(this.isWideRow ? this.wideRowCardWidth : '100%')
262 不是凑出来的整数。收起态高度原本固定 68vp,262 ÷ 3.86 ≈ 67.9 ——固定 262 宽之后,高度由 aspectRatio(3.86) 算出来恰好还是 68,与历史高度逐像素一致。这也是源码注释里那句「所以不必分支」的底气:isWideRow 只改宽度,高度、底图、九宫格 slice、内容全部共用窄屏逻辑,宽窄两种形态用同一份代码。
对照两种坏方案:
| 方案 | 底图横向拉伸 | 结果 |
|---|---|---|
| 改前:窄屏堆叠铺进宽屏(全宽 874vp) | 4.9× | 纸纹糊成一条,四角云纹变形 |
| 反例:手机全宽 340vp 维持堆叠 | 1.9× | 有拉伸但云纹完整,可接受 |
| 宽行固定 262×68 | 1.0× | 零拉伸,与设计稿逐像素一致 |
注意第二行——手机上这行代码不生效,卡片仍全宽 340vp、拉伸 1.9 倍,我们选择接受。理由是收益不对称:手机的 1.9 倍拉伸在真机上肉眼看不出纸纹劣化(九宫格 slice 包住了云纹,拉伸只发生在中间纸纹上);而给手机也套 262 固定宽,两侧留白反而破坏首页满宽的观感。宽屏的 4.9 倍则完全不同,拉伸量大到纸纹方向性都糊掉了。

四、实测:dumpLayout 里的这条 Row
平板(2880×1920,2|1 分栏态)上 uitest dumpLayout 的硬数字:
| 节点 | bounds | 换算 |
|---|---|---|
| 宽行 Row | [789,532][2537,677] | 874×72.5vp |
| 四张卡(各) | 宽 524px | 262vp |
| 「已发现」值区 | 高 49px + 标签 22px + 注 22px | 卡内三行结构原样 |
| 「已收录」总账 | 20 篇 · 912 段 · 1,762 条旧注,单行 33px | 折叠态在 262 宽下不换行 |
最后一条值得单独说:折叠态总账是知识卡里最长的一行字,262vp 宽度下它仍然单行放下——固定宽度不是拍脑袋缩的,缩之前先确认了最长内容在目标宽度下不折行、不截断。同时这份 dump 是在右栏打开阅读页的状态下抓的:首页宽行与分栏右栏的篇章阅读共存无干扰,宽行改造没有为适配而牺牲任何一边。
另一个实测点是判定链的端到端一致性:同一状态下,壳层侧导航成立(左栏 960vp ≥ 840)与首页宽行成立来自同一个 isWideScreen() 返回值,dumpLayout 里两者同时出现——链路上没有第二个会撒谎的布尔值。

五、坑:展开把行撑高,三卡悬在半空
宽行上线两天后随知识卡展开改造暴露出一个错位:知识卡是手风琴,展开后内容约 330vp 高,把整行 Row 撑高——而 Row 默认 alignItems 是 VerticalAlign.Center,三张进度卡被垂直居中到这个很高的 Row 中间,与卡片头部错开一大截,视觉上像悬在半空。
修复是一行:
// 2026-10-27:已收录卡展开后会把这一行撑高,三张进度卡必须顶对齐,
// 否则它们会被居中到很高的 Row 中间、和卡片头部错位。
.alignItems(VerticalAlign.Top)
修复前后的对照:
| 状态 | Row 高度 | alignItems | 三卡位置 |
|---|---|---|---|
| 收起 | ≈72.5vp | Center / Top 无差异 | 与知识卡头部齐平 |
| 展开 + Center(改前) | ≈400vp | 居中 | 悬在行中央,头部错位 |
| 展开 + Top(改后) | ≈400vp | 顶部 | 与知识卡头部齐平 |
这类「默认值在窄屏没事、宽屏撑高后才暴露」的对齐问题,是布局改造里最容易漏的一类:收起态两个取值视觉完全相同,只有展开才见分晓。它没有进模拟器首验清单,是展开功能上线后人工点出来的。

六、门禁欠账与收尾
要诚实记录:262×68 这个不变量目前由源码注释和 wideRowCardWidth 常量把守,alignItems(VerticalAlign.Top) 同样没有进 verify-home-knowledge-ledger.mjs 的断言清单——这两个宽屏特有约束的门禁化是明确的欠账。本系列的惯例是「凡是评审时看着对的东西都要留一条会红的断言」,宽行这条链上,expanded: false、数字真源、九宫格都有断言,唯独宽行本身还没有。谁先动 wideRowCardWidth 或删掉那行 alignItems,现有门禁都不会红。

收个尾。这次改造的全部增量:一个复用 840 阈值的判定、一个 if (this.isWideScreen) 分支、一个 isWideRow Prop、一个 262 常量、一行 alignItems(Top)。没有给平板单独设计首页,没有新增断点体系,窄屏零改动。它成立的根基是前两篇埋的两条底座:九宫格让同一张底图在 262×68 上零拉伸可用,840 阈值让宽屏判定不用重新发明。宽屏适配最好的形态不是多设计一版,而是让既有结构在正确的判定链上换一种排布——前提是那条判定链本身被实测过。
附:本篇证据清单
- 布局实测:
acceptance/2026-10-26-home-wide-row/layout_dump.json(宽行 Row 1748×145px、四卡各 524px、折叠总账单行 33px,右栏开阅读页共存状态抓取)、layout_reader_now.json(分栏右栏阅读页对照)。 - 源码:
ShanhaiHomePageContent.ets(宽行分支与alignItems(Top),含 2026-10-21/10-26/10-27 三段决策注释)、ShanhaiKnowledgeStructureCard.ets(isWideRow/wideRowCardWidth: 262/ 高度不分支)、ShanhaiResponsive.ets(isWideScreen=usesSideNavigation,840vp 阈值)。 - 关联稿件:840vp 阈值的标定过程与 2|1 定稿见系列第 59 篇;知识卡本体(手风琴、九宫格底图、展开浮层坑)见系列第 58 篇。
- 已知欠账:
wideRowCardWidth与alignItems(VerticalAlign.Top)尚无门禁断言;手机 1.9× 拉伸为记录在案的接受项,非疏漏。
更多推荐


所有评论(0)