三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

HarmonyOS 7 / API 26 折叠屏适配实战:窗口断点、双栏切换和状态保留一次验清

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

← 返回列表