HarmonyOS实战教程《台词拼图》(二)—— 响应式布局与断点系统

让同一套代码在手机竖屏、平板横屏、折叠屏上都呈现最佳布局。

一、为什么需要响应式布局?

HarmonyOS 运行在手机、平板、2in1、车机等多种设备上。同一段代码可能面对 320vp 的手机屏幕,也可能面对 1440vp 的平板屏幕。如果布局不做适配,要么手机上内容拥挤,要么平板上大片空白。

LineCard 的解决方案是 断点系统 + 条件渲染

二、断点系统架构

2.1 断点枚举定义

typescript 复制代码
// entry/src/main/ets/model/GlobalInfoModel.ets
export enum BreakpointTypeEnum {
  XS = 'xs',   // < 320vp
  SM = 'sm',   // 320~600vp
  MD = 'md',   // 600~840vp
  LG = 'lg',   // 840~1440vp
  XL = 'xl',   // >= 1440vp
}

// 断点大小排序(用于条件比较)
export const breakpointOrder: Record<BreakpointTypeEnum, number> = {
  [BreakpointTypeEnum.XS]: 0,
  [BreakpointTypeEnum.SM]: 1,
  [BreakpointTypeEnum.MD]: 2,
  [BreakpointTypeEnum.LG]: 3,
  [BreakpointTypeEnum.XL]: 4,
}

breakpointOrder 是一个巧妙的工具:通过数字大小比较,就能判断当前断点是否达到某个级别。例如:

typescript 复制代码
// 当前断点是否 >= LG(大屏)
breakpointOrder[this.globalInfoModel.currentBreakpoint] >= breakpointOrder[BreakpointTypeEnum.LG]

2.2 断点计算器:BreakpointSystem

typescript 复制代码
// entry/src/main/ets/util/BreakpointSystem.ets
export class BreakpointSystem {
  private static instance: BreakpointSystem;
  private currentBreakpoint: BreakpointTypeEnum = BreakpointTypeEnum.MD;

  public static getInstance(): BreakpointSystem {
    if (!BreakpointSystem.instance) {
      BreakpointSystem.instance = new BreakpointSystem();
    }
    return BreakpointSystem.instance;
  }

  public updateWidthBp(window: window.Window): void {
    const mainWindow = window.getWindowProperties();
    const windowWidthVp = px2vp(mainWindow.windowRect.width);

    let widthBp: BreakpointTypeEnum = BreakpointTypeEnum.MD;
    if (windowWidthVp < 320) {
      widthBp = BreakpointTypeEnum.XS;
    } else if (windowWidthVp < 600) {
      widthBp = BreakpointTypeEnum.SM;
    } else if (windowWidthVp < 840) {
      widthBp = BreakpointTypeEnum.MD;
    } else if (windowWidthVp < 1440) {
      widthBp = BreakpointTypeEnum.LG;
    } else {
      widthBp = BreakpointTypeEnum.XL;
    }

    this.updateCurrentBreakpoint(widthBp);
  }

  public updateCurrentBreakpoint(breakpoint: BreakpointTypeEnum): void {
    if (this.currentBreakpoint !== breakpoint) {
      this.currentBreakpoint = breakpoint;
      const globalInfoModel = AppStorage.get('GlobalInfoModel') || new GlobalInfoModel();
      globalInfoModel.currentBreakpoint = this.currentBreakpoint;
      AppStorage.setOrCreate('GlobalInfoModel', globalInfoModel);
    }
  }
}

设计要点:

  1. 单例模式 ------ 全局只有一个断点计算器实例。
  2. 窗口宽度计算 ------ 使用 px2vp 将像素转换为 vp,确保不同分辨率设备上断点一致。
  3. AppStorage 联动 ------ 断点变化时自动更新 GlobalInfoModel,所有使用 @StorageProp 的组件都会自动刷新。

2.3 监听窗口变化

WindowUtil 中注册窗口尺寸变化监听:

typescript 复制代码
// WindowUtil.ets - registerBreakPoint
public static registerBreakPoint(windowStage: window.WindowStage) {
  windowStage.getMainWindow((err, data) => {
    // 初始计算
    BreakpointSystem.getInstance().updateWidthBp(data);

    // 监听窗口变化(折叠屏展开/合拢、横竖屏切换)
    data.on('windowSizeChange', () => WindowUtil.onWindowSizeChange(data));
  });
}

public static onWindowSizeChange(window: window.Window) {
  WindowUtil.getDeviceSize(getContext());
  BreakpointSystem.getInstance().onWindowSizeChange(window);
}

这意味着:当用户在折叠屏上展开屏幕,或在平板上旋转方向时,断点会自动重新计算,UI 自动适配。

三、首页的 GridRow 响应式布局

首页功能卡片列表使用了 GridRow + GridCol 实现列数自适应:

typescript 复制代码
// Index.ets
GridRow({
  columns: { xs: 1, sm: 1, md: 1, lg: 2, xl: 2 },
  gutter: { x: 8, y: 16 },
  breakpoints: { reference: BreakpointsReference.ComponentSize },
}) {
  ForEach(this.features, (feature: Feature) => {
    GridCol({ span: { xs: 1, sm: 1, md: 1, lg: 1, xl: 1 } }) {
      this.FeatureCard(feature)
    }
  })
}

效果:

设备 断点 列数 效果
手机竖屏 SM 1 单列卡片
平板竖屏 MD 1 单列卡片
平板横屏 LG 2 双列卡片
大屏设备 XL 2 双列卡片

GridRow 是 HarmonyOS 提供的响应式栅格组件,它会根据断点自动调整列数,无需手动写条件判断。

四、功能页面的条件布局

功能页面(台词拼图、水印、分割)都采用了 条件渲染 实现小屏竖排、大屏横排的布局切换。

4.1 Flex 方向切换(StackLinePage)

typescript 复制代码
// StackLinePage.ets
Flex({
  direction: breakpointOrder[this.globalInfoModel.currentBreakpoint] >=
    breakpointOrder[BreakpointTypeEnum.LG]
    ? FlexDirection.Row       // 大屏:左右排列
    : FlexDirection.Column,   // 小屏:上下排列
  space: { main: LengthMetrics.px(30) },
  justifyContent: FlexAlign.Center,
  alignItems: breakpointOrder[this.globalInfoModel.currentBreakpoint] >=
    breakpointOrder[BreakpointTypeEnum.LG]
    ? ItemAlign.Start         // 大屏:左对齐
    : ItemAlign.Center        // 小屏:居中
}) {
  // 预览区域
  this.resultPreview()
  // 设置面板
  Column({ space: 12 }) {
    // ...
  }
}

效果:

  • 手机:预览区在上,设置面板在下
  • 平板横屏:预览区在左,设置面板在右

4.2 if-else 条件布局(WatermarkPage)

typescript 复制代码
// WatermarkPage.ets
Scroll() {
  if (breakpointOrder[this.globalInfoModel.currentBreakpoint] >=
      breakpointOrder[BreakpointTypeEnum.LG]) {
    // 大屏:Row 横排
    Row() {
      Column() { this.previewSection() }.layoutWeight(1)
      Column() { this.settingsSection() }.layoutWeight(1)
    }
  } else {
    // 小屏:Column 竖排
    Column() {
      this.previewSection()
      this.settingsSection()
    }
  }
}

4.3 面板可见性控制(StackLinePage)

有些面板在大屏时放在预览区旁边,小屏时放在预览区下方:

typescript 复制代码
// 调整间距面板 - 大屏时在右侧面板中显示
Column() {
  this.adjustLayout()
}
.visibility(
  breakpointOrder[this.globalInfoModel.currentBreakpoint] >=
    breakpointOrder[BreakpointTypeEnum.LG]
    ? Visibility.Visible    // 大屏:在右侧面板显示
    : Visibility.None       // 小屏:隐藏(因为下面还有一个)
)

// 预览区下方 - 小屏时显示
Column() {
  this.adjustLayout()
}
.visibility(
  breakpointOrder[this.globalInfoModel.currentBreakpoint] >=
    breakpointOrder[BreakpointTypeEnum.LG]
    ? Visibility.None       // 大屏:隐藏
    : Visibility.Visible    // 小屏:在预览下方显示
)

这避免了同一组件渲染两次的浪费,只控制可见性即可。

五、画布尺寸的响应式计算

图片预览区的宽度需要根据屏幕和断点动态计算:

5.1 手机端

typescript 复制代码
// StackLinePage.ets - aboutToAppear
this.canvasWidth = px2vp(display.getDefaultDisplaySync().width) - 32;
// 32 = 左右各16的padding

5.2 大屏端(ImageSplitPage)

typescript 复制代码
// ImageSplitPage.ets - aboutToAppear
this.canvasWidth = breakpointOrder[this.globalInfoModel.currentBreakpoint] >=
  breakpointOrder[BreakpointTypeEnum.LG]
  ? px2vp(display.getDefaultDisplaySync().width) / 2 - 32   // 大屏:半屏宽度
  : px2vp(display.getDefaultDisplaySync().width) - 32;       // 小屏:全屏宽度

5.3 动态监听尺寸变化

组件的 onSizeChange 回调可以在运行时响应容器尺寸变化:

typescript 复制代码
// StackLinePage.ets - 预览区Stack
Stack()
  .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
    let newWidth = Math.round(newValue.width as number);
    let oldWidth = Math.round(this.canvasWidth);
    if (newWidth !== oldWidth && newWidth > 0) {
      this.canvasWidth = newWidth;
      this.calculateCanvasSize();  // 重新计算画布高度
    }
  });

当折叠屏展开/合拢时,容器宽度变化,画布高度自动重算。

六、BreakpointType 工具类

项目还提供了一个通用的断点值映射工具:

typescript 复制代码
// BreakpointSystem.ets
export class BreakpointType<T> {
  private xs: T;
  private sm: T;
  private md: T;
  private lg: T;
  private xl: T;

  public constructor(param: BreakpointTypes<T>) {
    this.xs = param.xs ?? param.sm;  // xs 缺省取 sm
    this.xl = param.xl ?? param.lg;  // xl 缺省取 lg
  }

  public getValue(currentBreakpoint: string): T {
    if (currentBreakpoint === BreakpointTypeEnum.XS) return this.xs;
    if (currentBreakpoint === BreakpointTypeEnum.SM) return this.sm;
    if (currentBreakpoint === BreakpointTypeEnum.MD) return this.md;
    if (currentBreakpoint === BreakpointTypeEnum.XL) return this.xl;
    return this.lg;
  }
}

使用示例:

typescript 复制代码
// 不同断点使用不同的列数
const columns = new BreakpointType({
  sm: 1,
  md: 2,
  lg: 3
}).getValue(this.globalInfoModel.currentBreakpoint);

七、裁剪页面的断点适配

图片裁剪页根据断点调整裁剪框大小:

typescript 复制代码
// ImageCropPage.ets
.onReady((context: NavDestinationContext) => {
  let screenWidth = px2vp(display.getDefaultDisplaySync().width);
  let frameWidth = screenWidth;
  if (breakpointOrder[this.globalInfoModel.currentBreakpoint] >=
      breakpointOrder[BreakpointTypeEnum.LG]) {
    frameWidth = screenWidth * 0.5;  // 大屏:裁剪框为屏幕宽度的50%
  }
  this.model.setImage(params.uri)
    .setFrameWidth(frameWidth)
    .setFrameRatio(1);
})

八、总结

本篇我们学习了 LineCard 的响应式布局体系:

层级 技术方案 适用场景
栅格布局 GridRow + GridCol 列数自适应的列表
方向切换 Flex direction 条件切换 小屏竖排/大屏横排
条件渲染 if-else 不同布局树 差异较大的布局
可见性控制 Visibility.Visible/None 同一组件不同位置显示
尺寸计算 屏幕宽度 + 断点判断 画布/容器宽度自适应
动态监听 onSizeChange 折叠屏等运行时尺寸变化

核心思想是:断点系统提供全局状态,各页面根据断点值决定布局策略

下一篇,我们将深入 图片选择与组件截图保存 的完整流程。


系列导航:

相关推荐
Catrice02 小时前
HarmonyOS ArkTS 实战:实现一个电影追剧与观影记录应用(完整源码)
华为·harmonyos
●VON2 小时前
鸿蒙 PC Markdown 编辑器自由窗口:覆盖侧栏与响应式预算
安全·华为·编辑器·harmonyos·鸿蒙
SameX2 小时前
ArkTS 用 Preferences 存 App 配置的正确姿势 —— 从踩坑到 singleton 封装
harmonyos
懿路向前2 小时前
【HarmonyOS学习笔记】2026-07-24 | textProcessing 实体识别与踩坑实录
笔记·学习·边缘计算·harmonyos
贾伟康3 小时前
【笔下生辉|02】HarmonyOS ArkTS 素材库详情实战:组织例句、解释、收藏和练习入口
harmonyos·arkts·详情页·学习进度·收藏功能
独隅3 小时前
DevEco Code 在 Windows/MacOS 双系统上的完整使用指南
ide·人工智能·windows·macos·华为·harmonyos
●VON3 小时前
鸿蒙 PC Markdown 编辑器有界版本历史:沙箱快照、完整性校验与安全恢复
安全·华为·编辑器·harmonyos·鸿蒙
痕忆丶4 小时前
OpenHarmony北向开发基础之 沙箱机制+分布式文件
harmonyos
logomister设计公司阿燕5 小时前
华为商标“减法哲学”:极简主义如何成就全球品牌?
python·华为