HarmonyOS 实战教程(九):响应式布局与断点系统 —— 以「柚兔自测量表」为例

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

HarmonyOS 应用可运行在手机、平板、2in1 设备甚至折叠屏上,屏幕尺寸差异巨大。MeCharts 采用了断点系统(Breakpoint System)+ 条件渲染的方式,实现一套代码适配多种设备。

二、断点系统设计

2.1 断点定义

MeCharts 定义了五个断点等级:

typescript 复制代码
export enum BreakpointTypeEnum {
  XS = 'xs',   // 超小屏(折叠屏半屏)
  SM = 'sm',   // 小屏(手机竖屏)
  MD = 'md',   // 中屏(手机横屏/小平板)
  LG = 'lg',   // 大屏(平板)
  XL = 'xl',   // 超大屏(2in1/外接显示器)
}

2.2 BreakpointType 工具类

这是断点系统的核心工具,根据当前断点返回不同的值:

typescript 复制代码
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.sm = param.sm;
    this.md = param.md;
    this.lg = param.lg;
    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;   // 默认返回 lg
  }
}

使用方式非常简洁:

typescript 复制代码
new BreakpointType({
  sm: FlexDirection.Row,       // 小屏水平排列
  lg: FlexDirection.Column,    // 大屏垂直排列
}).getValue(this.globalInfoModel.currentBreakpoint)

2.3 全局断点状态

断点信息存储在 GlobalInfoModel 中,通过 AppStorage 全局共享:

typescript 复制代码
@Observed
export class GlobalInfoModel {
  public currentBreakpoint: BreakpointTypeEnum = BreakpointTypeEnum.MD;
  public naviIndicatorHeight: number = 0;
  public statusBarHeight: number = 0;
  public deviceHeight: number = 0;
  public deviceWidth: number = 0;
}

2.4 断点更新

WindowUtil.registerBreakPoint 中监听窗口尺寸变化:

typescript 复制代码
public static registerBreakPoint(windowStage: window.WindowStage) {
  windowStage.getMainWindow((err: BusinessError, data: window.Window) => {
    // 获取安全区域信息
    const systemAvoidArea: window.AvoidArea = data.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
    globalInfoModel.statusBarHeight = px2vp(systemAvoidArea.topRect.height);
    
    const bottomArea: window.AvoidArea = data.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
    globalInfoModel.naviIndicatorHeight = px2vp(bottomArea.bottomRect.height);
    
    // 监听窗口尺寸变化
    data.on('windowSizeChange', () => WindowUtil.onWindowSizeChange(data));
    
    // 监听安全区域变化
    data.on('avoidAreaChange', (avoidAreaOption) => {
      if (avoidAreaOption.type === window.AvoidAreaType.TYPE_SYSTEM ||
          avoidAreaOption.type === window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
        WindowUtil.setAvoidArea(avoidAreaOption.type, avoidAreaOption.area);
      }
    });
    
    AppStorage.setOrCreate('GlobalInfoModel', globalInfoModel);
  });
}

三、响应式 TabBar 实现

自定义 TabBar 是响应式布局最典型的应用场景:

3.1 布局方向响应

typescript 复制代码
build() {
  Flex({
    direction: new BreakpointType({
      sm: FlexDirection.ColumnReverse,  // 手机:底部水平 TabBar
      md: FlexDirection.ColumnReverse,
      lg: FlexDirection.Row,            // 平板:左侧垂直 TabBar
    }).getValue(this.globalInfoModel.currentBreakpoint),
    alignItems: ItemAlign.Center,
  }) {
    // Tab 项容器
    Flex({
      direction: new BreakpointType({
        sm: FlexDirection.Row,          // 手机:水平排列 Tab 项
        md: FlexDirection.Row,
        lg: FlexDirection.Column,       // 平板:垂直排列 Tab 项
      }).getValue(this.globalInfoModel.currentBreakpoint),
      alignItems: ItemAlign.Center,
      justifyContent: FlexAlign.SpaceAround,
    }) {
      ForEach(TABS_LIST, (item: TabBarData) => {
        this.TabItemBuilder(item)
      })
    }
    .margin({
      bottom: new BreakpointType({
        sm: this.globalInfoModel.naviIndicatorHeight,  // 手机:底部留出导航条空间
        md: this.globalInfoModel.naviIndicatorHeight,
        lg: 0,                                         // 平板:无需避让
      }).getValue(this.globalInfoModel.currentBreakpoint),
    })
  }
  .size(new BreakpointType<SizeOptions>({
    sm: { width: '100%', height: CommonConstants.TAB_BAR_HEIGHT + this.globalInfoModel.naviIndicatorHeight },
    md: { width: '100%', height: CommonConstants.TAB_BAR_HEIGHT + this.globalInfoModel.naviIndicatorHeight },
    lg: { width: CommonConstants.TAB_BAR_WIDTH, height: '100%' },  // 平板:窄高条
  }).getValue(this.globalInfoModel.currentBreakpoint))
}

3.2 响应式效果

设备 TabBar 位置 排列方向 尺寸
手机 底部 水平 宽100% + 高48vp + 底部避让
平板 左侧 垂直 宽96vp + 高100%

四、安全区域避让

全屏模式下,内容可能被状态栏和导航条遮挡。MeCharts 的避让策略是"手动计算 padding"。

4.1 TopBar 顶部避让

typescript 复制代码
.padding({
  top: this.statusBarHeight,   // 状态栏高度
  bottom: 20,
  left: 12,
  right: 12
})

4.2 页面底部避让

子页面通过 padding 留出导航条空间:

typescript 复制代码
NavDestination() {
  // 页面内容
}
.padding({ bottom: this.globalInfoModel.naviIndicatorHeight })

4.3 首页内容区底部避让

首页内容区需要在 TabBar 高度基础上再加导航条高度:

typescript 复制代码
Scroll() {
  // 内容
}
.height('100%')
.margin({ bottom: CommonConstants.TAB_BAR_HEIGHT + this.globalInfoModel.naviIndicatorHeight })

五、px2vp 单位转换

HarmonyOS 中存在两种长度单位:

  • px:物理像素,与设备屏幕密度相关
  • vp:虚拟像素,与密度无关,1vp ≈ 1/160 英寸

窗口 API 返回的尺寸是 px,需要转换为 vp:

typescript 复制代码
globalInfoModel.statusBarHeight = px2vp(systemAvoidArea.topRect.height);
globalInfoModel.naviIndicatorHeight = px2vp(bottomArea.bottomRect.height);
globalInfoModel.deviceHeight = px2vp(properties.windowRect.height);
globalInfoModel.deviceWidth = px2vp(properties.windowRect.width);

px2vp 是 ArkUI 内置的转换函数,无需手动计算。

六、其他响应式实践

6.1 条件渲染

对于断点差异较大的场景,可以使用条件渲染替代属性切换:

typescript 复制代码
if (this.globalInfoModel.currentBreakpoint === BreakpointTypeEnum.LG) {
  // 大屏布局
} else {
  // 小屏布局
}

6.2 资源限定词

HarmonyOS 支持通过资源限定词为不同设备提供不同的资源:

复制代码
resources/
  base/           # 默认资源
  tablet/         # 平板专用资源
  2in1/           # 2in1 设备专用资源

在代码中统一使用 $r('app.media.xxx') 引用,系统自动根据设备类型选择最匹配的资源。

6.3 常量定义

MeCharts 将布局相关的常量集中定义在 CommonConstants 中:

typescript 复制代码
export class CommonConstants {
  public static TAB_BAR_HEIGHT: number = 48;
  public static TAB_BAR_WIDTH: number = 96;
  public static SIDE_BAR_WIDTH: number = 240;
  public static NAVIGATION_HEIGHT: number = 56;
  public static BANNER_ASPECT_SM: number = 1.28;
  public static BANNER_ASPECT_MD: number = 0.53;
  public static BANNER_ASPECT_LG: number = 0.53;
}

不同断点使用不同的横纵比、高度等常量值,确保在各种设备上都有良好的视觉效果。

七、折叠屏适配

折叠屏设备需要在展开/折叠状态间平滑切换。MeCharts 通过监听 windowSizeChange 事件实现:

typescript 复制代码
data.on('windowSizeChange', () => {
  WindowUtil.onWindowSizeChange(data);
  // 此处可加入断点重新计算逻辑
});

当折叠屏状态变化时,窗口尺寸改变,触发重新计算断点和布局参数。@StorageProp('GlobalInfoModel') 的响应式机制确保 UI 自动刷新。

八、小结

本篇讲解了 MeCharts 的响应式布局体系,包括断点系统、BreakpointType 工具类、安全区域避让和单位转换。通过 BreakpointType<T> 泛型工具,一套代码即可适配手机和平板两种形态。下一篇将总结项目全貌并给出扩展建议。

相关推荐
山璞1 小时前
将一个 Flutter 项目转为用 ArkUI-X 框架实现(4)
flutter·harmonyos
程序员黑豆2 小时前
鸿蒙开发实战:使用 List 组件构建新闻列表
前端·华为·harmonyos
爱写代码的阿森3 小时前
鸿蒙三方库 | harmony-utils之RegexUtil正则匹配验证详解
服务器·华为·harmonyos·鸿蒙·huawei
qizayaoshuap3 小时前
# 卡片轮播组件开发实战:HarmonyOS ArkTS 优雅轮播效果实现解析
华为·harmonyos
echohelloworld113 小时前
HarmonyOS应用《玄象》开发实战:项目目录约定:common/components/constants/utils/pages 六层架构
harmonyos·鸿蒙
程序员黑豆3 小时前
鸿蒙应用开发:Scroll 组件从入门到实战
前端·华为·harmonyos
熊猫钓鱼>_>4 小时前
2026 鸿蒙全栈开发实战:从新能力落地到多设备上架的完整路径
华为·架构·app·harmonyos·arkts·鸿蒙·运营
qizayaoshuap5 小时前
# 备忘录应用开发实战:HarmonyOS ArkTS 快速记事本应用解析
华为·harmonyos
FF2501_940228585 小时前
HarmonyOS应用《玄象》开发实战:罗盘手势缩放:PinchGesture + scale 属性的协同
harmonyos·鸿蒙