HarmonyOS 横竖屏切换与响应式布局实战指南

HarmonyOS 横竖屏切换与响应式布局实战指南

目录


前言

在移动应用开发中,横竖屏切换和响应式布局是提升用户体验的重要特性。本文将通过实际代码示例,带你掌握 HarmonyOS 中横竖屏适配和响应式布局的核心技术。

你将学到:

  • 如何配置应用支持横竖屏切换
  • 使用 GridRow/GridCol 实现响应式布局
  • 监听和响应屏幕方向变化
  • 构建自适应的卡片网格系统

基础概念

1. 屏幕方向类型

HarmonyOS 支持以下屏幕方向:

  • PORTRAIT - 竖屏
  • LANDSCAPE - 横屏
  • AUTO_ROTATION - 自动旋转
  • UNSPECIFIED - 未指定

2. 响应式断点

HarmonyOS 提供了基于屏幕宽度的断点系统:

  • xs (extra small): < 320vp
  • sm (small): 320vp ~ 600vp
  • md (medium): 600vp ~ 840vp
  • lg (large): 840vp ~ 1024vp
  • xl (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

GridRowGridCol 是 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: 检查以下几点:

  1. 确保设备的自动旋转功能已开启
  2. 检查 module.json5 中的 orientation 配置
  3. 某些模拟器可能不支持旋转,使用真机测试

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 的预览功能:

  1. 点击预览器右上角的设备切换按钮
  2. 选择不同的设备尺寸和方向
  3. 观察布局变化

Q4: mediaquery 和 GridRow 哪个更好?

A:

  • GridRow: 适合网格布局,自动响应式,推荐用于卡片、按钮等
  • mediaquery: 适合需要完全不同布局的场景,更灵活但需要手动管理

总结

本文通过实际代码示例,介绍了 HarmonyOS 中横竖屏切换和响应式布局的实现方法:

  1. 配置支持 : 在 module.json5 中设置 orientation
  2. 响应式网格 : 使用 GridRowGridCol 实现自适应布局
  3. 监听变化 : 使用 mediaqueryonBreakpointChange 监听屏幕变化
  4. 最佳实践: 使用相对单位、合理设置断点、提供不同布局

掌握这些技术,你就能开发出适配各种设备和屏幕方向的 HarmonyOS 应用!


参考资料


班级

https://developer.huawei.com/consumer/cn/training/classDetail/fd34ff9286174e848d34cde7f512ce22?type=1%3Fha_source%3Dhmosclass\&ha_sourceId=89000248

相关推荐
钅日 勿 XiName1 小时前
一小时速通pytorch之训练分类器(四)(完结)
人工智能·pytorch·python
青瓷程序设计1 小时前
水果识别系统【最新版】Python+TensorFlow+Vue3+Django+人工智能+深度学习+卷积神经网络算法
人工智能·python·深度学习
AI模块工坊2 小时前
CVPR 即插即用 | 当RetNet遇见ViT:一场来自曼哈顿的注意力革命,中科院刷新SOTA性能榜!
人工智能·深度学习·计算机视觉·transformer
爱笑的眼睛112 小时前
深入解析HarmonyOS应用包管理Bundle:从原理到实践
华为·harmonyos
*才华有限公司*2 小时前
基于BERT的文本分类模型训练全流程:从环境搭建到显存优化实战
python
Lxinccode3 小时前
python(59) : 多线程调用大模型ocr提取图片文本
开发语言·python·图片提取文字·批量提取文件·多线程ocr
梁辰兴3 小时前
PyCharm使用了Conda的虚拟环境创建的的Python项目,下载库(包)到该项目的虚拟环境中
python·pycharm·conda·错误·异常·异常报错
自由日记3 小时前
python简单线性回归
开发语言·python·线性回归
强化学习与机器人控制仿真3 小时前
Meta 最新开源 SAM 3 图像视频可提示分割模型
人工智能·深度学习·神经网络·opencv·目标检测·计算机视觉·目标跟踪