一、为什么需要响应式布局
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> 泛型工具,一套代码即可适配手机和平板两种形态。下一篇将总结项目全貌并给出扩展建议。