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 当状态依据;
  • 本地脚本检查断点和必须保留的状态;
  • 真机或模拟器上验证窗口变宽、缩窄、返回和后台恢复。

多设备适配不是简单把卡片拉宽。真正要守住的是状态连续性:窗口变了,用户正在看的内容、选中的项、搜索条件和返回位置都不能乱。

Logo

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

更多推荐