HarmonyOS 7 API 26 折叠屏断点适配图

折叠屏适配最容易出问题的地方,不是页面能不能铺满屏幕,而是窗口变化以后状态还能不能保住。用户从外屏切到内屏,或者在平板、鸿蒙电脑窗口里拖动宽度,页面结构可能从单栏变成双栏。如果列表选中项、滚动位置、详情页参数都丢了,体验会很差。

这篇按 HarmonyOS 7 / API 26 的多设备适配思路来拆:先把窗口宽度转成断点,再把单双栏结构和业务状态分开,最后用两条可复现路径验证。

先定义断点,不要到处写魔法数字

我一般会先把窗口宽度分成三档:compact、medium、expanded。

断点 典型场景 页面结构
compact 手机竖屏、折叠屏外屏 单栏列表,详情新页打开
medium 折叠屏半展开、小平板 列表加轻量详情预览
expanded 平板横屏、鸿蒙电脑窗口 左列表右详情双栏

断点判断单独封装,后面页面只消费结果。

~~~ts

type WindowBreakpoint = 'compact' | 'medium' | 'expanded'

function getBreakpoint(widthVp: number): WindowBreakpoint {

if (widthVp < 600) {

return 'compact'

}

if (widthVp < 840) {

return 'medium'

}

return 'expanded'

}

~~~

这样做的好处是清楚。后面产品说 840 这个点要调整,也只改一个地方。

页面状态不要绑死在布局里

问题写法通常是:单栏页面有一套状态,双栏页面又有一套状态。窗口一变,组件重建,状态跟着丢。

~~~ts

type RecipeListState = {

selectedId?: string

scrollIndex: number

keyword: string

}

class RecipeStateStore {

private state: RecipeListState = {

selectedId: undefined,

scrollIndex: 0,

keyword: ''

}

snapshot(): RecipeListState {

return { ...this.state }

}

update(next: Partial<RecipeListState>): void {

this.state = { ...this.state, ...next }

}

}

~~~

这个 store 不关心当前是单栏还是双栏,它只保存“用户在看什么”。布局切换时,页面从这里恢复。

案例一:外屏单栏切到内屏双栏

单栏时用户点列表进入详情;展开后应该变成左列表右详情,而且仍然选中刚才那条。

~~~ts

class AdaptiveRecipePage {

private store = new RecipeStateStore()

private breakpoint: WindowBreakpoint = 'compact'

onWindowSizeChange(widthVp: number): void {

const next = getBreakpoint(widthVp)

if (next === this.breakpoint) {

return

}

const snapshot = this.store.snapshot()

this.breakpoint = next

this.renderByBreakpoint(snapshot)

}

selectRecipe(id: string): void {

this.store.update({ selectedId: id })

const snapshot = this.store.snapshot()

this.renderByBreakpoint(snapshot)

}

private renderByBreakpoint(state: RecipeListState): void {

if (this.breakpoint === 'compact') {

renderSingleColumn(state.selectedId)

return

}

renderTwoColumn(state.selectedId)

}

}

~~~

这里的关键是:窗口变化只改变布局,不重置 selectedId。状态和布局分开以后,单双栏切换就不会把用户当前选择冲掉。

案例二:拖动窗口宽度后滚动位置丢失

平板或鸿蒙电脑窗口拖动宽度时,列表可能重排。如果滚动位置没有单独保存,页面会跳回顶部。

~~~ts

class ListScrollKeeper {

private lastIndex = 0

onScroll(index: number): void {

this.lastIndex = index

}

restore(): number {

return this.lastIndex

}

}

class AdaptiveListController {

private scrollKeeper = new ListScrollKeeper()

onVisibleAreaChange(firstVisibleIndex: number): void {

this.scrollKeeper.onScroll(firstVisibleIndex)

}

onLayoutRebuild(): void {

const index = this.scrollKeeper.restore()

scrollToIndex(index)

}

}

~~~

滚动位置不一定要保存像素值。很多列表在数据稳定时,保存 firstVisibleIndex 更实用,适配重排后也更容易恢复。

断点切换要有日志

适配问题如果没有日志,很难知道到底是窗口事件没触发,还是触发后状态没恢复。

~~~ts

type BreakpointTrace = {

oldValue: WindowBreakpoint

newValue: WindowBreakpoint

widthVp: number

selectedId?: string

scrollIndex: number

}

function printBreakpointTrace(trace: BreakpointTrace): void {

console.info(

'[breakpoint]',

trace.oldValue + '->' + trace.newValue,

'width=' + trace.widthVp,

'selected=' + (trace.selectedId ?? 'none'),

'scroll=' + trace.scrollIndex

)

}

~~~

每次窗口变化都输出一次,回归时能直接看出状态是否被保住。

本地验证脚本

先不接真实 UI,也可以验证状态逻辑。

~~~ts

function verifyFoldableState(): void {

const store = new RecipeStateStore()

store.update({ selectedId: 'recipe-12', scrollIndex: 40, keyword: 'noodle' })

const compact = getBreakpoint(390)

const expanded = getBreakpoint(900)

const snapshot = store.snapshot()

console.info('[verify-breakpoint]', compact, expanded)

console.info('[verify-state]', snapshot.selectedId, snapshot.scrollIndex, snapshot.keyword)

}

~~~

预期结果是断点从 compact 到 expanded,但 selectedId、scrollIndex、keyword 都不丢。

方案对比

方案 好处 问题
布局里直接放状态 写起来快 窗口一变容易丢状态
全局 store 保存所有状态 恢复方便 容易过度设计
页面级状态仓库 状态清楚,适配成本低 需要维护快照和恢复入口

我更推荐页面级状态仓库。它比把状态散在组件里稳,也比把所有东西塞进全局 store 更轻。

回归检查

场景 操作 通过标准
外屏到内屏 选中一条后展开 双栏仍显示同一条详情
拖动窗口 列表滚动到中间再改宽度 滚动位置不回到顶部
搜索后切换 输入关键词再切断点 关键词和筛选结果保留
详情返回 双栏切回单栏后返回 路由不乱跳,不重复打开详情

小结

HarmonyOS 7 / API 26 做折叠屏和多窗口适配,重点不是把页面拉宽,而是窗口变化以后状态不丢。把断点判断、页面状态、滚动恢复和日志分开,单双栏切换就会稳定很多。后面再遇到折叠屏适配问题,也能直接从断点、状态、滚动三个位置排查。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐