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. 创建项目
bash
# 使用 DevEco Studio 创建一个空白 ArkTS 项目
# 选择 Stage 模型
实战一:配置横竖屏支持
步骤 1:配置 module.json5
在 entry/src/main/module.json5 中配置页面支持的屏幕方向:
json
{
"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:在代码中动态设置方向
typescript
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:基础响应式网格
typescript
@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:功能按钮网格(实际应用场景)
typescript
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 监听
typescript
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(推荐)
typescript
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)
}
}
实战四:自适应卡片布局
完整示例:数据展示卡片
typescript
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. 使用相对单位
typescript
// ✅ 推荐:使用 vp(虚拟像素)
.width('100%')
.height(200) // 默认单位是 vp
.fontSize(16)
// ❌ 避免:使用固定像素
.width(375) // 不同设备宽度不同
2. 合理设置断点
typescript
GridRow({
columns: {
sm: 4, // 手机竖屏
md: 8, // 手机横屏/平板竖屏
lg: 12 // 平板横屏/PC
},
breakpoints: {
value: ['320vp', '600vp', '840vp'],
reference: BreakpointsReference.WindowSize
}
})
3. 提供横竖屏不同的布局
typescript
@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
typescript
GridRow({
columns: { sm: 4, md: 8, lg: 12 }
})
.onBreakpointChange((breakpoint: string) => {
console.info(`当前断点: ${breakpoint}`);
// 根据断点调整 UI
})
常见问题
Q1: 为什么设置了 auto_rotation 但屏幕不旋转?
A: 检查以下几点:
- 确保设备的自动旋转功能已开启
- 检查
module.json5中的orientation配置 - 某些模拟器可能不支持旋转,使用真机测试
Q2: GridRow 的 span 如何计算?
A: span 表示占据的列数:
typescript
// 如果 columns 设置为 12
GridCol({ span: 3 }) // 占 3/12 = 25% 宽度
GridCol({ span: 6 }) // 占 6/12 = 50% 宽度
GridCol({ span: 12 }) // 占 12/12 = 100% 宽度
Q3: 如何调试响应式布局?
A: 使用 DevEco Studio 的预览功能:
- 点击预览器右上角的设备切换按钮
- 选择不同的设备尺寸和方向
- 观察布局变化
Q4: mediaquery 和 GridRow 哪个更好?
A:
- GridRow: 适合网格布局,自动响应式,推荐用于卡片、按钮等
- mediaquery: 适合需要完全不同布局的场景,更灵活但需要手动管理
总结
本文通过实际代码示例,介绍了 HarmonyOS 中横竖屏切换和响应式布局的实现方法:
- 配置支持 : 在
module.json5中设置orientation - 响应式网格 : 使用
GridRow和GridCol实现自适应布局 - 监听变化 : 使用
mediaquery或onBreakpointChange监听屏幕变化 - 最佳实践: 使用相对单位、合理设置断点、提供不同布局
掌握这些技术,你就能开发出适配各种设备和屏幕方向的 HarmonyOS 应用!
参考资料
班级