山海万灵 HarmonyOS 文化知识实战(02):响应式侧栏与多设备内容宽度

📅 2026/8/3 5:49:37 👁️ 阅读次数 📝 编程学习
山海万灵 HarmonyOS 文化知识实战(02):响应式侧栏与多设备内容宽度

把“放大页面”改成“重算可用空间”

文化知识应用的首页同时容纳探索路线、统计卡片、地图和底部导航。手机上最重要的是让单手浏览的内容不拥挤;屏幕变宽后,如果仍把全部区域按比例拉伸,卡片之间会出现大块空白,正文也会变成难以扫读的长行。真正需要响应的是页面结构,而不是给每个组件单独写一组宽度。

山海万灵把窗口宽高交给一个布局计算类。页面壳层只保存当前的screenWidthVpscreenHeightVp,首页、图鉴、探索、馆长和“我的”都从同一个入口取得侧边距、内容宽度、卡片布局和底部安全留白。这样一来,切换一级入口或进入详情时不会重新发明断点规则,横向空间也不会被多个页面各自扣减。

窗口条件页面壳层动作内容区结果
宽度不足以放下侧栏保留底部导航,内容从完整窗口宽度计算保持紧凑的阅读和点击节奏
宽度与高度同时达到侧栏阈值为侧栏预留固定占位主内容不被导航覆盖
内容区超过页面上限继续使用居中的最大内容宽度地图和说明文字不会无限拉长

用宽度和高度共同决定导航形态

只看宽度容易在矮窗口里误开侧栏:导航虽然能排下,内容和底部操作区却会互相挤压。因此侧栏判断同时约束宽度与高度。宽屏条件满足时,壳层给侧栏留下 109vp 的占位;不满足时,这一占位为零,内容可以完整使用窗口宽度。

const SHANHAI_SIDE_NAV_MIN_WIDTH: number = 840; const SHANHAI_SIDE_NAV_MIN_HEIGHT: number = 520; const SHANHAI_SIDE_NAV_FOOTPRINT: number = 109; static usesSideNavigation(windowWidthVp: number, windowHeightVp: number): boolean { return windowWidthVp >= SHANHAI_SIDE_NAV_MIN_WIDTH && windowHeightVp >= SHANHAI_SIDE_NAV_MIN_HEIGHT; } static shellContentWidth(windowWidthVp: number, windowHeightVp: number): number { const sideNavigationWidth: number = ShanhaiResponsive.usesSideNavigation(windowWidthVp, windowHeightVp) ? SHANHAI_SIDE_NAV_FOOTPRINT : 0; return Math.max(0, windowWidthVp - sideNavigationWidth); }

这个判断把“导航在哪里”从各业务页面中移走。首页不需要知道设备类别,图鉴也不需要自行扣除侧栏宽度;它们只关心壳层已经算出的可用内容区。窗口高度不足时,页面会自然回到不占侧栏宽度的形态,避免一套只适合桌面比例的布局出现在窄高场景中。

内容宽度、内边距与可读行长分开计算

内容区宽度并不等于组件真实可用宽度。先扣除导航占位,再以页面最大宽度限制阅读区域,最后根据断点计算左右边距,才能保证标题、地图说明和卡片正文都落在稳定的行长范围内。这里的“最大宽度”是阅读约束,不是设备限制;更大的窗口仍然可以展示更多背景与导航,只是不让文字和交互卡片无限横向扩张。

const SHANHAI_DEFAULT_PAGE_MAX_WIDTH: number = 920; const SHANHAI_COMPACT_SIDE_INSET: number = 14; const SHANHAI_MEDIUM_SIDE_INSET: number = 20; const SHANHAI_WIDE_SIDE_INSET: number = 24; static pageSideInset(windowWidthVp: number): number { if (windowWidthVp >= 840) return SHANHAI_WIDE_SIDE_INSET; if (windowWidthVp >= 600) return SHANHAI_MEDIUM_SIDE_INSET; return SHANHAI_COMPACT_SIDE_INSET; } static pageContentWidth(windowWidthVp: number, windowHeightVp: number): number { return Math.min( ShanhaiResponsive.shellContentWidth(windowWidthVp, windowHeightVp), SHANHAI_DEFAULT_PAGE_MAX_WIDTH ); } static pageInnerWidth(windowWidthVp: number, windowHeightVp: number): number { const pageWidth: number = ShanhaiResponsive.pageContentWidth(windowWidthVp, windowHeightVp); return Math.max(0, pageWidth - ShanhaiResponsive.pageSideInset(windowWidthVp) * 2); }
计算层次输入输出解决的问题
壳层窗口宽高、侧栏判定shellContentWidth避免导航与正文重叠
页面壳层宽度、页面最大宽度pageContentWidth控制长行文本与地图跨度
组件页面宽度、左右内边距pageInnerWidth给卡片、网格和按钮稳定边界

卡片列数从剩余宽度推导

探索、馆长推荐和图鉴并不共用同一种卡片。通用功能卡以 420vp 为基准,图鉴卡允许在更宽的内容区排成三列。两类组件都不直接读取屏幕宽度,而是先使用已经扣除侧栏与内边距后的pageInnerWidth。这能避免一个页面刚打开侧栏,另一个页面仍按旧宽度排两列的错位。

static cardLayoutMode( windowWidthVp: number, windowHeightVp: number, baseCardWidth: number, gap: number = 12 ): ShanhaiCardLayoutMode { const availableWidth: number = ShanhaiResponsive.pageInnerWidth(windowWidthVp, windowHeightVp); if (availableWidth < baseCardWidth) return 'COMPACT'; if (availableWidth >= baseCardWidth * 2 + gap) return 'PAIR'; return 'SCALED_SINGLE'; } static archiveColumns(windowWidthVp: number, windowHeightVp: number): number { const availableWidth: number = ShanhaiResponsive.pageInnerWidth( windowWidthVp, windowHeightVp, 1360 ); if (availableWidth >= 420 * 3 + 12 * 2) return 3; if (availableWidth >= 420 * 2 + 12) return 2; return 1; }

COMPACTSCALED_SINGLEPAIR把卡片的呈现选择限制在少量可解释状态:空间不够时维持紧凑卡片;中间区间使用单列缩放;满足两张基准卡片与间距后再进入双列。图鉴使用同样的可用宽度原则,但可根据档案页较大的内容上限返回一、二、三列。组件因此不必硬编码“手机”“平板”名称,也能随分屏和浮窗的实际空间变化。

页面壳层只分发结果,不复制规则

窗口变化发生在最外层容器。区域变化回调更新宽高状态后,首页把已计算好的isWideScreen、内容宽度、边距和底部留白传给内容组件;图鉴、探索和馆长页面则接收各自需要的网格或卡片模式。布局计算类保持纯函数,使组件在状态刷新时只消费结果,不在渲染过程中修改设备状态。

.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; } }) ShanhaiHomePageContent({ isWideScreen: this.isWideScreen(), contentWidth: this.pageContentWidth(), horizontalInset: this.pageSideInset(), bottomInset: this.pageBottomInset(), panelItemWidth: this.adaptivePanelItemWidth() })

手机运行界面中,探索路线、三项统计、地图和底部五个入口处于同一内容节奏,底部导航没有压住“继续探索”操作区。进入更大的可用窗口时,导航形态、边距、内容上限和卡片列数都沿着这条计算链重新得出,而不是由业务页面追加一套特例。

验收时关注空间契约而非单个像素

响应式页面的回归重点是“同一份状态在不同可用空间中仍能阅读和操作”。可依次检查:在窄窗口确认底部导航和操作区不覆盖;在满足侧栏阈值的窗口确认主内容已扣除导航占位;在图鉴页观察一、二、三列切换时项目宽度与间距是否稳定;在窗口变化后返回首页,确认探索进度和当前入口没有被布局刷新重置。

还有一个容易被忽略的边界:窗口宽度变化不应触发业务数据重载。布局函数只读取宽高并返回数字或枚举,探索进度、已发现神兽和当前区域仍由页面状态与数据层维护。这样在旋转、分屏或浮窗调整时,组件可以重新排版,却不会把用户带回初始路线,也不会因为一次尺寸变化重新请求内容。把尺寸计算与业务状态分离后,布局问题更容易定位:卡片过窄先检查可用宽度和间距,数据不刷新再检查状态链路,两类问题不会互相掩盖。

HarmonyOS 的响应式布局能力可以参考 官方响应式布局指南。把空间计算收口到页面壳层之后,后续增加地图详情、馆长推荐或知识图谱卡片时,只需选择适合的布局函数,而不需要在每个新页面重写设备分支。