让同一套代码在手机竖屏、平板横屏、折叠屏上都呈现最佳布局。
一、为什么需要响应式布局?
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);
}
}
}
设计要点:
- 单例模式 ------ 全局只有一个断点计算器实例。
- 窗口宽度计算 ------ 使用
px2vp将像素转换为 vp,确保不同分辨率设备上断点一致。 - 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 | 折叠屏等运行时尺寸变化 |
核心思想是:断点系统提供全局状态,各页面根据断点值决定布局策略。
下一篇,我们将深入 图片选择与组件截图保存 的完整流程。
系列导航: