HarmonyOS 7 / API 26 折叠屏断点布局实战:窗口宽度变化、列表重排和状态保留怎么验

HarmonyOS 7 / API 26 做多设备应用开发时,折叠屏、平板和鸿蒙电脑窗口不只是“屏幕变宽”。真正麻烦的是窗口宽度一变,页面结构、列表列数、详情区域、选中状态和返回位置都会一起变。
很多页面在手机竖屏上没问题,到了折叠屏展开态就开始露馅:列表从一列变两列以后状态串了,详情页回到列表时滚动位置丢了,窗口拖动后当前选中的内容又被重置了。这个问题不能只靠响应式样式兜,需要把断点、布局和状态边界一起设计。
先把适配目标说清楚
我会把多设备断点适配拆成四个问题:
- 当前窗口属于紧凑、中等还是展开态;
- 列表是一列、两列还是主从布局;
- 断点切换时哪些状态必须保留;
- 返回页面时滚动位置和选中项能不能恢复。
如果只根据屏幕宽度临时改 UI,很容易出现“样式变了,状态没跟上”的问题。更稳的做法是把布局模式变成一个明确的状态,再让页面根据这个状态决定结构。
案例一:列表列数变化后选中状态丢失
先看一个容易出问题的写法:
@Builder
ItemCard(item: FoodItem) {
Row() {
Text(item.name)
if (this.selectedIndex === item.index) {
Text('已选')
}
}
}
这里用 `index` 判断选中,看着没问题,但一旦折叠屏展开后列表从一列变两列,排序、筛选或瀑布流重排都会改变 index。最后页面显示出来的“已选”可能不是原来那个对象。
我会改成稳定 id:
class SelectionStore {
private selectedId: string = ''
select(id: string) {
this.selectedId = id
}
isSelected(id: string): boolean {
return this.selectedId === id
}
}
页面渲染时只认业务 id,不认 index:
@Builder
ItemCard(item: FoodItem) {
Row() {
Text(item.name)
if (this.selectionStore.isSelected(item.id)) {
Text('已选')
}
}
.onClick(() => {
this.selectionStore.select(item.id)
})
}
这样窗口宽度变化、列表重排、分页补数据,都不会把选中状态带偏。
案例二:断点切换后滚动位置回不来
第二类问题是滚动位置。手机上列表一列,展开后变成两列,旧的滚动 offset 已经不能直接复用。如果还直接恢复像素位置,就可能跳到很奇怪的位置。
我更建议记录锚点,而不是只记录像素:
type ScrollAnchor = {
itemId: string
offsetInItem: number
layoutMode: 'compact' | 'medium' | 'expanded'
}
保存滚动状态时,记录当前可见的第一个 item:
class ScrollAnchorStore {
private anchor?: ScrollAnchor
save(anchor: ScrollAnchor) {
this.anchor = anchor
}
restore(currentMode: ScrollAnchor['layoutMode']): ScrollAnchor | undefined {
if (!this.anchor) {
return undefined
}
return {
...this.anchor,
layoutMode: currentMode,
}
}
}
恢复时先找到 `itemId`,再根据当前布局模式滚过去。这样一列变两列以后,页面仍然能回到同一个内容附近,而不是回到旧像素点。
用脚本先扫断点配置
下面这个脚本可以直接跑,检查断点配置有没有明显问题:
const breakpoints = [
{ mode: 'compact', minWidth: 0, columns: 1, masterDetail: false },
{ mode: 'medium', minWidth: 600, columns: 2, masterDetail: false },
{ mode: 'expanded', minWidth: 840, columns: 2, masterDetail: true },
]
const stateRules = [
{ name: 'selectedItem', keepWhenModeChange: true },
{ name: 'searchKeyword', keepWhenModeChange: true },
{ name: 'scrollAnchor', keepWhenModeChange: true },
{ name: 'temporaryHover', keepWhenModeChange: false },
]
function inspectBreakpoints(points, rules) {
const problems = []
for (let i = 1; i < points.length; i++) {
if (points[i].minWidth <= points[i - 1].minWidth) {
problems.push(`${points[i].mode} 的 minWidth 必须大于前一个断点`)
}
}
const mustKeep = rules.filter(rule => rule.keepWhenModeChange)
if (!mustKeep.find(rule => rule.name === 'selectedItem')) {
problems.push('断点切换时必须保留 selectedItem')
}
if (!mustKeep.find(rule => rule.name === 'scrollAnchor')) {
problems.push('断点切换时必须保留 scrollAnchor')
}
return {
passed: problems.length === 0,
breakpointCount: points.length,
keepStateCount: mustKeep.length,
problems,
}
}
console.log(JSON.stringify(inspectBreakpoints(breakpoints, stateRules), null, 2))
这段脚本会输出:
{
"passed": true,
"breakpointCount": 3,
"keepStateCount": 3,
"problems": []
}
如果后面有人改了断点顺序,或者把滚动锚点从保留状态里删掉,这个脚本能先报出来。
三种布局模式怎么选
| 布局模式 | 适合宽度 | 典型结构 | 状态重点 |
| compact | 手机竖屏 | 单列表 + 独立详情页 | 返回位置、搜索词 |
| medium | 折叠屏半展开、平板窄窗口 | 双列列表或宽卡片 | 列表重排、选中 id |
| expanded | 平板横屏、鸿蒙电脑窗口 | 主从布局 | 左侧列表选中、右侧详情同步 |
这里最容易出错的是 medium 和 expanded 的边界。页面一旦从“点列表进入详情”变成“左列表右详情”,状态就不能再散落在两个组件里,必须收口到一个 store 里。
ArkTS 里可以封装一个布局状态
type LayoutMode = 'compact' | 'medium' | 'expanded'
class AdaptiveLayoutState {
mode: LayoutMode = 'compact'
selectedId: string = ''
searchKeyword: string = ''
scrollAnchorId: string = ''
updateWidth(width: number) {
if (width >= 840) {
this.mode = 'expanded'
} else if (width >= 600) {
this.mode = 'medium'
} else {
this.mode = 'compact'
}
}
select(id: string) {
this.selectedId = id
}
saveAnchor(id: string) {
this.scrollAnchorId = id
}
}
页面只关心 `mode` 和状态,不要在每个组件里重新算一遍:
build() {
if (this.layoutState.mode === 'expanded') {
this.MasterDetailLayout()
} else {
this.SingleColumnList()
}
}
这样做的好处是,断点逻辑、选中状态、搜索词和滚动锚点都在同一个地方,后面改布局时不会到处找状态。
发布前我会检查哪些点
| 检查项 | 合格标准 |
| 断点范围 | minWidth 从小到大,没有重叠和倒挂 |
| 选中状态 | 使用稳定业务 id,不使用列表 index |
| 滚动恢复 | 记录 itemId 锚点,不只记录像素 offset |
| 主从布局 | 左侧列表和右侧详情共享同一份状态 |
| 搜索筛选 | 断点切换后关键词和筛选条件不丢 |
| 异常兜底 | 大屏无数据、详情为空、窗口缩回都有兜底 |
后面怎么避免
我会把折叠屏和平板适配提前放到页面设计阶段,而不是等页面写完再补:
- 新页面先定义 compact、medium、expanded 三种模式;
- 每个模式都画出页面结构;
- 选中、搜索、滚动锚点这些状态先确定归属;
- 列表渲染只用稳定 id,不用 index 当状态依据;
- 本地脚本检查断点和必须保留的状态;
- 真机或模拟器上验证窗口变宽、缩窄、返回和后台恢复。
多设备适配不是简单把卡片拉宽。真正要守住的是状态连续性:窗口变了,用户正在看的内容、选中的项、搜索条件和返回位置都不能乱。
更多推荐



所有评论(0)