HarmonyOS 鸿蒙Next横竖屏切换与响应式布局如何做?

HarmonyOS 鸿蒙Next横竖屏切换与响应式布局如何做?

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

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. 创建项目

# 使用 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

GridRowGridCol 是 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四种断点(对应不同设备宽度范围),开发者也可通过GridRowbreakpoints属性自定义。
  • 设置列数: 通过GridColspan属性(如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与方向监听,可以构建动态卡片网格。

  • 基础网格: 使用GridRowGridCol定义卡片在不同断点下的基础布局。
  • 方向适配: 在orientationChange回调中,可以根据newOrientation动态调整GridColspan值或其他布局参数(例如通过状态变量控制),实现横竖屏下不同的列数或排列策略。
  • 组件化: 将单个卡片封装为自定义组件,在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 }) // 自定义卡片组件
        }
      })
    }
    

总结流程

  1. 在配置文件中声明支持方向切换。
  2. 使用GridRow/GridCol构建基于断点的响应式栅格布局骨架。
  3. 监听orientationChange事件,获取屏幕方向变化。
  4. 根据方向变化,通过状态管理动态调整GridCol的布局参数(如span),或切换不同的UI结构,实现自适应的卡片网格布局。

这种方式充分利用了ArkUI的响应式设计特性,使布局能同时适配不同屏幕尺寸和方向变化。

回到顶部