HarmonyOS 鸿蒙Next横竖屏切换与响应式布局如何做?
HarmonyOS 鸿蒙Next横竖屏切换与响应式布局如何做?
- 如何配置应用支持横竖屏切换?
- 使用 GridRow/GridCol 实现响应式布局
- 监听和响应屏幕方向变化
- 构建自适应的卡片网格系统
HarmonyOS 横竖屏切换与响应式布局实战指南
目录
前言
在移动应用开发中,横竖屏切换和响应式布局是提升用户体验的重要特性。本文将通过实际代码示例,带你掌握 HarmonyOS 中横竖屏适配和响应式布局的核心技术。
你将学到:
- 如何配置应用支持横竖屏切换
- 使用 GridRow/GridCol 实现响应式布局
- 监听和响应屏幕方向变化
- 构建自适应的卡片网格系统
基础概念
1. 屏幕方向类型
HarmonyOS 支持以下屏幕方向:
PORTRAIT- 竖屏LANDSCAPE- 横屏AUTO_ROTATION- 自动旋转UNSPECIFIED- 未指定
2. 响应式断点
HarmonyOS 提供了基于屏幕宽度的断点系统:
xs(extra small): < 320vpsm(small): 320vp ~ 600vpmd(medium): 600vp ~ 840vplg(large): 840vp ~ 1024vpxl(extra large): ≥ 1024vp
环境准备
1. 开发环境
- DevEco Studio 5.0+
- HarmonyOS SDK API 17+
2. 创建项目
# 使用 DevEco Studio 创建一个空白 ArkTS 项目
# 选择 Stage 模型
实战一:配置横竖屏支持
步骤 1:配置 module.json5
在 entry/src/main/module.json5 中配置页面支持的屏幕方向:
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
],
"orientation": "auto_rotation"
}
]
}
}
orientation 可选值:
"unspecified"- 默认,跟随系统"landscape"- 仅横屏"portrait"- 仅竖屏"auto_rotation"- 自动旋转(推荐)
步骤 2:在代码中动态设置方向
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct OrientationDemo {
aboutToAppear(): void {
this.setOrientation();
}
async setOrientation(): Promise<void> {
try {
// 获取当前窗口
const windowClass = await window.getLastWindow(getContext(this));
// 设置屏幕方向
// window.Orientation.AUTO_ROTATION - 自动旋转
// window.Orientation.PORTRAIT - 竖屏
// window.Orientation.LANDSCAPE - 横屏
await windowClass.setPreferredOrientation(window.Orientation.AUTO_ROTATION);
console.info('Screen orientation set successfully');
} catch (err) {
const error = err as BusinessError;
console.error('Failed to set orientation:', error);
}
}
build() {
Column() {
Text('横竖屏切换示例')
.fontSize(24)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
实战二:响应式网格布局
核心组件:GridRow 和 GridCol
GridRow 和 GridCol 是 HarmonyOS 提供的响应式网格布局组件。
示例 1:基础响应式网格
@Entry
@Component
struct ResponsiveGridDemo {
build() {
Scroll() {
Column() {
Text('响应式网格布局')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
// GridRow 定义网格系统
GridRow({
columns: {
sm: 4, // 小屏:4列
md: 8, // 中屏:8列
lg: 12 // 大屏:12列
},
gutter: { x: 12, y: 12 }, // 列间距和行间距
breakpoints: {
value: ['320vp', '600vp', '840vp'],
reference: BreakpointsReference.WindowSize
}
}) {
// 每个 GridCol 占据的列数
ForEach([1, 2, 3, 4, 5, 6, 7, 8], (item: number) => {
GridCol({
span: {
sm: 2, // 小屏占2列(4列中的2列 = 50%)
md: 4, // 中屏占4列(8列中的4列 = 50%)
lg: 3 // 大屏占3列(12列中的3列 = 25%)
}
}) {
Column() {
Text(`卡片 ${item}`)
.fontSize(16)
.fontColor(Color.White)
}
.width('100%')
.height(100)
.backgroundColor('#007DFF')
.borderRadius(8)
.justifyContent(FlexAlign.Center)
}
})
}
.width('100%')
}
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
}
}
示例 2:功能按钮网格(实际应用场景)
interface FunctionItem {
icon: string;
name: string;
route?: string;
}
@Entry
@Component
struct FunctionGridDemo {
private functions: FunctionItem[] = [
{ icon: '🏠', name: '首页' },
{ icon: '📊', name: '数据分析' },
{ icon: '⚙️', name: '设置' },
{ icon: '👤', name: '个人中心' },
{ icon: '📝', name: '记录' },
{ icon: '📈', name: '统计' },
{ icon: '🔔', name: '通知' },
{ icon: '💬', name: '消息' }
];
build() {
Column() {
Text('功能菜单')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.width('100%')
.margin({ bottom: 16 })
GridRow({
columns: {
sm: 4, // 竖屏:4列
md: 4, // 中屏竖屏:4列
lg: 6, // 横屏:6列
xl: 8 // 超大屏:8列
},
gutter: { x: 16, y: 16 }
}) {
ForEach(this.functions, (item: FunctionItem) => {
GridCol({ span: 1 }) {
this.buildFunctionButton(item)
}
.height(80)
})
}
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
}
@Builder
buildFunctionButton(item: FunctionItem) {
Column({ space: 8 }) {
Text(item.icon)
.fontSize(32)
Text(item.name)
.fontSize(12)
.fontColor('#333333')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#F5F5F5')
.borderRadius(12)
.onClick(() => {
console.info(`Clicked: ${item.name}`);
})
}
}
实战三:监听屏幕方向变化
方法 1:使用 mediaquery 监听
import { mediaquery } from '@kit.ArkUI';
@Entry
@Component
struct OrientationListener {
@State isLandscape: boolean = false;
private listener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(orientation: landscape)');
aboutToAppear(): void {
// 注册监听器
this.listener.on('change', (result: mediaquery.MediaQueryResult) => {
this.isLandscape = result.matches;
console.info(`Screen orientation changed: ${this.isLandscape ? 'Landscape' : 'Portrait'}`);
});
}
aboutToDisappear(): void {
// 注销监听器
this.listener.off('change');
}
build() {
Column() {
Text(this.isLandscape ? '当前:横屏模式' : '当前:竖屏模式')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
Text('屏幕方向会自动检测')
.fontSize(16)
.fontColor('#666666')
// 根据屏幕方向显示不同布局
if (this.isLandscape) {
this.buildLandscapeLayout()
} else {
this.buildPortraitLayout()
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(20)
}
@Builder
buildLandscapeLayout() {
Row({ space: 20 }) {
Column() {
Text('左侧内容')
.fontSize(18)
}
.width('50%')
.height(200)
.backgroundColor('#E3F2FD')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
Column() {
Text('右侧内容')
.fontSize(18)
}
.width('50%')
.height(200)
.backgroundColor('#FFF3E0')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
}
.width('100%')
.margin({ top: 30 })
}
@Builder
buildPortraitLayout() {
Column({ space: 20 }) {
Column() {
Text('上方内容')
.fontSize(18)
}
.width('100%')
.height(150)
.backgroundColor('#E3F2FD')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
Column() {
Text('下方内容')
.fontSize(18)
}
.width('100%')
.height(150)
.backgroundColor('#FFF3E0')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
}
.width('100%')
.margin({ top: 30 })
}
}
方法 2:使用 BreakpointSystem(推荐)
import { BreakpointSystem, BreakpointConstants } from '@ohos.arkui.observer';
@Entry
@Component
struct BreakpointDemo {
@State currentBreakpoint: string = 'sm';
private breakpointSystem: BreakpointSystem = new BreakpointSystem();
aboutToAppear(): void {
this.breakpointSystem.register();
}
aboutToDisappear(): void {
this.breakpointSystem.unregister();
}
build() {
Column() {
Text(`当前断点: ${this.currentBreakpoint}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
GridRow({
columns: {
sm: 4,
md: 8,
lg: 12
},
gutter: 16
}) {
ForEach([1, 2, 3, 4, 5, 6], (item: number) => {
GridCol({
span: {
sm: 4, // 小屏:每行1个
md: 4, // 中屏:每行2个
lg: 3 // 大屏:每行4个
}
}) {
Column() {
Text(`项目 ${item}`)
.fontSize(16)
}
.width('100%')
.height(100)
.backgroundColor('#4CAF50')
.borderRadius(8)
.justifyContent(FlexAlign.Center)
}
})
}
.width('100%')
.onBreakpointChange((breakpoint: string) => {
this.currentBreakpoint = breakpoint;
console.info(`Breakpoint changed to: ${breakpoint}`);
})
}
.width('100%')
.height('100%')
.padding(16)
}
}
实战四:自适应卡片布局
完整示例:数据展示卡片
interface DataCard {
title: string;
value: string;
unit: string;
icon: string;
color: string;
}
@Entry
@Component
struct AdaptiveCardLayout {
@State cards: DataCard[] = [
{ title: '总数量', value: '1,234', unit: '个', icon: '📊', color: '#2196F3' },
{ title: '本月新增', value: '156', unit: '个', icon: '📈', color: '#4CAF50' },
{ title: '完成率', value: '85', unit: '%', icon: '✅', color: '#FF9800' },
{ title: '待处理', value: '23', unit: '项', icon: '⏰', color: '#F44336' }
];
build() {
Scroll() {
Column() {
// 标题
Text('数据概览')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.width('100%')
.margin({ bottom: 20 })
// 响应式卡片网格
GridRow({
columns: {
sm: 4, // 小屏:2列
md: 8, // 中屏:4列
lg: 12 // 大屏:4列
},
gutter: { x: 16, y: 16 }
}) {
ForEach(this.cards, (card: DataCard) => {
GridCol({
span: {
sm: 2, // 小屏:占2列(50%宽度)
md: 2, // 中屏:占2列(25%宽度)
lg: 3 // 大屏:占3列(25%宽度)
}
}) {
this.buildDataCard(card)
}
})
}
.width('100%')
}
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
buildDataCard(card: DataCard) {
Column({ space: 12 }) {
// 图标
Text(card.icon)
.fontSize(36)
// 数值
Row({ space: 4 }) {
Text(card.value)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor(card.color)
Text(card.unit)
.fontSize(14)
.fontColor('#999999')
.alignSelf(ItemAlign.End)
.margin({ bottom: 4 })
}
// 标题
Text(card.title)
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.height(150)
.backgroundColor(Color.White)
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.shadow({
radius: 8,
color: '#00000010',
offsetY: 2
})
}
}
最佳实践
1. 使用相对单位
// ✅ 推荐:使用 vp(虚拟像素)
.width('100%')
.height(200) // 默认单位是 vp
.fontSize(16)
// ❌ 避免:使用固定像素
.width(375) // 不同设备宽度不同
2. 合理设置断点
GridRow({
columns: {
sm: 4, // 手机竖屏
md: 8, // 手机横屏/平板竖屏
lg: 12 // 平板横屏/PC
},
breakpoints: {
value: ['320vp', '600vp', '840vp'],
reference: BreakpointsReference.WindowSize
}
})
3. 提供横竖屏不同的布局
@State isLandscape: boolean = false;
build() {
if (this.isLandscape) {
// 横屏布局:左右分栏
Row() {
Column().width('50%')
Column().width('50%')
}
} else {
// 竖屏布局:上下堆叠
Column() {
Column().width('100%')
Column().width('100%')
}
}
}
4. 使用 GridRow 的 onBreakpointChange
GridRow更多关于HarmonyOS 鸿蒙Next横竖屏切换与响应式布局如何做?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
鸿蒙Next中横竖屏切换通过UIAbility的onWindowStageChange监听窗口变化,配合响应式布局实现。使用ArkUI的媒体查询(@ohos.mediaquery)检测屏幕方向变化,结合栅格系统(GridRow/GridCol)或百分比布局自适应调整。通过限制子组件尺寸、使用Flex弹性布局和RelativeContainer相对布局适配不同屏幕。
在HarmonyOS Next中,实现横竖屏切换与响应式布局主要依赖ArkUI的声明式UI框架和响应式设计能力。
1. 配置应用支持横竖屏切换
在module.json5配置文件的abilities字段下,为对应Ability设置orientation属性。
"orientation": "unspecified"(默认): 应用跟随系统设置。"orientation": "landscape": 锁定为横屏。"orientation": "portrait": 锁定为竖屏。 如需支持动态切换,通常设置为"unspecified"。
2. 使用GridRow/GridCol实现响应式布局
GridRow是核心的栅格容器组件,GridCol为其子组件,用于定义在不同断点下的占位列数。
- 定义断点: 系统预设了xs、sm、md、lg四种断点(对应不同设备宽度范围),开发者也可通过
GridRow的breakpoints属性自定义。 - 设置列数: 通过
GridCol的span属性(如span={6})或响应式对象(如span={{ xs: 12, sm: 6, md: 4 }})来定义在不同断点下占据的列数(总列数默认为12)。 - 示例:
GridRow() { GridCol({ span: { xs: 12, sm: 6, md: 4 } }) { // 内容组件1 } GridCol({ span: { xs: 12, sm: 6, md: 4 } }) { // 内容组件2 } // ... 更多GridCol } .breakpoints({ value: ['320vp', '520vp', '840vp'] }) // 可选:自定义断点
3. 监听和响应屏幕方向变化
可以通过window模块的getLastWindow方法获取窗口对象,并监听其orientationChange事件。
- 获取当前方向:
window.getLastWindow(this.context).orientation - 监听方向变化:
通常结合import { window } from '@kit.ArkUI'; // 在aboutToAppear或合适时机注册监听 window.getLastWindow(this.context).on('orientationChange', (newOrientation) => { // newOrientation: 0-竖屏,1-横屏 // 在此处更新UI状态或重新计算布局 this.currentOrientation = newOrientation; });@State或@Prop装饰的变量,在回调中更新状态以触发UI重新渲染。
4. 构建自适应的卡片网格系统
结合上述GridRow/GridCol与方向监听,可以构建动态卡片网格。
- 基础网格: 使用
GridRow和GridCol定义卡片在不同断点下的基础布局。 - 方向适配: 在
orientationChange回调中,可以根据newOrientation动态调整GridCol的span值或其他布局参数(例如通过状态变量控制),实现横竖屏下不同的列数或排列策略。 - 组件化: 将单个卡片封装为自定义组件,在
GridCol内复用,保持布局与内容的分离。 - 示例逻辑:
@State currentSpan: number = 6; // 默认占6列 // 在方向变化回调中 onOrientationChange(newOrientation) { // 横屏时卡片占4列,竖屏时占6列(假设总列数12) this.currentSpan = (newOrientation === 1) ? 4 : 6; } // 在UI中使用 GridRow() { ForEach(this.cardList, (item) => { GridCol({ span: this.currentSpan }) { CardComponent({ data: item }) // 自定义卡片组件 } }) }
总结流程:
- 在配置文件中声明支持方向切换。
- 使用
GridRow/GridCol构建基于断点的响应式栅格布局骨架。 - 监听
orientationChange事件,获取屏幕方向变化。 - 根据方向变化,通过状态管理动态调整
GridCol的布局参数(如span),或切换不同的UI结构,实现自适应的卡片网格布局。
这种方式充分利用了ArkUI的响应式设计特性,使布局能同时适配不同屏幕尺寸和方向变化。

