在这里插入图片描述
在这里插入图片描述

一、前置思考

HarmonyOS作为"万物互联"操作系统的核心优势之一就是一次开发,多端部署——同一套代码可以运行在手机、平板、折叠屏、车机、智慧屏上。但"多端部署"不是简单的屏幕拉伸,而是需要设计一套自适应系统来处理不同屏幕尺寸、折叠状态、输入模式下的UI布局。

真实场景挑战

  1. 手机竖屏(360vp宽) → 列表全屏、单列布局
  2. 折叠屏展开(680vp宽) → 双栏Master-Detail布局
  3. 平板横屏(1024vp宽) → 三栏布局 + 侧边导航
  4. 折叠屏帐篷模式 → 上下分屏显示不同内容
  5. 车机横屏 → 左右分区 + 驾驶模式简化UI

一套代码如何优雅地处理以上所有场景? 答案是断点体系(Breakpoint System)+ 响应式布局

二、断点体系详解

2.1 标准断点定义

HarmonyOS的断点体系将屏幕宽度划分为4级:

断点 宽度范围 典型设备 推荐布局
sm (Small) 0~320vp 手表/小型手机 单列垂直列表 + 底部导航
md (Medium) 320~600vp 手机竖屏 双列网格 + 顶部导航
lg (Large) 600~840vp 折叠屏展开/手机横屏 双栏布局(Master-Detail)
xl (xLarge) 840+vp 平板/智慧屏/车机 三栏布局 + 侧边栏

关键理解:vp(virtual pixel)是虚拟像素单位,与屏幕物理分辨率和DPI无关。在密度为3.0的设备上,1vp = 3px。这使得320vp在任何设备上都代表"物理上一指宽",保证了设计的一致性。

2.2 断点获取方式

import { display } from '@kit.ArkUI';

// 获取屏幕宽度
const defaultDisplay: display.Display = display.getDefaultDisplaySync();
const screenWidth: number = defaultDisplay.width;

// 根据宽度计算断点
function getBreakpoint(width: number): string {
  if (width < 320) {
    return 'sm';
  }
  if (width < 600) {
    return 'md';
  }
  if (width < 840) {
    return 'lg';
  }
  return 'xl';
}

Demo中使用了模拟断点的方式(通过Slider手动调整容器宽度),这在实际开发中非常有用——可以在不连接不同设备的情况下预览各种断点下的布局效果。

2.3 断点与布局策略映射

const BP_HINTS: Record<string, string> = {
  'sm': '适合单列垂直列表',
  'md': '适合双列网格',
  'lg': '适合双栏布局:左侧列表 + 右侧详情',
  'xl': '适合多栏布局:三列网格 + 侧边栏',
};

断点切换触发的事件链

设备旋转/折叠展开
  → display.width 变化
    → getBreakpoint() 返回新断点
      → UI根据新断点切换布局模式
        → 组件重新build()

三、响应式布局策略

3.1 单栏 → 双栏 → 三栏演进

单栏(sm/md):全屏列表,点击item跳转详情页

┌──────────────────┐
│   Header          │
├──────────────────┤
│  Item 1           │
│  Item 2           │
│  Item 3           │
│  Item 4           │
│  Item 5           │
└──────────────────┘

双栏 Master-Detail(lg):左侧列表 + 右侧详情,无需跳转

┌────────┬─────────┐
│ Header │ Header  │
├────────┼─────────┤
│ Item 1 │         │
│ Item 2 │ Detail  │
│ Item 3 │ Content │
│ Item 4 │         │
└────────┴─────────┘

三栏(xl):侧边导航 + 列表 + 详情

┌────┬──────┬───────┐
│Nav │ List │Detail │
│ A  │ I1   │       │
│ B  │ I2   │ 选中  │
│ C  │ I3   │ I4的  │
│ D  │ I4   │ 详情  │
└────┴──────┴───────┘

3.2 响应式Grid列数

Grid的列数应该随断点动态调整:

// 根据断点计算最优列数
private getGridColumns(): number {
  switch (this.currentBreakpoint) {
    case 'sm': return 1;   // 小屏一列
    case 'md': return 2;   // 中屏两列
    case 'lg': return 3;   // 大屏三列
    case 'xl': return 4;   // 超大屏四列
    default:   return 2;
  }
}

配合Grid的响应式宽度:

Grid() {
  ForEach(MOCK_ITEMS, (item: DemoItem) => {
    GridItem() {
      // 卡片内容
    }
  })
}
.columnsTemplate('1fr '.repeat(this.getGridColumns())) // 等分列
.columnsGap(12)
.rowsGap(12)

3.3 弹性布局 vs 固定布局

固定宽度(不推荐)

.width(360)  // 在平板上显示为窄条,浪费屏幕

弹性宽度(推荐)

.width('100%')  // 撑满可用空间
.padding({ left: 16, right: 16 })  // 固定边距

比例宽度(推荐)

// Master占40%,Detail占60%
.width(this.currentBreakpoint === 'lg' ? '40%' : '100%')

3.4 布局切换实现

Demo中的核心逻辑——根据断点渲染不同布局:

@Builder
buildResponsiveLayout() {
  if (this.currentBreakpoint === 'sm' || this.currentBreakpoint === 'md') {
    // 单栏/双列网格
    this.buildGridLayout()
  } else if (this.currentBreakpoint === 'lg') {
    // 双栏Master-Detail
    this.buildSplitLayout()
  } else {
    // 多栏布局
    this.buildMultiColumnLayout()
  }
}

关键设计:不同断点的布局是完全独立的Builder,互不干扰。这比在一个Builder中用大量if/else调整属性要清晰得多。

四、折叠屏适配

4.1 折叠状态API

HarmonyOS提供display.getFoldStatus()获取折叠状态:

import { display } from '@kit.ArkUI';

// 获取折叠状态
const foldStatus: display.FoldStatus = display.getFoldStatus();

// 折叠状态枚举
// FOLD_STATUS_UNKNOWN   = 0  (未知)
// FOLD_STATUS_EXPANDED  = 1  (完全展开)
// FOLD_STATUS_FOLDED    = 2  (完全折叠)
// FOLD_STATUS_HALF_FOLDED = 3 (半折叠/帐篷模式)

4.2 折叠状态与UI适配

折叠状态 典型场景 UI策略
EXPANDED(展开) 平板模式 切换到大屏布局(双栏/三栏)
FOLDED(折叠) 单屏手机模式 使用md断点布局
HALF_FOLDED(半折叠) 帐篷模式/支架模式 上下分区,上半显示内容,下半显示控件

4.3 折叠屏的特殊考量

  1. 折痕区域:折叠屏中间有一道物理折痕,避免在这条线上放置可点击的交互元素
  2. 宽度瞬间变化:从折叠→展开,宽度可能从360vp跳到680vp,动画过渡需要平滑处理
  3. 状态保持:折叠/展开时,用户当前的操作上下文(滚动位置、选中项)应保持不变
// 折叠状态变化时保持选中状态
onFoldStatusChange(newStatus: display.FoldStatus): void {
  const selectedId: number = this.selectedItemId; // 保存
  this.isFoldExpanded = (newStatus === 1);
  // 更新断点 → 切换布局
  this.selectedItemId = selectedId; // 恢复
}

五、Master-Detail 模式完整实现

5.1 双栏布局结构

这是折叠屏展开/平板横屏时最经典的布局模式:

@Builder
buildSplitLayout(): void {
  Row() {
    // 左侧:列表(40%宽度)
    Column() {
      List() {
        ForEach(MOCK_ITEMS, (item: DemoItem) => {
          ListItem() {
            this.buildListItem(item)
          }
          .onClick(() => {
            this.selectedItemId = item.id;
          })
        })
      }
    }
    .width('40%')
    .border({ width: { right: 1 }, color: 'rgba(255,255,255,0.08)' })

    // 右侧:详情(60%宽度)
    Column() {
      this.buildDetailPanel(this.getSelectedItem())
    }
    .width('60%')
  }
  .width('100%')
  .layoutWeight(1)
}

5.2 选中状态双向同步

在Master-Detail模式下,列表选中项必须和详情面板内容保持一致:

@State selectedItemId: number = -1; // 共享状态

// 列表侧:点击item设置selectedItemId
// 详情侧:根据selectedItemId渲染内容

getSelectedItem(): DemoItem | undefined {
  if (this.selectedItemId === -1) {
    return undefined;
  }
  for (let i: number = 0; i < MOCK_ITEMS.length; i++) {
    if (MOCK_ITEMS[i].id === this.selectedItemId) {
      return MOCK_ITEMS[i];
    }
  }
  return undefined;
}

5.3 单栏模式下的差异处理

在单栏(sm/md)模式下,点击item应跳转详情或展开内联内容:

// 单栏模式:网格展示,点击弹出详情或其他交互
// 双栏模式:列表+详情并排

六、横竖屏切换处理

6.1 横竖屏检测

// 通过宽高比判断横竖屏
const aspectRatio: number = displayWidth / displayHeight;
const isLandscape: boolean = aspectRatio > 1;
const isPortrait: boolean = aspectRatio <= 1;

6.2 横屏适配要点

关注点 竖屏 横屏
导航位置 底部TabBar 左侧侧边栏(空间更多)
列表布局 单列/双列 多列Grid
搜索栏 顶部独立一行 和标题合并在一行
Header高度 较大(纵向空间充裕) 紧凑(横向空间换纵向空间)
// Header高度根据横竖屏动态调整
.headerHeight(isLandscape ? 48 : 64)

6.3 状态保存

横竖屏切换时系统会重建Activity,必须保存当前状态:

aboutToDisappear(): void {
  // 保存关键状态
  AppStorage.setOrCreate('savedSelectedId', this.selectedItemId);
  AppStorage.setOrCreate('savedScrollOffset', this.scrollOffset);
}

aboutToAppear(): void {
  // 恢复状态
  const savedId: number =
    AppStorage.get<number>('savedSelectedId') as number ?? -1;
  this.selectedItemId = savedId;
}

七、多端输入适配

7.1 输入模式检测

不同设备有不同的输入模式:

设备 主输入模式 特殊处理
手机 触摸 点击区域≥44vp
平板 触摸 + 键盘 支持键盘快捷键
车机 触摸 + 语音 TTS语音反馈
智慧屏 遥控器 焦点导航
手表 触摸 + 表冠 旋转输入适配

7.2 焦点管理(非触摸设备)

// 遥控器/键盘导航时的焦点样式
.focusable(true)
.defaultFocus(this.currentBreakpoint === 'xl') // 大屏默认获取焦点
.onFocus(() => {
  // 聚焦时的样式
  this.isFocused = true;
})
.onBlur(() => {
  this.isFocused = false;
})

7.3 点击区域适配

// 移动端最小触摸区域 44vp(Apple HIG标准)
.hitTestBehavior(HitTestMode.Block)
.minHitWidth(44)
.minHitHeight(44)

八、避坑速查

现象 原因 解决
断点检测不实时 折叠后布局没更新 只在aboutToAppear检测一次 onAreaChange监听宽度变化
列表/Scroll嵌套方向冲突 双栏内外都Scroll,滑动异常 同方向Scroll嵌套 给内层Scroll固定高度或换成List
平板布局闪回手机 旋转后布局短暂错误 重建期间用了默认断点值 在aboutToAppear中优先恢复保存的断点
Master-Detail选中不同步 点了列表项,详情不变 selectedItemId没触发详情刷新 确保selectedItemId是@State/@Local
Grid列数计算错误 大屏下列数不够或过多 死板的列数计算 根据containerWidth动态计算,而非固定断点
折叠屏折痕遮挡 折痕处的按钮点击无响应 物理遮挡 避免在屏幕中线区域放置交互元素
状态在切换中丢失 折叠后滚动位置重置 组件重建 用AppStorage保存关键状态并在重建后恢复

九、总结

多端自适应的核心思想是以断点为锚点,以弹性布局为手段。不要为每个设备写一套UI,而是让UI根据断点自适应变化。

实施Checklist

  1. ✅ 定义清晰的断点体系(sm/md/lg/xl),每种断点对应一种布局策略
  2. ✅ 使用百分比/弹性单位('100%'layoutWeight),而非固定vp
  3. ✅ 为折叠屏的瞬间宽度变化提供平滑过渡(状态先于布局更新)
  4. ✅ 用独立Builder封装不同断点的布局,避免巨型if/else
  5. ✅ 横竖屏/折叠切换时保存关键状态(选中项、滚动位置)
  6. ✅ 在不同设备上实测,不要在单一机型上"脑补"效果

记住:好的自适应架构让用户在手机、折叠屏展开、平板横屏之间无缝切换时,感觉不到"换了一个设备"——只是空间大了,信息更密了,但不乱

对应Demo文件:entry/src/main/ets/pages/MultiDeviceDemo.ets

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐