文档管理中心
最佳实践多设备开发一次开发,多端部署多设备界面开发多设备窗口形态窗口方向

窗口方向

概述

窗口方向适配旨在解决应用不同场景下窗口的朝向问题。以直板机上的视频类应用为例,应用首页通常竖屏显示;而全屏视频播放页通常横屏显示。其核心的策略在于动态调整应用窗口方向的显示策略(即window的Orientation,以下简称“窗口旋转策略”),确保在不同用户交互场景下提升用户体验。

本文主要内容如下:

  • 前置约束与限制:介绍窗口方向的含义。明确指出在设备形态多样化的前提下,如何选择更合适的窗口旋转策略。
  • 窗口旋转策略枚举:介绍窗口旋转策略的枚举值,并解析各值在不同设备形态下的行为映射,帮助开发者理解系统的底层适配逻辑。
  • 实现原理:介绍配置页面窗口旋转策略的技术实现机制与核心流程。
  • 典型场景:
    • 应用首页案例:通用页面窗口旋转策略。
    • 游戏应用案例:竖屏或横屏方向锁定的窗口旋转策略。
    • 图库案例:四个方向自动旋转且受控制中心的旋转开关控制的窗口旋转策略。
    • 个股详情页 & 股票K线图页:应用组合页面内根据场景不同切换的窗口旋转策略。
    • 视频详情页 & 全屏播放页:相同页面内根据用户行为切换的窗口旋转策略。

前置约束与限制

在阅读本文前,建议开发者先了解窗口管理窗口旋转屏幕管理一次开发,多端部署组件导航(Navigation)等相关知识。

横竖屏切换功能可实现应用内既支持竖屏显示也支持横屏显示的效果。对于应用内不同页面显示方向不同的情况,需在应用逻辑中动态修改窗口方向以实现该效果。例如,在直板机上具备视频播放功能的应用中,首页内容是采用竖屏方式,而全屏播放页则采用横屏方式展示。

随着设备形态日益丰富,应用页面支持旋转已从部分页面适配发展为全面支持。因此,选择合适的旋转策略,对应用开发至关重要。

目前HarmonyOS系统中设备的显示方向有以下四种,对应真机实际状态如下:

基本定义:

以设备物理屏幕尺寸为判定依据,设备的显示方向定义如下:

  • 竖屏(PORTRAIT):屏幕高度大于宽度,用户正向握持设备时充电口朝下(默认竖屏状态)。
  • 反向竖屏(PORTRAIT_INVERTED):屏幕高度大于宽度,但设备倒置,即充电口朝上。
  • 横屏(LANDSCAPE):屏幕宽度大于高度,用户正向握持设备时充电口朝右(默认横屏状态)。
  • 反向横屏(LANDSCAPE_INVERTED):屏幕宽度大于高度,但设备倒置,即充电口朝左。

区分方法

系统提供了@ohos.display模块来获取屏幕的当前方向(Orientation)和旋转角度(即Display的rotation)。屏幕方向直接对应上述四种方向枚举值,而旋转角度表示屏幕相对于默认方向的顺时针旋转度数,其对应关系如下表(以常见直板机为例):

展开

屏幕旋转角度返回值 (rotation)

对应度数

屏幕方向 (Orientation)

0

竖屏 (PORTRAIT)

1

90°

反向横屏 (LANDSCAPE_INVERTED)

2

180°

反向竖屏 (PORTRAIT_INVERTED)

3

270°

横屏 (LANDSCAPE)

了解窗口旋转策略

窗口旋转策略提供了18种窗口旋转策略(即window的Orientation),开发者可通过预设相关窗口旋转策略控制应用在不同场景下的窗口显示方向。为帮助开发者能更快速的理解这些策略,下文会分类说明18个枚举值的含义及对应效果。

固定方向策略

固定方向旋转策略是指应用窗口在启动或页面跳转时被锁定在特定显示方向(如竖屏、横屏等),且不随设备物理方向改变而自动旋转,包含以下五类:

展开

名称

说明

PORTRAIT

1

表示竖屏显示模式。

LANDSCAPE

2

表示横屏显示模式。

PORTRAIT_INVERTED

3

表示反向竖屏显示模式。

LANDSCAPE_INVERTED

4

表示反向横屏显示模式。

LOCKED

11

表示锁定模式,窗口显示方向与屏幕当前方向(参考Orientation)一致。

以三折叠G态为例,窗口初始方向的效果图如下:

展开

初始方向

枚举值

设备竖屏时,应用启动效果图

设备横屏时,应用启动效果图

竖屏

PORTRAIT

反向竖屏

PORTRAIT_INVERTED

横屏

LANDSCAPE

反向横屏

LANDSCAPE_INVERTED

锁定模式

LOCKED

自动旋转策略

自动旋转策略是指应用窗口能够根据设备物理方向(即重力传感器)的变化自动调整显示方向,且可能受系统控制中心“旋转锁定”开关的影响。

说明

控制中心的旋转开关用于控制屏幕是否可以旋转。当“旋转锁定”高亮时,表示已锁定,无法旋转;当“旋转锁定”为灰色时,表示已解锁,可以旋转。

例如,若要实现跟随控制中心的自动旋转,包括横屏、竖屏、反向横屏、反向竖屏,则可设置为AUTO_ROTATION_RESTRICTED。

若不希望跟随控制中心的旋转控制,只需设置为AUTO_ROTATION,此时应用的旋转不受控制中心锁定的影响。其他旋转方式亦然。

不受控制中心控制的自动旋转

不受控制中心控制的自动旋转策略包含以下三类:

展开

名称

说明

AUTO_ROTATION

5

跟随传感器自动旋转,可以旋转到竖屏、横屏、反向竖屏、反向横屏四个方向,且不受控制中心的旋转开关控制。

AUTO_ROTATION_PORTRAIT

6

跟随传感器自动竖向旋转,可以旋转到竖屏、反向竖屏,无法旋转到横屏、反向横屏,且不受控制中心的旋转开关控制。

AUTO_ROTATION_LANDSCAPE

7

跟随传感器自动横向旋转,可以旋转到横屏、反向横屏,无法旋转到竖屏、反向竖屏,且不受控制中心的旋转开关控制。

以三折叠G态为例,不受控制中心控制的自动旋转策略效果图如下:

展开
  

不受开关控制枚举值

不受开关控制效果图

自由旋转(竖屏/反向竖屏/横屏/反向横屏)

AUTO_ROTATION

竖屏旋转(竖屏/反向竖屏)

AUTO_ROTATION_PORTRAIT

横屏旋转(横屏/反向横屏)

AUTO_ROTATION_LANDSCAPE

说明

控制中心的旋转开关用于控制屏幕是否可以旋转。当“旋转锁定”高亮时,表示已锁定,无法旋转;当“旋转锁定”为灰色时,表示已解锁,可以旋转。

例如,若要实现跟随控制中心的自动旋转,包括横屏、竖屏、反向横屏、反向竖屏,则可设置为AUTO_ROTATION_RESTRICTED。

若不希望跟随控制中心的旋转控制,只需设置为AUTO_ROTATION,此时应用的旋转不受控制中心锁定的影响。其他旋转方式亦然。

受控制中心控制的自动旋转

受控制中心控制的自动旋转策略包含以下四类:

展开

名称

说明

AUTO_ROTATION_RESTRICTED

8

跟随传感器自动旋转,可以旋转到竖屏、横屏、反向竖屏、反向横屏四个方向,且受控制中心的旋转开关控制。

AUTO_ROTATION_PORTRAIT_RESTRICTED

9

跟随传感器自动竖向旋转,可以旋转到竖屏、反向竖屏,无法旋转到横屏、反向横屏,且受控制中心的旋转开关控制。

AUTO_ROTATION_LANDSCAPE_RESTRICTED

10

跟随传感器自动横向旋转,可以旋转到横屏、反向横屏,无法旋转到竖屏、反向竖屏,且受控制中心的旋转开关控制。

AUTO_ROTATION_UNSPECIFIED

12

跟随传感器自动旋转,受控制中心的旋转开关控制,且可旋转方向受系统判定。

以三折叠G态(即三折叠设备完全展开时的三屏显示状态)为例,受控制中心控制的自动旋转策略效果图如下:

自由旋转(竖屏/反向竖屏/横屏/反向横屏)

AUTO_ROTATION_RESTRICTED

竖屏旋转(竖屏/反向竖屏)

AUTO_ROTATION_PORTRAIT_RESTRICTED

横屏旋转(横屏/反向横屏)

AUTO_ROTATION_LANDSCAPE_RESTRICTED

跟随传感器自动旋转,受控制中心的旋转开关控制,且可旋转方向受系统判定。

AUTO_ROTATION_UNSPECIFIED

带首选方向的自动旋转

带首选方向的旋转策略允许应用在启动时或调用接口时临时切换到指定方向(如竖屏、横屏等),之后跟随设备传感器自动旋转,且该自动旋转受控制中心“旋转锁定”开关控制,同时可旋转方向受系统对当前设备形态判定的影响,具体可分为以下四类:

展开

名称

说明

USER_ROTATION_PORTRAIT

13

调用时临时旋转到竖屏,之后跟随传感器自动旋转,受控制中心的旋转开关控制,且可旋转方向受系统判定。

USER_ROTATION_LANDSCAPE

14

调用时临时旋转到横屏,之后跟随传感器自动旋转,受控制中心的旋转开关控制,且可旋转方向受系统判定。

USER_ROTATION_PORTRAIT_INVERTED

15

调用时临时旋转到反向竖屏,之后跟随传感器自动旋转,受控制中心的旋转开关控制,且可旋转方向受系统判定。

USER_ROTATION_LANDSCAPE_INVERTED

16

调用时临时旋转到反向横屏,之后跟随传感器自动旋转,受控制中心的旋转开关控制,且可旋转方向受系统判定。

说明

可旋转方向受系统判定:在自动旋转开关开启的状态下,窗口可旋转至的具体方向(如竖屏、横屏、反向横屏等)由系统根据当前设备的形态(如直板机、折叠屏展开态、平板等)自动决定,以提供最佳体验。在具体设备上会禁用不适合用户使用的方向,例如在直板机上可以旋转到竖屏、横屏、反向横屏三个方向,无法旋转到反向竖屏。

跟随桌面显示策略

跟随桌面显示策略适用于适配多种设备形态(如手机、平板、折叠屏)的应用,使应用自动继承系统桌面的旋转策略,从而在不同设备上提供一致且符合用户预期的旋转体验。例如,在同时适配手机和平板的应用中,若希望应用在平板上随桌面横竖屏旋转,而在手机上保持竖屏锁定,可采用此策略,无需为不同设备单独编写复杂的旋转逻辑。

该策略简化了多设备适配的复杂度,开发者无需针对每种设备形态单独配置旋转行为,系统会自动根据桌面状态管理应用窗口的方向。具体实现可参考“跟随桌面的旋转策略”章节。

展开

名称

说明

FOLLOW_DESKTOP

17

表示跟随桌面的旋转模式,如果桌面可以旋转则可旋转,桌面不可旋转则不可旋转。

选择合适的窗口旋转策略

应用在不同业务界面需设置合适的窗口旋转策略,以提供最佳用户体验。

为正确选择旋转策略枚举,开发者可通过通过是否支持自动旋转、支持旋转的方向及预设初始方向三个维度进行匹配,具体参考如下表:

展开

是否支持自动旋转

支持旋转的方向

预设初始方向

窗口旋转策略

固定方向

NA

竖屏

PORTRAIT

NA

横屏

LANDSCAPE

NA

反向竖屏

PORTRAIT_INVERTED

NA

反向横屏

LANDSCAPE_INVERTED

受控自动旋转

竖两向可旋转

NA

AUTO_ROTATION_PORTRAIT_RESTRICTED

横两向可旋转

NA

AUTO_ROTATION_LANDSCAPE_RESTRICTED

最多四向可旋转,但受系统判定

NA

AUTO_ROTATION_UNSPECIFIED

竖屏

USER_ROTATION_PORTRAIT

横屏

USER_ROTATION_LANDSCAPE

反向竖屏

USER_ROTATION_PORTRAIT_INVERTED

反向横屏

USER_ROTATION_LANDSCAPE_INVERTED

跟随桌面策略

NA

NA

FOLLOW_DESKTOP

上述表格也可以抽象为如下的决策逻辑,如图所示:

说明

不推荐使用不受控制中心限制的自动旋转策略,故未将其列入表格。如特定场景需要,可直接使用自动旋转策略中的该策略。

窗户策略工具类

为提升开发者窗口旋转策略选择的易用性,结合上述策略决策逻辑图,我们提供了一套窗口旋转策略选择工具类,支持三种使用形式。

  1. 通过链式属性访问直接获取策略值

    若开发者需要在代码的中硬编码策略值,可通过工具类中的OrientationPresets常量,采用三层递进的链式调用获取旋转策略枚举,示例如下:

    收起
    自动换行
    深色代码主题
    复制
    1. @Component
    2. export struct Home {
    3. windowObj: window.Window | undefined = undefined;
    4. // ...
    5. aboutToAppear(): void {
    6. this.tabBarsInfo.setTabList(TabBarsInfo);
    7. try {
    8. this.windowObj = (this.getUIContext().getHostContext() as common.UIAbilityContext).windowStage.getMainWindowSync()
    9. } catch (err) {
    10. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
    11. }
    12. // Use the WindowOrientationHelper tool to directly obtain the rotation strategy enumeration through chained calls.
    13. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.presets.FOLLOW_DESKTOP)
    14. .catch((err: BusinessError) => {
    15. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
    16. });
    17. // ...
    18. }
    19. aboutToDisappear() {
    20. // Use the WindowOrientationHelper tool to directly obtain the rotation strategy enumeration through chained calls.
    21. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.presets.FIXED.UNSPECIFIED)
    22. .catch((err: BusinessError) => {
    23. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
    24. });
    25. }
    26. // ...
    27. build() {
    28. // ...
    29. }
    30. }
  2. 通过函数式选择器动态选择

    若开发者在特定界面场景下已确定主行为模式,仅需根据条件细化策略,则可采用该方法,示例如下:

    收起
    自动换行
    深色代码主题
    复制
    1. @Component
    2. export struct PortraitModeGame {
    3. windowObj: window.Window | undefined = undefined;
    4. // ...
    5. aboutToAppear(): void {
    6. try {
    7. this.windowObj = (this.getUIContext().getHostContext() as common.UIAbilityContext).windowStage.getMainWindowSync()
    8. } catch (err) {
    9. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
    10. }
    11. // Obtain the PORTRAIT rotation strategy enumeration through the function selector.
    12. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.fixed('PORTRAIT'))
    13. .catch((err: BusinessError) => {
    14. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
    15. });
    16. // ...
    17. }
    18. // ...
    19. build() {
    20. // ...
    21. }
    22. }
  3. 通过通用选择器动态选择

    若开发者在特定界面场景下所有旋转参数均需动态确定,可通过select方法,利用联合参数定义策略选择器入参实现,示例如下:

    收起
    自动换行
    深色代码主题
    复制
    1. @Component
    2. export struct VideoDetail {
    3. windowObj: window.Window | undefined = undefined;
    4. // ...
    5. aboutToAppear() {
    6. // ...
    7. // Dynamically select an appropriate rotation strategy through a selector.
    8. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.select({
    9. mode: 'autoRotate',
    10. range: 'ALL_ORIENTATIONS',
    11. preferred: 'UNSPECIFIED'
    12. }))
    13. .catch((err: BusinessError) => {
    14. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
    15. });
    16. }
    17. onFullScreenChange(): void {
    18. if (this.isFullScreen) {
    19. if (this.isClick) {
    20. if (this.widthBp === WidthBreakpoint.WIDTH_SM || this.widthBp === WidthBreakpoint.WIDTH_LG ||
    21. this.heightBp === HeightBreakpoint.HEIGHT_LG) {
    22. // Dynamically select an appropriate rotation strategy through a selector.
    23. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.select({
    24. mode: 'autoRotate',
    25. range: 'LANDSCAPE_ONLY'
    26. }))
    27. .catch((err: BusinessError) => {
    28. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
    29. });
    30. }
    31. }
    32. } else {
    33. // Dynamically select an appropriate rotation strategy through a selector.
    34. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.select({
    35. mode: 'autoRotate',
    36. range: 'ALL_ORIENTATIONS',
    37. preferred: 'UNSPECIFIED'
    38. }))
    39. .catch((err: BusinessError) => {
    40. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
    41. });
    42. }
    43. }
    44. private onWindowSizeChange: (windowSize: window.Size) => void = () => {
    45. if (this.isClick) {
    46. return;
    47. }
    48. if (this.widthBp === WidthBreakpoint.WIDTH_SM) {
    49. this.isFullScreen = false
    50. // Dynamically select an appropriate rotation strategy through a selector.
    51. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.select({
    52. mode: 'autoRotate',
    53. range: 'ALL_ORIENTATIONS',
    54. preferred: 'UNSPECIFIED'
    55. }))
    56. .catch((err: BusinessError) => {
    57. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
    58. });
    59. }
    60. if (this.widthBp === WidthBreakpoint.WIDTH_MD && this.heightBp === HeightBreakpoint.HEIGHT_SM) {
    61. this.isFullScreen = true;
    62. }
    63. };
    64. async aboutToDisappear() {
    65. // ...
    66. // Dynamically select an appropriate rotation strategy through a selector.
    67. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.select({
    68. mode: 'autoRotate',
    69. range: 'ALL_ORIENTATIONS',
    70. preferred: 'UNSPECIFIED'
    71. }))
    72. .catch((err: BusinessError) => {
    73. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
    74. });
    75. // ...
    76. }
    77. build() {
    78. // ...
    79. }
    80. }

    在工具类中,select方法的入参类型为OrientationConfig,根据逻辑选型分为三种子类型:自动旋转AutoRotateConfig、跟随桌面FollowDesktopConfig及固定方向FixedConfig。例如自动旋转分支,采用三层抽象的维度,需在配置中继续添加旋转范围range字段及首选方向preferred字段,以确定符合场景的窗口旋转策略。OrientationConfig类型定义如下:

    收起
    自动换行
    深色代码主题
    复制
    1. /**
    2. * Orientation Type (used uniformly for fixed orientation and preferred orientation of auto rotation)
    3. */
    4. export type WindowOrientationType =
    5. | 'UNSPECIFIED' // Unspecified (lock current orientation for fixed mode, no preferred orientation for auto rotation)
    6. | 'PORTRAIT'
    7. | 'LANDSCAPE'
    8. | 'PORTRAIT_INVERTED'
    9. | 'LANDSCAPE_INVERTED';
    10. /**
    11. * Auto Rotation Range
    12. */
    13. export type AutoRotateRange =
    14. | 'LANDSCAPE_ONLY' // Landscape only (including forward and reverse landscape)
    15. | 'PORTRAIT_ONLY' // Portrait only (including forward and reverse portrait)
    16. | 'ALL_ORIENTATIONS'; // All orientations (support all directions)
    17. // ...
    18. /**
    19. * Fixed Orientation Configuration
    20. */
    21. export interface FixedConfig {
    22. mode: 'fixed';
    23. orientation?: WindowOrientationType; // Omitted or 'UNSPECIFIED' means lock current orientation
    24. }
    25. /**
    26. * Auto Rotation Configuration
    27. */
    28. export interface AutoRotateConfig {
    29. mode: 'autoRotate';
    30. range: AutoRotateRange; // Rotation range
    31. preferred?: WindowOrientationType; // Preferred orientation (valid only when range = 'ALL_ORIENTATIONS')
    32. }
    33. /**
    34. * Follow Desktop Configuration
    35. */
    36. export interface FollowDesktopConfig {
    37. mode: 'followDesktop';
    38. }
    39. /**
    40. * Union type of window rotation strategy configuration
    41. */
    42. export type OrientationConfig = FixedConfig | AutoRotateConfig | FollowDesktopConfig;

    上述工具类的使用示例可参考本文典型场景中的各类案例。

为应用配置旋转策略

为了满足灵活多变的UI交互需求,系统支持应用级窗口级页面级的窗口旋转策略配置方案,并提供子窗口悬浮窗旋转的窗口旋转策略配置。

应用级配置

通过在hap包的module.json5文件中配置orientation属性,可设置应用的初始窗口旋转策略,会影响整个应用的启动方向。

该字段用于配置应用启动时的窗口显示状态。若应用需以默认的横屏或竖屏方式启动,应在字段中进行相应配置。

其支持的参数可以参考module.json5配置项中abilities标签下orientation的orientation枚举值。

收起
自动换行
深色代码主题
复制
  1. {
  2. "module": {
  3. // ...
  4. "abilities": [
  5. {
  6. "name": "EntryAbility",
  7. // ...
  8. "orientation": "unspecified",
  9. // ...
  10. }
  11. ],
  12. // ...
  13. }
  14. }

应用可根据业务需求配置默认旋转策略:

  • 若应用在直板机和双折叠折叠态是竖屏应用,平板和双折叠展开态是可旋转应用,推荐配置FOLLOW_DESKTOP为默认旋转策略。
  • 若应用为竖屏应用,建议配置PORTRAIT为默认旋转策略。
  • 若应用为横屏应用(如MOBA类游戏),启动时默认为横屏,存在以下两种情况:
    • 仅支持横屏时,建议配置LANDSCAPE为默认旋转策略;
    • 支持横屏和反向横屏切换时,建议配置AUTO_ROTATION_LANDSCAPE或AUTO_ROTATION_LANDSCAPE_RESTRICTED(是否受控制中心旋转开关控制)。
  • 若应用为可旋转应用,建议配置AUTO_ROTATION_RESTRICTED为默认旋转策略。
说明

对于需要通过控制中心进行旋转锁定控制的情况,可选择字段后方带有RESTRICTED字段的旋转策略。

该字段表示旋转行为受到控制中心按钮控制:开关打开时,不随设备方向旋转;关闭时,则跟随设备旋转。

以备忘录应用为例,当系统关闭旋转锁定后,应用页面会随手机旋转自动切换横竖屏;打开旋转锁定时,则不会发生旋转行为,此时需配置为AUTO_ROTATION_RESTRICTED。

窗口级配置

它作用于整个应用窗口(window),定义该窗口的横竖屏旋转策略,并对基于Navigation组件和Router模块实现的路由跳转均生效。一旦配置,除非显式修改,否则对窗口内所有页面生效。

1. 在onWindowStageCreate()中调用window.setPreferredOrientation()方法即可设置整个应用窗口默认方向。

收起
自动换行
深色代码主题
复制
  1. setWindowOrientation(orientation: window.Orientation): void {
  2. this.mainWindow.setPreferredOrientation(orientation)
  3. .then(() => {
  4. hilog.info(0x0000, 'testLog', `Succeeded in setting window orientation.`);
  5. // Update window orientation.
  6. this.mainWindowInfo.orientation = orientation;
  7. })
  8. .catch((err: BusinessError) => {
  9. hilog.error(0x0000, 'testLog', `Failed to set window orientation. Code: ${err.code}, message: ${err.message}`);
  10. });
  11. }

2. 如果应用内页面的窗口旋转策略不一致,则需要执行本步骤。在页面进入时(aboutToAppear),调用window.setPreferredOrientation()定义当前页面对应的窗口旋转策略;在页面退出时(aboutToDisappear),调用window.setPreferredOrientation()恢复即将展示页面对应的窗口旋转策略。

收起
自动换行
深色代码主题
复制
  1. @StorageLink('mainWindow') mainWindow?: window.Window = undefined;
  2. public lastOrientation?: window.Orientation;
  3. aboutToAppear(): void {
  4. if (this.mainWindow === undefined) {
  5. return;
  6. }
  7. this.lastOrientation = this.mainWindow!.getPreferredOrientation();
  8. this.mainWindow!.setPreferredOrientation(window.Orientation.LANDSCAPE);
  9. }
  10. aboutToDisappear(): void {
  11. this.mainWindow!.setPreferredOrientation(this.lastOrientation)
  12. }

典型场景如一些视频类应用、图片类应用等。

视频播窗横竖屏切换

页面级配置

它作用于当前显示的具体页面(NavDestination组件),仅对基于Navigation组件实现的路由跳转生效。它允许根据业务需求动态调整不同页面的窗口旋转策略。在页面路由跳转时,系统自动切换为下一个展示页面对应的窗口旋转策略。

NavDestination组件提供preferredOrientation属性,支持每个页面独立配置窗口旋转策略,互相不影响。页面跳转时,窗口旋转策略自动更新为下一个页面对应的preferredOrientation。页面返回时,窗口旋转策略也会自动更新为上一个页面对应的preferredOrientation。

方案对比

展开

窗口旋转策略配置方案

优势

劣势

推荐使用场景

应用级

  • 可设置应用启动的初始方向
  • 应用所有页面窗口旋转策略一致时仅需配置一次

应用内页面窗口旋转策略不一致时,无法切换,需要配合窗口级或页面级窗口旋转策略

  • 应用需要设置启动的初始方向。
  • 应用所有页面窗口旋转策略一致。

窗口级

  • 配置后同一窗口内所有页面生效
  • 支持Navigation组件与Router模块实现的路由
  • 版本兼容性高(API9+

页面窗口旋转策略不一致时,需要在页面进入及退出时设置两次窗口旋转策略。

  • 使用Router模块实现页面路由
  • 应用基于API19之前的版本开发

页面级

  • 单独配置每个页面的窗口旋转策略,页面跳转时窗口旋转策略跟随自动更新
  • 针对页面配置窗口旋转策略,使用更简单、更灵活
  • 版本兼容性有限(API19+
  • 仅支持Navigation组件实现的页面路由
  • 应用内页面的窗口旋转策略多处不一致
  • 基于Navigation模块实现页面路由
  • 应用基于API19之后的版本开发

应用子窗口的旋转

在应用旋转场景中,应用主窗的尺寸由系统控制,而应用子窗的尺寸和位置由应用控制。因此,建议应用开发者在有应用子窗的旋转场景中,同步调整应用子窗的尺寸和位置,避免因旋转过程中应用子窗的尺寸和位置保持不变而导致如下图所示的应用子窗显示截断问题(直板机默认的旋转策略为UNSPECIFIED,旋转锁定按钮关闭的情况下不允许应用旋转,可以通过module.json5配置文件中abilities标签的"orientation"字段配置应用的旋转策略为AUTO_ROTATION,使应用跟随设备方向旋转)。

展开

旋转前竖屏显示

旋转后横屏显示(调整前)

实现方案

系统为设备窗口尺寸变化监听、设置应用子窗尺寸和位置提供了如下接口:

  1. on('windowSizeChange')接口用于开启窗口尺寸变化的监听,当窗口发生旋转后,会触发其中的回调。
  2. resize()接口用于改变当前窗口的大小,可以在窗口发生旋转后及时调整子窗的宽高。
  3. moveWindowTo()接口用于移动窗口位置,可以在窗口发生旋转后及时调整子窗的位置。

为实现根据应用旋转方向设置应用子窗尺寸,开发者可使用on('windowSizeChange')接口监听窗口尺寸的变化,并在回调函数中通过resize()接口和moveWindowTo()接口分别调整应用子窗的尺寸和位置。

需要指出的是,开发者可以使用setFollowParentWindowLayoutEnabled()接口设置子窗或模态窗口的布局信息是否跟随主窗,如果设置为跟随主窗,那么子窗的旋转便不再需要额外适配。

收起
自动换行
深色代码主题
复制
  1. import { window } from '@kit.ArkUI';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. import { hilog } from '@kit.PerformanceAnalysisKit';
  4. const SUB_WINDOW_LEFT_OFFSET: number = 50;
  5. const SUB_WINDOW_TOP_OFFSET: number = 500;
  6. const TAG: string = 'subWindowAdaptWhenRotate';
  7. const DOMAIN: number = 0x0000;
  8. @Entry
  9. @Component
  10. struct Index {
  11. public mainWindow: window.Window | undefined = undefined;
  12. public subWindow: window.Window | undefined = undefined;
  13. aboutToAppear(): void {
  14. // create subWindow
  15. this.createSubWindow();
  16. this.mainWindow = AppStorage.get('mainWindow');
  17. if (!this.mainWindow) {
  18. return;
  19. }
  20. this.mainWindow.on('windowSizeChange', () => {
  21. this.adjustSubwindowSizeAndPosition();
  22. })
  23. }
  24. private adjustSubwindowSizeAndPosition(): void {
  25. if (!this.subWindow) {
  26. hilog.error(DOMAIN, TAG, 'subWindow is null');
  27. return;
  28. }
  29. let subwindowRect: window.Rect | null = null;
  30. try {
  31. subwindowRect = this.subWindow.getWindowProperties().windowRect;
  32. } catch (error) {
  33. hilog.warn(0x000, 'testTag', `getWindowProperties failed, code: ${error.code}, message: ${error.message}`);
  34. }
  35. let newWidth: number = subwindowRect!.height;
  36. let newHeight: number = subwindowRect!.width;
  37. let newX: number = subwindowRect!.top;
  38. let newY: number = subwindowRect!.left;
  39. this.subWindow.resize(newWidth, newHeight)
  40. .then(() => {
  41. hilog.info(DOMAIN, TAG, 'Succeeded in changing the window size')
  42. }).catch((err: BusinessError) => {
  43. hilog.error(DOMAIN, TAG, `Failed to change the window size. Cause code: ${err.code}, message: ${err.message}`);
  44. });
  45. this.subWindow.moveWindowTo(newX, newY)
  46. .then(() => {
  47. hilog.info(DOMAIN, TAG, 'Succeeded in moving the window');
  48. }).catch((err: BusinessError) => {
  49. hilog.error(DOMAIN, TAG, `Failed to move the window. Cause code: ${err.code}, message: ${err.message}`);
  50. });
  51. }
  52. // ...
  53. }

实现效果

根据示例代码为不同旋转方向设置不同的应用子窗尺寸和位置的实际效果如下图所示,应用子窗的尺寸和位置在竖屏显示和横屏显示下是不同的。

展开

旋转前竖屏显示

旋转后横屏显示

悬浮窗的旋转

悬浮窗默认是竖向的,但是对于横向游戏和视频应用,横向的悬浮窗体验会更好。开发者可以通过在module.json5配置文件中abilities标签下的preferMultiWindowOrientation属性增加“landscape”或者“landscape_auto”,配合API以声明应用支持横向悬浮窗或上下分屏模式。

收起
自动换行
深色代码主题
复制
  1. {
  2. "module": {
  3. // ...
  4. "abilities": [
  5. {
  6. "name": "EntryAbility",
  7. // ...
  8. "preferMultiWindowOrientation": "landscape_auto",
  9. // ...
  10. }
  11. ],
  12. // ...
  13. }
  14. }

该场景下多窗布局动态可变为横向,需要配合API(enableLandscapeMultiWindow()/ disableLandscapeMultiWindow())使用。

收起
自动换行
深色代码主题
复制
  1. private windowClass = (this.getUIContext().getHostContext() as common.UIAbilityContext).windowStage.getMainWindowSync()
  2. aboutToAppear(): void {
  3. this.windowClass.enableLandscapeMultiWindow();
  4. }
  5. aboutToDisappear(): void {
  6. this.windowClass.disableLandscapeMultiWindow();
  7. }

例如:视频或者游戏类应用在横屏模式下开启悬浮窗后,页面没有适配横屏,导致内容显示不全或者观看体验不好。

优化后效果如下图所示。

为多设备配置旋转策略

随着设备的多样化,应用某些页面需要根据设备类型配置不同的窗口旋转策略以达到较好的用户体验,为了开发者能快速适配不同设备,我们提供了多设备的窗口旋转策略。

背景

  1. 不同设备对旋转策略的使用约束不同

    下述特定场景下,由于产品定义与使用场景的不同,开发者自定义的窗口旋转策略可能会显著降低用户体验,因此系统配置的窗口旋转策略优先级会高于应用配置。此时,应用实际显示的窗口方向将由系统统一调度,开发者自定义的窗口旋转策略将被覆盖而不生效。

    展开

    设备场景

    Pura X折叠态

    电脑

    智慧屏

    智能穿戴

    特定显示方向

    跟随屏幕方向显示

    效果图

  2. 不同交互场景对旋转策略的使用约束不同
    例如下述场景中,自由多窗不支持竖屏模式,悬浮窗默认是竖向的,但是但是对于横向游戏和视频应用,横向的悬浮窗体验会更好。
    展开

    使用场景

    分屏

    全景多窗

    自由多窗

    全局批注

    任务列表视图

    特定显示方向

    跟随传感器自动旋转,可以旋转到竖屏、横屏、反向竖屏、反向横屏四个方向,且受控制中心的旋转开关控制

    跟随屏幕方向显示

    手写笔点击全局批注后,锁定当前窗口方向

    锁定当前窗口方向

    效果图

  3. 相同的页面,开发者希望在不同的设备上,应用不同的旋转策略。例如:视频详情页应用在直板机上默认只能竖向,而在折叠屏展开态则希望能四个方向自由旋转。
  4. 由于设备的形态差异,应用在不同的设备上也希望有不同的启动方向。

跟随桌面的旋转策略

当前HarmonyOS主流设备桌面的横竖屏旋转策略如下表所示:

展开

产品类型

手机

阔折(Pura X系列)

大阔折(Pura X MAX系列)

双折叠(Mate X系列)

三折叠(Mate XT系列)

平板

电脑

是否支持横竖屏旋转

不支持

内屏:不支持

外屏:不支持

内屏:支持

外屏:不支持

内屏:支持

外屏:不支持

F态(单屏显示):不支持

M态(双屏显示):支持

G态(三屏显示):支持

支持

应用无法配置窗口旋转策略

对于某些应用,在直板手机上默认采用竖屏显示策略,但在平板或折叠屏设备上,需支持自动旋转。若在Ability的生命周期中调用setPreferredOrientation,可能会导致应用启动时出现旋转动画。因此,可通过修改module.json5配置文件中的orientation属性,设置为FOLLOW_DESKTOP,以跟随桌面的旋转模式。

实现响应式旋转策略

在设备切换形态时,有时应用对于相同页面希望采用不同的旋转策略,这时需要通过监听设备的窗口尺寸变化配合系统断点实现响应式旋转策略,至于断点与设备的映射关系,请先了解响应式布局

1.在应用EntryAbility的onWindowStageCreate生命周期中,通过on('windowSizeChange')方法监听窗口尺寸变化,在其回调中通过getWindowWidthBreakpoint()及getWindowHeightBreakpoint()实时获取并存储横竖断点变化信息,配合各个页面实现响应式旋转策略。

收起
自动换行
深色代码主题
复制
  1. export default class EntryAbility extends UIAbility {
  2. uiContext?: UIContext;
  3. onWindowSizeChange: (windowSize: window.Size) => void = () => {
  4. let widthBp: WidthBreakpoint = this.uiContext!.getWindowWidthBreakpoint();
  5. AppStorage.setOrCreate(CommonConstants.WIDTH_BREAK_POINT, widthBp);
  6. let heightBp: HeightBreakpoint = this.uiContext!.getWindowHeightBreakpoint();
  7. AppStorage.setOrCreate(CommonConstants.HEIGHT_BREAK_POINT, heightBp);
  8. }
  9. // ...
  10. onWindowStageCreate(windowStage: window.WindowStage): void {
  11. // ...
  12. windowStage.loadContent('pages/Index', (err) => {
  13. // ...
  14. windowStage.getMainWindow().then((data: window.Window) => {
  15. try {
  16. this.uiContext = data.getUIContext();
  17. } catch (err) {
  18. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  19. }
  20. let widthBp: WidthBreakpoint = this.uiContext!.getWindowWidthBreakpoint();
  21. AppStorage.setOrCreate(CommonConstants.WIDTH_BREAK_POINT, widthBp);
  22. let heightBp: HeightBreakpoint = this.uiContext!.getWindowHeightBreakpoint();
  23. AppStorage.setOrCreate(CommonConstants.HEIGHT_BREAK_POINT, heightBp);
  24. data.on('windowSizeChange', this.onWindowSizeChange);
  25. }).catch((err: BusinessError) => {
  26. hilog.error(0x0000, 'testTag', `Error occured, error code: ${err.code}, error message: ${err.message}`);
  27. })
  28. });
  29. }
  30. // ...
  31. }

2.在需要实现响应式旋转策略页面的aboutToAppear生命周期中,通过on('windowSizeChange')方法监听窗口尺寸变化,在其回调中实时获取设备的窗口尺寸变化信息。

收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct VideoDetail {
  3. windowObj: window.Window | undefined = undefined;
  4. // ...
  5. aboutToAppear() {
  6. try {
  7. this.windowObj = (this.getUIContext().getHostContext() as common.UIAbilityContext).windowStage.getMainWindowSync()
  8. } catch (err) {
  9. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  10. }
  11. // ...
  12. this.windowObj?.on('windowSizeChange', this.onWindowSizeChange);
  13. // ...
  14. }
  15. // ...
  16. }

并在aboutToDisappear中取消监听:

收起
自动换行
深色代码主题
复制
  1. async aboutToDisappear() {
  2. // ...
  3. this.windowObj?.off('windowSizeChange')
  4. }

3.在页面windowSizeChange回调方法中,配合全局横竖断点变化,保证页面切换时不同设备上配置合适的窗口旋转策略。

收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct VideoDetail {
  3. // ...
  4. @StorageLink(CommonConstants.WIDTH_BREAK_POINT) widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_SM;
  5. @StorageLink(CommonConstants.HEIGHT_BREAK_POINT) heightBp: HeightBreakpoint = HeightBreakpoint.HEIGHT_SM;
  6. // ...
  7. // ...
  8. private onWindowSizeChange: (windowSize: window.Size) => void = () => {
  9. if (this.isClick) {
  10. return;
  11. }
  12. if (this.widthBp === WidthBreakpoint.WIDTH_SM) {
  13. this.isFullScreen = false
  14. // Dynamically select an appropriate rotation strategy through a selector.
  15. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.select({
  16. mode: 'autoRotate',
  17. range: 'ALL_ORIENTATIONS',
  18. preferred: 'UNSPECIFIED'
  19. }))
  20. .catch((err: BusinessError) => {
  21. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  22. });
  23. }
  24. if (this.widthBp === WidthBreakpoint.WIDTH_MD && this.heightBp === HeightBreakpoint.HEIGHT_SM) {
  25. this.isFullScreen = true;
  26. }
  27. };
  28. // ...
  29. build() {
  30. // ...
  31. }

在折叠屏设备上,通过display.on('foldStatusChange', callback())方法监听折叠的状态,并通过@StorageLink('isHalfFolded')保存并实时更新全局变量。

收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct VideoPlayer {
  3. // ...
  4. @StorageLink('isHalfFolded') isHalfFolded: boolean = false;
  5. // ...
  6. private onFoldStatusChange: Callback<display.FoldStatus> = (data: display.FoldStatus) => {
  7. this.foldStatus = data;
  8. if (canIUse('SystemCapability.Window.SessionManager')) {
  9. if (data === display.FoldStatus.FOLD_STATUS_EXPANDED || data === display.FoldStatus.FOLD_STATUS_FOLDED ||
  10. data === display.FoldStatus.FOLD_STATUS_EXPANDED_WITH_SECOND_EXPANDED ||
  11. data === display.FoldStatus.FOLD_STATUS_FOLDED_WITH_SECOND_EXPANDED) {
  12. let widthBp: WidthBreakpoint = this.getUIContext().getWindowWidthBreakpoint();
  13. AppStorage.setOrCreate(CommonConstants.WIDTH_BREAK_POINT, widthBp);
  14. let heightBp: HeightBreakpoint = this.getUIContext().getWindowHeightBreakpoint();
  15. AppStorage.setOrCreate(CommonConstants.HEIGHT_BREAK_POINT, heightBp);
  16. }
  17. if (data === display.FoldStatus.FOLD_STATUS_FOLDED_WITH_SECOND_EXPANDED && this.isFullScreen) {
  18. this.windowObj?.setPreferredOrientation(window.Orientation.AUTO_ROTATION_LANDSCAPE_RESTRICTED)
  19. .catch((err: BusinessError) => {
  20. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  21. });
  22. } else {
  23. this.windowObj?.setPreferredOrientation(window.Orientation.AUTO_ROTATION_UNSPECIFIED)
  24. .catch((err: BusinessError) => {
  25. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  26. });
  27. }
  28. }
  29. };
  30. aboutToAppear(): void {
  31. // ...
  32. if (canIUse('SystemCapability.Window.SessionManager')) {
  33. try {
  34. display.on('foldStatusChange', this.onFoldStatusChange);
  35. } catch (error) {
  36. let err = error as BusinessError;
  37. Logger.error('VideoPlayer', `onFoldStatusChange failed, code = ${err.code}, message = ${err.message}`);
  38. }
  39. }
  40. }
  41. // ...
  42. build() {
  43. // ...
  44. }

优化横竖屏切换性能

在窗口旋转时,屏幕尺寸变化会导致界面重新布局。为提高横竖屏切换的流畅度,需进行性能优化。

使用自定义组件冻结

旋转时,由于整窗一起旋转,会导致页面重新布局,但是实际上需要展示的可能只有播放内容,对于其他的组件可以使用自定义组件冻结功能,避免由于旋转导致的UI更新操作。例如视频播放底下的详情内容,可能是单独的组件。

收起
自动换行
深色代码主题
复制
  1. @Component({ freezeWhenInactive: true })
  2. // Added custom component freezing function
  3. struct VideoDetailView {
  4. build() {
  5. Scroll() {
  6. // ...
  7. }
  8. }
  9. }

对图片使用autoResize

如果当前旋转页面存在一些图片,未经合理的裁剪,图片过大,可以对图片设置autoResize属性,使图片裁剪到合适的大小进行绘制。该属性是将组件显示区域作为绘制的图源尺寸,以减少内存占用。例如原图是1920px*1080px,但是显示区域是200vp*100vp,则在解码时会降低采样编码到200vp*100vp尺寸。

收起
自动换行
深色代码主题
复制
  1. @Builder
  2. function ImageItem(imageSrc: ResourceStr) {
  3. Stack({}) {
  4. Image(imageSrc)
  5. .width('100%')
  6. .height('100%')
  7. .autoResize(true)// Use auto_resize attributes on images
  8. .borderRadius(8)
  9. .objectFit(ImageFit.Fill)
  10. .backgroundColor('#1AFFFFFF')
  11. }
  12. }

排查一些耗时操作

排查当前页面是否存在冗余的OnAreaChange事件、blur模糊属性或linearGradient属性,这些属性较为耗时,应根据是否必须使用来决定是否进行优化。

使用多设备工具模块设置窗口旋转策略

模块简介

在 HarmonyOS 应用开发中,系统提供了多种窗口旋转策略(详情请参考了解窗口旋转策略),涵盖了固定方向、自动旋转、跟随桌面等多种模式。策略枚举数量多且分类复杂,开发者在不同业务场景下需反复查阅文档进行选型;在多设备(直板机、折叠屏、平板等)适配时,需针对各设备使用场景单独实现适配逻辑,缺乏统一的封装与动态适配能力,导致代码复杂度高、难以维护与复用。

多设备工具模块的设计初衷是为了解决上述痛点。它内置了20种常见页面类型的预设窗口策略配置,将复杂的代码逻辑判断抽象为简洁的配置声明,开发者仅需传入对应的页面类型,即可利用模块完成窗口策略的解析与应用。同时,模块提供了响应式规则引擎,支持根据设备形态动态匹配窗口策略配置,实现一次配置、多设备自适应。

下图展示了应用层、多设备工具模块与系统层之间的整体协作流程:

响应式规则引擎(responsiverule目录模块)

响应式规则引擎模块的核心是通过内部的一系列规则动态匹配窗口旋转策略。

窗口策略控制(orientation目录模块)

窗口策略控制模块负责将解析后的方向配置应用到系统窗口。其中,OrientationStrategy提供从抽象配置到系统枚举的映射逻辑,支持fixed,autoRotate、followDesktop三种模式;presets提供了开箱即用的20种常见页面类型预设方向配置。

协作关系

窗口策略控制中的预设或自定义配置提供选择内容,规则引擎决定匹配方式,窗口策略控制中的OrientationStrategy负责应用生效。开发者只需将设备上下文及预设或自定义配置传给响应式规则引擎,响应式规则引擎会根据窗口形态、设备形态、折叠状态等变化动态匹配返回一个窗口策略配置值,OrientationStrategy模块将此返回值映射为对应的系统窗口旋转策略。

响应式规则引擎

在多设备适配场景中,同一个页面在不同设备形态下往往需要采用不同的窗口旋转策略。传统做法需要在每个页面中手动实现条件判断逻辑,根据设备尺寸和宽高比进行窗口旋转策略适配,代码分散且难以维护。响应式规则引擎将这类条件判断抽象为声明式的规则配置——开发者只需描述"在什么条件下使用什么策略",引擎在运行时根据实际的设备上下文自动求值并返回匹配的配置,消除了手写条件分支的繁琐工作。

规则结构

规则引擎的核心数据结构由三层组成:

展开

层级

类型

说明

条件

Condition

由字段名 field、运算符 operator、比较值 value 组成,一个完整的Condition如:

{ field: 'windowWidthVp', operator: EQUAL, value: 600 },代表的条件为“窗口宽度的vp值是否等于600”。

规则

Rule<T>

一组条件的 AND 组合(conditions: Condition[]),全部满足时命中,返回对应的 value: T,一个完整的Rule<number>如:{conditions: [{ field: 'windowWidthVp', operator: GREATER_THAN_OR_EQUAL, value: 600 },{ field: 'windowWidthVp', operator: LESS_THAN, value: 840 }],value: 2},代表的条件为“当窗口宽度的vp值大于等于600且小于840时,返回数值2”。

响应式值

ConditionResponsiveValue<T>

顶层容器,包含规则列表 rules: Rule<T>[] 和兜底默认值 defaultValue: T。

运算符

引擎内置多种operator运算符,按使用场景可分为以下四类:

  • 数值比较:
展开

运算符

含义

EQUAL

等于

NOT_EQUAL

不等于

GREATER_THAN

大于

GREATER_THAN_OR_EQUAL

大于等于

LESS_THAN

小于

LESS_THAN_OR_EQUAL

小于等于

  • 区间判断:
展开

运算符

含义

BETWEEN

闭区间

BETWEEN_LEFT_OPEN

左开区间

BETWEEN_RIGHT_OPEN

右开区间

BETWEEN_OPEN

开区间

NOT_BETWEEN

不在区间内

  • 字符串匹配
展开

运算符

含义

CONTAINS

包含子串

NOT_CONTAINS

不包含子串

STARTS_WITH

以...开头

ENDS_WITH

以...结尾

MATCHES

正则匹配(条件值为正则表达式字符串)

  • 集合运算:
展开

运算符

含义

IN

在集合中

NOT_IN

不在集合中(同上取反)

求值流程

ResponsiveValueResolver.getValue(context: Object | undefined, responsiveValue: ResponsiveValue<T>) 是引擎的求值入口,其内部流程可总结为如下四点:

  1. 短路匹配:规则按 Rules 数组顺序求值,首条全部条件命中的规则立即返回,后续规则不再执行。
  2. AND 语义:同一条规则内的多个条件必须全部满足,任一条件失败则整条规则跳过。
  3. 类型安全:引擎在求值前进行运行时类型检查,类型不匹配时输出警告并回退到defaultValue,而非静默失败。
  4. 兜底保障:无论context是否完整、规则是否匹配,始终有defaultValue作为最终结果返回,保证不中断业务流程。
收起
自动换行
深色代码主题
复制
  1. export class ResponsiveValueResolver {
  2. // Evaluate rules against the developer-defined Context.
  3. // Returns the first matching rule's value, or defaultValue when none match.
  4. // Context is typed as Object so the developer can pass their own context class.
  5. static getValue<T>(context: Object | undefined, responsiveValue: ResponsiveValue<T>): T | undefined {
  6. if (!responsiveValue) {
  7. return undefined;
  8. }
  9. if (!context) {
  10. return responsiveValue.defaultValue;
  11. }
  12. const ctxRecord: Record<string, ConditionValue> = context as Object as Record<string, ConditionValue>;
  13. for (let i = 0; i < responsiveValue.rules.length; i++) {
  14. const rule: Rule<T> = responsiveValue.rules[i];
  15. let allMatch = true;
  16. for (let j = 0; j < rule.conditions.length; j++) {
  17. const condition: Condition = rule.conditions[j];
  18. const ctxValue: ConditionValue | undefined = ctxRecord[condition.field];
  19. if (ctxValue === undefined) {
  20. Logger.warn(
  21. `[ResponsiveValueResolver] field '${condition.field}' not in context, rule will fall back to defaultValue`);
  22. allMatch = false;
  23. break;
  24. }
  25. if (!ResponsiveValueResolver.isConditionValue(ctxValue as Object)) {
  26. Logger.warn(
  27. `[ResponsiveValueResolver] field '${condition.field}' value is not a valid ConditionValue ` +
  28. `(string|number|boolean|array of them), rule will fall back to defaultValue`);
  29. allMatch = false;
  30. break;
  31. }
  32. const predicate: Predicate = OPERATOR_PREDICATES[condition.operator];
  33. if (!predicate) {
  34. Logger.warn(`[ResponsiveValueResolver] Unknown operator: ${condition.operator}`);
  35. allMatch = false;
  36. break;
  37. }
  38. if (!ResponsiveValueResolver.areTypesCompatible(condition.operator, ctxValue, condition.value)) {
  39. Logger.warn(
  40. `[ResponsiveValueResolver] type mismatch for field '${condition.field}':` +
  41. ` operator '${condition.operator}' is incompatible with context value and condition value types,` +
  42. ` rule will fall back to defaultValue`);
  43. allMatch = false;
  44. break;
  45. }
  46. if (!predicate(ctxValue, condition.value)) {
  47. allMatch = false;
  48. break;
  49. }
  50. }
  51. if (allMatch) {
  52. return rule.value;
  53. }
  54. }
  55. return responsiveValue.defaultValue;
  56. }
  57. // Validate that a context field value conforms to ConditionValue at runtime,
  58. // guarding against unsafe casts when a developer passes a custom context object.
  59. private static isConditionValue(value: Object): boolean {
  60. // ...
  61. }
  62. // Ensure the runtime types of ctxValue and condValue are compatible with the
  63. // operator before invoking the predicate, so mismatches are reported instead
  64. // of silently making a rule fail to match.
  65. private static areTypesCompatible(operator: Operator, ctxValue: ConditionValue, condValue: ConditionValue): boolean {
  66. // ...
  67. }
  68. }

横向视频全屏播放场景的窗口策略规则代码示例

长视频应用的横向视频全屏视频播放页配置了完整的条件规则,是理解规则引擎的最佳切入点。横向视频在全屏播放时,不同设备形态需要不同的旋转策略,对应的预设配置定义如下:

收起
自动换行
深色代码主题
复制
  1. export class ResponsiveOrientationConfig {
  2. // ...
  3. private static getLandscapeVideoFullscreenConfigData(): ConditionResponsiveValue<OrientationConfig> {
  4. return {
  5. rules: [
  6. {
  7. conditions: [
  8. { field: 'displayLongEdgeVp', operator: Operator.GREATER_THAN_OR_EQUAL,
  9. value: LANDSCAPE_FULLSCREEN_LONG_EDGE_MIN },
  10. { field: 'displayLongEdgeVp', operator: Operator.LESS_THAN,
  11. value: LANDSCAPE_FULLSCREEN_LONG_EDGE_MID },
  12. { field: 'displayLongShortRatio', operator: Operator.GREATER_THAN_OR_EQUAL,
  13. value: LANDSCAPE_FULLSCREEN_RATIO_BAND_1 }
  14. ],
  15. value: { mode: 'autoRotate', range: AutoRotateRange.LANDSCAPE_ONLY }
  16. },
  17. {
  18. conditions: [
  19. { field: 'displayLongEdgeVp', operator: Operator.GREATER_THAN_OR_EQUAL,
  20. value: LANDSCAPE_FULLSCREEN_LONG_EDGE_MID },
  21. { field: 'displayLongEdgeVp', operator: Operator.LESS_THAN,
  22. value: LANDSCAPE_FULLSCREEN_LONG_EDGE_MAX },
  23. { field: 'displayLongShortRatio', operator: Operator.GREATER_THAN_OR_EQUAL,
  24. value: LANDSCAPE_FULLSCREEN_RATIO_BAND_2 }
  25. ],
  26. value: { mode: 'autoRotate', range: AutoRotateRange.LANDSCAPE_ONLY }
  27. }
  28. ],
  29. defaultValue: { mode: 'autoRotate', range: AutoRotateRange.ALL_ORIENTATIONS }
  30. };
  31. }
  32. }

不同设备下的匹配结果:

展开

设备形态

设备长边 (vp)

设备长宽比

命中规则

最终策略

直板机

600~840

1.25

规则1

仅横屏旋转

窄长直板机(以Pocket2为例)

861

2.36

规则2

仅横屏旋转

双折叠折叠态(以Mate X5为例)

801

2.32

规则1

仅横屏旋转

双折叠展开态(以Mate X5为例)

798

1.12

无命中规则

全方向自由旋转

Pura X Max折叠态

672

1.46

规则1

仅横屏旋转

Pura X Max展开态

939

1.41

无命中规则

全方向自由旋转

三折叠G态(以Mate XT为例)

1107

1.42

无命中默认

全方向自由旋转

平板

1600

1.6

无命中默认

全方向自由旋转

当前工具模块对部分高频场景进行了预设配置,开发者调用ResponsiveOrientationConfig.getConfig()并传入PageType的场景枚举后获取响应式方向配置OrientationConfig,再调用OrientationStrategy.resolveResponsive()方法(方法实现参考下方章节)并传入OrientationConfig后,引擎自动完成求值,从而提升选型效率,使用方式如下:

收起
自动换行
深色代码主题
复制
  1. aboutToAppear() {
  2. // ...
  3. const config: ConditionResponsiveValue<OrientationConfig> =
  4. ResponsiveOrientationConfig.getConfig(PageType.LONG_VIDEO);
  5. try {
  6. this.windowObj?.setPreferredOrientation(
  7. OrientationStrategy.resolveResponsive(globalThis.context, config));
  8. } catch (err) {
  9. const error = err as BusinessError;
  10. Logger.error('VideoDetail', `setPreferredOrientation failed, code: ${error.code}, message: ${error.message}`);
  11. }
  12. }

Orientation窗口策略控制

Orientation窗口策略控制将开发者声明的抽象配置映射为系统 window.Orientation 枚举值。开发者只需关注三种模式的配置方式与适用场景,无需记忆底层所有的系统枚举。

方向配置类型

在了解策略映射之前,先看模块定义的三种方向配置类型。它们构成了一个联合类型,通过 mode 字段区分:

收起
自动换行
深色代码主题
复制
  1. // Physical orientation, used for fixed orientation and auto-rotate preferred.
  2. export enum OrientationType {
  3. UNSPECIFIED = 'UNSPECIFIED',
  4. PORTRAIT = 'PORTRAIT',
  5. LANDSCAPE = 'LANDSCAPE',
  6. PORTRAIT_INVERTED = 'PORTRAIT_INVERTED',
  7. LANDSCAPE_INVERTED = 'LANDSCAPE_INVERTED',
  8. }
  9. // Allowed auto-rotation range.
  10. export enum AutoRotateRange {
  11. LANDSCAPE_ONLY = 'LANDSCAPE_ONLY',
  12. PORTRAIT_ONLY = 'PORTRAIT_ONLY',
  13. ALL_ORIENTATIONS = 'ALL_ORIENTATIONS',
  14. }
  15. export interface FixedConfig {
  16. mode: 'fixed';
  17. orientation?: OrientationType;
  18. }
  19. export interface AutoRotateConfig {
  20. mode: 'autoRotate';
  21. range: AutoRotateRange;
  22. preferred?: OrientationType;
  23. }
  24. export interface FollowDesktopConfig {
  25. mode: 'followDesktop';
  26. }
  27. export type OrientationConfig = FixedConfig | AutoRotateConfig | FollowDesktopConfig;

OrientationStrategy:策略映射

OrientationStrategy 是窗口策略控制的核心,提供了一系列静态方法将响应式规则引擎返回的OrientationConfig转换为系统的window.Orientation,通过调用setPreferredOrientation()方法将匹配的窗口旋转策略应用到当前窗口。

OrientationStrategy的三种模式如下:

  1. fixed(固定方向策略

    固定方向策略将窗口锁定在指定方向,不随设备物理旋转而改变。

    展开

    配置 orientation 参数

    映射到 window.Orientation

    行为说明

    UNSPECIFIED

    LOCKED

    锁定当前屏幕方向,不跟随传感器旋转

    PORTRAIT

    PORTRAIT

    锁定竖屏

    LANDSCAPE

    LANDSCAPE

    锁定横屏

    PORTRAIT_INVERTED

    PORTRAIT_INVERTED

    锁定反向竖屏

    LANDSCAPE_INVERTED

    LANDSCAPE_INVERTED

    锁定反向横屏

    适用场景:

    • 需要保持进入页面时方向的场景,使用 fixed() 锁定当前方向。
    • 自定义响应式规则引擎时,直板机场景使用fixed(OrientationType.PORTRAIT)锁定仅竖屏。
  2. autoRotate(自动旋转策略)

    自动旋转策略允许窗口跟随设备物理方向自动旋转,并受控制中心"旋转锁定"开关控制。通过 range 参数限定可旋转的方向范围,通过可选的 preferred 参数指定首次应用时的首选方向。

    展开

    配置

    映射到 window.Orientation

    行为说明

    range: LANDSCAPE_ONLY

    AUTO_ROTATION_LANDSCAPE_RESTRICTED

    仅横屏和反向横屏之间旋转,受开关控制

    range: PORTRAIT_ONLY

    AUTO_ROTATION_PORTRAIT_RESTRICTED

    仅竖屏和反向竖屏之间旋转,受开关控制

    range: ALL_ORIENTATIONS 无首选

    AUTO_ROTATION_UNSPECIFIED

    全方向旋转,可旋转方向由系统根据设备形态判定,受开关控制

    range: ALL_ORIENTATIONS + preferred: PORTRAIT

    USER_ROTATION_PORTRAIT

    调用时临时切换到竖屏,之后全方向跟随传感器旋转,受开关控制

    range: ALL_ORIENTATIONS + preferred: LANDSCAPE

    USER_ROTATION_LANDSCAPE

    调用时临时切换到横屏,之后全方向跟随传感器旋转,受开关控制

    range: ALL_ORIENTATIONS + preferred: PORTRAIT_INVERTED

    USER_ROTATION_PORTRAIT_INVERTED

    调用时临时切换到反向竖屏,之后全方向跟随传感器旋转

    range: ALL_ORIENTATIONS + preferred: LANDSCAPE_INVERTED

    USER_ROTATION_LANDSCAPE_INVERTED

    调用时临时切换到反向横屏,之后全方向跟随传感器旋转

    适用场景:

    • 长视频详情页 / 图库应用:autoRotate(AutoRotationRange.ALL_ORIENTATIONS),全方向自由旋转。
    • 竖屏游戏(也支持反向竖屏):autoRotate(AutoRotationRange.PORTRAIT_ONLY),仅竖向旋转。
  3. followDesktop(跟随桌面策略)

    跟随桌面策略将旋转行为完全委托给系统桌面,应用窗口的方向随桌面方向自动变化。

    展开

    配置

    映射到 window.Orientation

    followDesktop

    FOLLOW_DESKTOP

    适用场景:

    • 应用首页,需要在不同设备上有差异化旋转行为。
    • 社交、购物、阅读等以竖屏为主但在大屏设备上允许旋转的通用页面。
    • 希望一次配置自动适配所有设备旋转策略的场景。

OrientationStrategy的核心静态方法resolveResponsive如下:

收起
自动换行
深色代码主题
复制
  1. export class OrientationStrategy {
  2. // ...
  3. private static resolveUserRotation(preferred?: OrientationType): window.Orientation {
  4. if (!preferred || preferred === OrientationType.UNSPECIFIED) {
  5. return window.Orientation.AUTO_ROTATION_UNSPECIFIED;
  6. }
  7. switch (preferred) {
  8. case OrientationType.PORTRAIT:
  9. return window.Orientation.USER_ROTATION_PORTRAIT;
  10. case OrientationType.LANDSCAPE:
  11. return window.Orientation.USER_ROTATION_LANDSCAPE;
  12. case OrientationType.PORTRAIT_INVERTED:
  13. return window.Orientation.USER_ROTATION_PORTRAIT_INVERTED;
  14. case OrientationType.LANDSCAPE_INVERTED:
  15. return window.Orientation.USER_ROTATION_LANDSCAPE_INVERTED;
  16. default:
  17. return window.Orientation.AUTO_ROTATION_UNSPECIFIED;
  18. }
  19. }
  20. static resolve(config: OrientationConfig): window.Orientation {
  21. switch (config.mode) {
  22. case 'followDesktop':
  23. return OrientationStrategy.followDesktop();
  24. case 'fixed':
  25. return OrientationStrategy.fixed(config.orientation);
  26. case 'autoRotate':
  27. return OrientationStrategy.autoRotate(config.range, config.preferred);
  28. default:
  29. return window.Orientation.AUTO_ROTATION_UNSPECIFIED;
  30. }
  31. }
  32. static resolveResponsive(
  33. context: Object | undefined,
  34. responsiveValue: ResponsiveValue<OrientationConfig>
  35. ): window.Orientation {
  36. const config: OrientationConfig | undefined =
  37. ResponsiveValueResolver.getValue<OrientationConfig>(context, responsiveValue);
  38. if (!config) {
  39. return window.Orientation.UNSPECIFIED;
  40. }
  41. return OrientationStrategy.resolve(config);
  42. }
  43. }

工具模块的使用

前面三章分别介绍了模块的整体架构、响应式规则引擎和窗口策略控制。本章将这三部分串联起来,完整展示从添加依赖、初始化上下文、到页面中应用窗口旋转策略的完整开发流程。

添加依赖

在模块的 oh-package.json5 中添加对 multidevicelibrary 的依赖:

收起
自动换行
深色代码主题
复制
  1. {
  2. "name": "default",
  3. "version": "1.0.0",
  4. "description": "Please describe the basic information.",
  5. "main": "",
  6. "author": "",
  7. "license": "",
  8. "dependencies": {
  9. "base": "file:../../commons/base",
  10. "multidevicelibrary": "file:../../commons/multidevicelibrary",
  11. "home": "file:../../features/home",
  12. "portrait": "file:../../features/portrait",
  13. "landscape": "file:../../features/landscape",
  14. "photos": "file:../../features/photos",
  15. "stock": "file:../../features/stock",
  16. "longvideo": "file:../../features/longvideo",
  17. "shortvideo": "file:../../features/shortvideo",
  18. "hovervideo": "file:../../features/hovervideo"
  19. }
  20. }

定义响应式上下文

在使用工具模块之前,需要先定义设备上下文类,用于承载运行时设备状态。该类将作为规则引擎的条件匹配数据源,ResponsiveContext 的字段也可根据业务需要灵活增减:

收起
自动换行
深色代码主题
复制
  1. // Developer-defined context for scene orientation resolution.
  2. // Maintained by EntryAbility and passed to OrientationController as Object.
  3. // The library's resolver accesses fields generically via Record<string, ConditionValue>.
  4. @Observed
  5. export class ResponsiveContext {
  6. public displayLongEdgeVp: number;
  7. public displayShortEdgeVp: number;
  8. public displayLongShortRatio: number;
  9. public widthBreakpoint: WidthBreakpoint;
  10. public heightBreakpoint: HeightBreakpoint;
  11. constructor(
  12. displayLongEdgeVp: number = 0,
  13. displayShortEdgeVp: number = 0,
  14. displayLongShortRatio: number = 1,
  15. widthBreakpoint: WidthBreakpoint = WidthBreakpoint.WIDTH_SM,
  16. heightBreakpoint: HeightBreakpoint = HeightBreakpoint.HEIGHT_SM
  17. ) {
  18. this.displayLongEdgeVp = displayLongEdgeVp;
  19. this.displayShortEdgeVp = displayShortEdgeVp;
  20. this.displayLongShortRatio = displayLongShortRatio;
  21. this.widthBreakpoint = widthBreakpoint;
  22. this.heightBreakpoint = heightBreakpoint;
  23. }
  24. }

存入 AppStorage 供业务页面使用

初始化流程的核心目标是将ResponsiveContext存入 AppStorage,使所有业务页面能够通过统一的 Key 获取上下文实例并调用ResponsiveValueResolver.getValue()。具体在 EntryAbility.onWindowStageCreate() 中完成以下三步:

收起
自动换行
深色代码主题
复制
  1. export default class EntryAbility extends UIAbility {
  2. // ...
  3. private responsiveContext?: ResponsiveContext;
  4. // ...
  5. onWindowStageCreate(windowStage: window.WindowStage): void {
  6. // ...
  7. windowStage.loadContent('pages/Index', (err) => {
  8. if (err.code) {
  9. Logger.error(`Failed to load the content. Cause: ${JSON.stringify(err)}`);
  10. return;
  11. }
  12. Logger.info('Succeeded in loading the content.');
  13. windowStage.getMainWindow().then((data: window.Window) => {
  14. try {
  15. this.uiContext = data.getUIContext();
  16. } catch (err) {
  17. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`);
  18. }
  19. const abilityRegi: AbilityRegister = new AbilityRegister(data);
  20. AppStorage.setOrCreate('multidevicelibrary.abilities', abilityRegi.registerContext('defaults'));
  21. // Maintain the ResponsiveContext for scene orientation resolution and store it in AppStorage.
  22. this.responsiveContext = new ResponsiveContext();
  23. this.updateResponsiveContext();
  24. AppStorage.setOrCreate('multidevicelibrary.context', this.responsiveContext);
  25. // ...
  26. }).catch((err: BusinessError) => {
  27. Logger.error(`Error occured, error code: ${err.code}, error message: ${err.message}`);
  28. });
  29. });
  30. }
  31. // ...
  32. }

监听设备状态变化和屏幕属性变化

设备状态(屏幕尺寸、折叠态、分辨率)可能在运行时发生变化。为保证规则引擎始终基于最新的设备上下文求值,需要在 EntryAbility 中注册两个关键监听,并在回调中刷新 ResponsiveContext。为了保证拿到的设备状态和屏幕属性是变化后的最终值,建议在屏幕属性变化后也更新一下相关的属性值。

收起
自动换行
深色代码主题
复制
  1. export default class EntryAbility extends UIAbility {
  2. // ...
  3. onWindowStageCreate(windowStage: window.WindowStage): void {
  4. // ...
  5. windowStage.loadContent('pages/Index', (err) => {
  6. if (err.code) {
  7. Logger.error(`Failed to load the content. Cause: ${JSON.stringify(err)}`);
  8. return;
  9. }
  10. Logger.info('Succeeded in loading the content.');
  11. windowStage.getMainWindow().then((data: window.Window) => {
  12. // ...
  13. data.on('windowSizeChange', this.onWindowSizeChange);
  14. this.registerDisplayListener();
  15. }).catch((err: BusinessError) => {
  16. Logger.error(`Error occured, error code: ${err.code}, error message: ${err.message}`);
  17. });
  18. });
  19. }
  20. // ...
  21. private onWindowSizeChange: (windowSize: window.Size) => void = (windowSize: window.Size) => {
  22. if (!this.uiContext) {
  23. Logger.error('[EntryAbility] uiContext is undefined in onWindowSizeChange.');
  24. return;
  25. }
  26. let widthBp: WidthBreakpoint = this.uiContext.getWindowWidthBreakpoint();
  27. AppStorage.setOrCreate(CommonConstants.WIDTH_BREAK_POINT, widthBp);
  28. let heightBp: HeightBreakpoint = this.uiContext.getWindowHeightBreakpoint();
  29. AppStorage.setOrCreate(CommonConstants.HEIGHT_BREAK_POINT, heightBp);
  30. let windowSizeVp: window.Size = {
  31. width: this.uiContext.px2vp(windowSize.width),
  32. height: this.uiContext.px2vp(windowSize.height)
  33. };
  34. AppStorage.setOrCreate(CommonConstants.WINDOW_SIZE_VP, windowSizeVp);
  35. this.updateResponsiveContext();
  36. this.scene?.recompute();
  37. }
  38. // ...
  39. private registerDisplayListener(): void {
  40. try {
  41. display.on('change', () => {
  42. this.updateResponsiveContext();
  43. this.scene?.recompute();
  44. });
  45. } catch (e) {
  46. Logger.error(`[EntryAbility] display.on('change') unavailable: ${JSON.stringify(e)}`);
  47. }
  48. }
  49. }

updateResponsiveContext() 实现从 defaultDisplay 实时计算屏幕参数:

收起
自动换行
深色代码主题
复制
  1. private updateResponsiveContext(): void {
  2. if (!this.responsiveContext) {
  3. return;
  4. }
  5. try {
  6. const defaultDisplay: display.Display = display.getDefaultDisplaySync();
  7. const longEdgePx: number =
  8. defaultDisplay.width >= defaultDisplay.height ? defaultDisplay.width : defaultDisplay.height;
  9. const shortEdgePx: number =
  10. defaultDisplay.width >= defaultDisplay.height ? defaultDisplay.height : defaultDisplay.width;
  11. const longShortRatio: number = shortEdgePx > 0 ? longEdgePx / shortEdgePx : 1;
  12. const densityPixels: number = defaultDisplay.densityPixels;
  13. this.responsiveContext.displayLongEdgeVp = longEdgePx / densityPixels;
  14. this.responsiveContext.displayShortEdgeVp = shortEdgePx / densityPixels;
  15. this.responsiveContext.displayLongShortRatio = longShortRatio;
  16. if (this.uiContext) {
  17. this.responsiveContext.widthBreakpoint = this.uiContext.getWindowWidthBreakpoint();
  18. this.responsiveContext.heightBreakpoint = this.uiContext.getWindowHeightBreakpoint();
  19. }
  20. AppStorage.setOrCreate(CommonConstants.DISPLAY_SHORT_EDGE_VP, this.responsiveContext.displayShortEdgeVp);
  21. AppStorage.setOrCreate(CommonConstants.DISPLAY_LONG_SHORT_RATIO, this.responsiveContext.displayLongShortRatio);
  22. } catch (error) {
  23. Logger.error(`[EntryAbility] updateResponsiveContext failed: ${JSON.stringify(error)}`);
  24. }
  25. }

业务页面标准用法

业务页面使用工具模块遵循统一的标准模式:

收起
自动换行
深色代码主题
复制
  1. aboutToAppear() {
  2. // ...
  3. const config: ConditionResponsiveValue<OrientationConfig> =
  4. ResponsiveOrientationConfig.getConfig(PageType.LONG_VIDEO);
  5. try {
  6. this.windowObj?.setPreferredOrientation(
  7. OrientationStrategy.resolveResponsive(globalThis.context, config));
  8. } catch (err) {
  9. const error = err as BusinessError;
  10. Logger.error('VideoDetail', `setPreferredOrientation failed, code: ${error.code}, message: ${error.message}`);
  11. }
  12. }

方案对比

页面数量少、适配设备单一、窗口策略固定不变的场景,推荐直接使用系统原生 API 开发;页面数量多、需适配多设备、窗口策略支持动态调整适配的场景,优先采用多设备工具模块,降低跨设备适配成本。原生API与多设备工具模块的对比表格如下,开发者可根据应用的场景选择合适的方案:

展开

对比维度

原生 API

多设备工具模块

使用方式

直接调用 window.setPreferredOrientation(枚举值)

声明式配置,调用ResponsiveValueResolver.getValue()

枚举选择

需从所有系统枚举中手动筛选

通过 PageType 预设或自定义 OrientationConfig,无需记忆枚举

多设备适配

手写 if-else 判断设备尺寸/宽高比/折叠态

响应式规则引擎自动匹配

代码复用

每个页面重复编写 setPreferredOrientation 逻辑

预设配置一次定义多处复用,自定义配置可跨页面共享

维护成本

策略调整需逐一修改各页面代码

集中修改配置常量即可全局生效

典型场景

以窗口旋转策略实现的五个高频场景为载体,通过窗口级配置实现多设备的窗口方向变化。

应用首页案例

应用首页通常支持横屏与竖屏显示。但是在类直板机上横屏的用户体验不好,所以直板机始终竖屏显示;在非类直板机(如平板、双折叠展开态、三折叠M/G态)支持竖屏与横屏展示。体验标准如下:

展开

体验标准

仅竖屏

支持自由旋转,受开关控制

支持设备形态

直板机、双折叠折叠态、三折叠F态

双折叠展开态、三折叠M/G态、平板

效果图

对于市场上大多数应用的首页用户行为及体验,推荐使用FOLLOW_DESKTOP策略,以满足应用在不同设备上的窗口旋转策略需求。同时,FOLLOW_DESKTOP支持在同设备的折叠状态切换时,窗口旋转策略自动更新。例如,三折叠F态仅支持竖屏,切换至三折叠M态时,自动变为自由旋转,并受控制中心旋转开关的控制。

首先,需对应用启动时的旋转策略进行设置,具体可参考配置module.json5文件中的orientation字段。以实现多开发为例,为满足直板机和平板设备的不同策略,设置为follow_desktop,此字段主要解决不同设备上默认旋转策略差异的问题。

在具体需要实现横竖屏切换的页面上,采用window窗口提供的设置窗口方向的能力,通过setPreferredOrientation()将窗口显示的方向修改为横屏或竖屏的状态。

具体如下:通过getContext获取对应的UIAbilityContext,并通过context获取对应的windowStage实例,然后通过windowStage.getMainWindowSync同步方法拿到对应的窗口实例win,然后调用setPreferredOrientation()方法设置窗口方向。

收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct Home {
  3. windowObj: window.Window | undefined = undefined;
  4. // ...
  5. aboutToAppear(): void {
  6. this.tabBarsInfo.setTabList(TabBarsInfo);
  7. try {
  8. this.windowObj = (this.getUIContext().getHostContext() as common.UIAbilityContext).windowStage.getMainWindowSync()
  9. } catch (err) {
  10. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  11. }
  12. // Use the WindowOrientationHelper tool to directly obtain the rotation strategy enumeration through chained calls.
  13. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.presets.FOLLOW_DESKTOP)
  14. .catch((err: BusinessError) => {
  15. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  16. });
  17. // ...
  18. }
  19. // ...
  20. build() {
  21. // ...
  22. }
  23. }

游戏应用案例

游戏应用通常仅支持竖屏或横屏显示。例如消除类游戏仅支持竖屏显示;MOBA类游戏仅支持横屏显示。体验标准如下:

展开

体验标准

竖屏游戏仅支持竖屏

横屏游戏支持横屏旋转,受开关控制

支持设备形态

直板机、双折叠折叠态、三折叠F/M/G态、平板

直板机、双折叠折叠态、三折叠F/M/G态、平板

效果图

对于游戏类应用,无论横竖屏游戏,均为固定方式或仅支持一个方向(例竖屏及反向竖屏)的旋转切换,此类应用均不需要在应用内进行开关控制,所以只需要在module.json5配置文件中进行相应的配置即可。一般有以下几种情况:

默认竖屏方向

如果该应用默认为仅竖屏状态,那么则需要在module.json5中的“orientation”字段进行配置为portrait。如果希望游戏同时支持反向竖屏显示,推荐设置为auto_rotation_portrait_restricted。

默认横屏方向

推荐横屏游戏使用auto_rotation_landscape_restricted策略,所有设备上初始窗口方向为横屏或反向横屏,支持横屏旋转,且受控制中心的旋转开关控制。同时,在同一设备切换折叠状态时,保持横屏或反向横屏显示。

图库应用案例

图库应用通常在所有设备上支持竖屏或横屏显示。但是在直板机上反向竖屏的用户体验不好,所以直板机只能旋转至竖屏、横屏、反向横屏三个方向,受开关控制;在非类直板机(如平板、双折叠展开态、三折叠M/G态)保持当前窗口方向,支持自由旋转,且受开关控制。体验标准如下:

展开

体验标准

三向旋转(竖屏/横屏/反向横屏),受开关控制

自由旋转,受开关控制

支持设备形态

直板机、双折叠折叠态、三折叠F态

双折叠展开态、三折叠M/G态、平板

效果图

推荐图库应用案例在module.json5中的“orientation”字段或页面中通过setPreferredOrientation()使用AUTO_ROTATION_UNSPECIFIED策略。

个股详情页 & 股票K线图页案例

个股详情页通常支持横屏与竖屏显示。但是在类直板机上横屏的用户体验不好,所以直板机始终竖屏显示,不支持旋转;在非类直板机(如平板、双折叠展开态、三折叠M/G态)保持当前窗口方向,支持自由旋转,且受控制中心的旋转开关控制。体验标准如下:

展开

体验标准

仅竖屏

支持自由旋转,受开关控制

支持设备形态

直板机、双折叠折叠态、三折叠F态

双折叠展开态、三折叠M/G态、平板

效果图

在个股详情页面上,在aboutToAppear生命周期中采用window窗口提供的设置窗口方向的能力,通过setPreferredOrientation()设置窗口旋转策略为FOLLOW_DESKTOP,在aboutToDisappear中恢复上级页面的窗口旋转策略。

收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct StockDetail {
  3. windowObj: window.Window | undefined = undefined;
  4. // ...
  5. aboutToAppear(): void {
  6. try {
  7. this.windowObj = (this.getUIContext().getHostContext() as common.UIAbilityContext).windowStage.getMainWindowSync()
  8. } catch (err) {
  9. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  10. }
  11. this.windowObj?.setPreferredOrientation(window.Orientation.FOLLOW_DESKTOP)
  12. .catch((err: BusinessError) => {
  13. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  14. });
  15. }
  16. aboutToDisappear() {
  17. this.windowObj?.setPreferredOrientation(window.Orientation.UNSPECIFIED)
  18. .catch((err: BusinessError) => {
  19. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  20. });
  21. }
  22. build() {
  23. // ...
  24. }
  25. }

股票K线图页通常仅横屏显示,支持横屏旋转,且受控制中心的旋转开关控制。体验标准如下:

展开

体验标准

横屏旋转,受开关控制

支持设备形态

直板机、双折叠折叠态、三折叠F/M/G态、平板

效果图

示例代码

在K线图页的aboutToAppear()和aboutToDisappear()生命周期中调用window.setPreferredOrientation(),设置K线图页显示时窗口旋转策略为AUTO_ROTATION_LANDSCAPE_RESTRICTED,K线图页返回时恢复窗口旋转策略为FOLLOW_DESKTOP。

收起
自动换行
深色代码主题
复制
  1. aboutToAppear(): void {
  2. try {
  3. this.windowObj = (this.getUIContext().getHostContext() as common.UIAbilityContext).windowStage.getMainWindowSync()
  4. } catch (err) {
  5. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  6. }
  7. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.autoRotate("LANDSCAPE_ONLY"))
  8. .catch((err: BusinessError) => {
  9. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  10. });
  11. }
  12. aboutToDisappear(): void {
  13. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.followDesktop())
  14. .catch((err: BusinessError) => {
  15. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  16. });
  17. }

视频详情页 & 全屏播放页案例

视频详情页通常支持横屏与竖屏显示。但是在直板机上反向竖屏的用户体验不好,所以直板机只能旋转至竖屏、横屏、反向横屏三个方向,且横屏时自动显示全屏播放页,竖屏时自动显示视频详情页;在非类直板机(如平板、双折叠展开态、三折叠M/G态)保持当前窗口方向,支持自由旋转,且受开关控制。体验标准如下:

展开

体验标准

三方向旋转(竖屏/横屏/反向横屏),受开关控制

自由旋转,受开关控制

支持设备形态

直板机、双折叠折叠态、三折叠F态

双折叠展开态、三折叠M/G态、平板

效果图

全屏播放页仅横屏显示,支持横屏旋转,并受控制中心旋转开关控制。在类直板机上,用户点击全屏按钮进入全屏播放页时,仅能旋转至横屏和反向横屏两个方向;若开启旋转开关,从横屏或反向横屏进入全屏播放页时,支持旋转至竖屏、横屏、反向横屏三个方向,并在旋转至竖屏时切换至视频详情页。在双折叠展开态(接近正方形)下,可自由旋转至四个方向,且受开关控制。体验标准如下:

展开

体验标准

横屏旋转,受开关控制

自由旋转,受开关控制

横屏旋转,受开关控制

支持设备形态

类直板机

双折叠展开态、三折叠M

三折叠G态、平板

效果图

对于视频类应用,在具体需要实现横竖屏切换的页面上,例如视频播放页面支持横屏,但是首页的内容是支持仅竖屏的,那么就需要在进入对应的页面时,采用window窗口提供的设置窗口方向的能力,通过setPreferredOrientation将窗口显示的方向修改为横屏、竖屏的状态。应用的默认旋转策略和如何通过setPreferredOrientation方法设置窗口方向可参考首页案例代码。

以视频播放为例,不仅可以通过系统控制横竖屏,也支持用户在系统锁定旋转的情况下,手动设置横屏状态,即需要满足以下条件:

  1. 应用跟随传感器旋转。
  2. 受到控制中心的旋转锁定按钮控制。
  3. 支持用户在应用页面中临时调用设置方向的能力,例如点击全屏按钮进行切换。

要实现上述效果,可通过窗口的 orientation 属性设置枚举类型来实现旋转功能。为支持临时方向设置,当用户点击全屏按钮时需手动触发横竖屏切换。若旋转锁定已关闭,窗口应跟随传感器旋转。因此推荐视频详情页采用 AUTO_ROTATION_UNSPECIFIED 策略,三折叠展开态及平板全屏播放页采用 AUTO_ROTATION_LANDSCAPE_RESTRICTED 策略,以实现临时调用旋转并支持后续传感器跟随。

在视频详情页中,设置窗口方向为AUTO_ROTATION_UNSPECIFIED:

收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct VideoDetail {
  3. windowObj: window.Window | undefined = undefined;
  4. // ...
  5. aboutToAppear() {
  6. try {
  7. this.windowObj = (this.getUIContext().getHostContext() as common.UIAbilityContext).windowStage.getMainWindowSync()
  8. } catch (err) {
  9. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  10. }
  11. // ...
  12. // Dynamically select an appropriate rotation strategy through a selector.
  13. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.select({
  14. mode: 'autoRotate',
  15. range: 'ALL_ORIENTATIONS',
  16. preferred: 'UNSPECIFIED'
  17. }))
  18. .catch((err: BusinessError) => {
  19. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  20. });
  21. }
  22. // ...
  23. build() {
  24. // ...
  25. }
  26. }

在aboutToAppear()生命周期中添加窗口尺寸变化的监听方法on('windowSizeChange', callback),当窗口尺寸变化时,通过窗口断点判断当前设备的横竖屏状态,切换全屏状态或更新窗口旋转策略;

监听视频详情页的全屏播放状态,在用户点击全屏播放按钮时,在回调方法onFullScreenChange()中判断当前设备的横竖屏状态,更新窗口旋转策略,并在用户返回视频详情页时恢复视频详情页的窗口旋转策略。

收起
自动换行
深色代码主题
复制
  1. @Component
  2. export struct VideoDetail {
  3. windowObj: window.Window | undefined = undefined;
  4. @StorageLink('isFullScreen') @Watch('onFullScreenChange') isFullScreen: boolean = false;
  5. // ...
  6. aboutToAppear() {
  7. // ...
  8. this.windowObj?.on('windowSizeChange', this.onWindowSizeChange);
  9. // ...
  10. }
  11. onFullScreenChange(): void {
  12. if (this.isFullScreen) {
  13. if (this.isClick) {
  14. if (this.widthBp === WidthBreakpoint.WIDTH_SM || this.widthBp === WidthBreakpoint.WIDTH_LG ||
  15. this.heightBp === HeightBreakpoint.HEIGHT_LG) {
  16. // Dynamically select an appropriate rotation strategy through a selector.
  17. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.select({
  18. mode: 'autoRotate',
  19. range: 'LANDSCAPE_ONLY'
  20. }))
  21. .catch((err: BusinessError) => {
  22. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  23. });
  24. }
  25. }
  26. } else {
  27. // Dynamically select an appropriate rotation strategy through a selector.
  28. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.select({
  29. mode: 'autoRotate',
  30. range: 'ALL_ORIENTATIONS',
  31. preferred: 'UNSPECIFIED'
  32. }))
  33. .catch((err: BusinessError) => {
  34. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  35. });
  36. }
  37. }
  38. private onWindowSizeChange: (windowSize: window.Size) => void = () => {
  39. if (this.isClick) {
  40. return;
  41. }
  42. if (this.widthBp === WidthBreakpoint.WIDTH_SM) {
  43. this.isFullScreen = false
  44. // Dynamically select an appropriate rotation strategy through a selector.
  45. this.windowObj?.setPreferredOrientation(WindowOrientationHelper.select({
  46. mode: 'autoRotate',
  47. range: 'ALL_ORIENTATIONS',
  48. preferred: 'UNSPECIFIED'
  49. }))
  50. .catch((err: BusinessError) => {
  51. Logger.error(`Invoke set preferred orientation failed, code is ${err.code}, message is ${err.message}`)
  52. });
  53. }
  54. if (this.widthBp === WidthBreakpoint.WIDTH_MD && this.heightBp === HeightBreakpoint.HEIGHT_SM) {
  55. this.isFullScreen = true;
  56. }
  57. };
  58. // ...
  59. build() {
  60. // ...
  61. }
  62. }

常见问题

display与window的区别

  • 屏幕(@ohos.display (屏幕属性))指物理或逻辑的显示设备,是显示内容的整体区域。例如:
    • 物理屏幕:显示器、手机屏幕、投影仪等硬件设备。
    • 逻辑屏幕:操作系统虚拟的多屏幕环境(如扩展桌面)。
  • 窗口(window)是运行在屏幕上的一个可交互的图形界面区域,属于软件层面。例如:
    • 应用程序窗口(如浏览器、文件夹窗口)。
    • 对话框、工具栏等子窗口。

display.rotation的定义

Display的属性rotation表示显示设备的屏幕顺时针旋转角度。使用场景:适用于和硬件设备角度强关联的场景,如相机预览角度补偿。

rotation的取值有4种,分别对应下图所示的4个方向(以直板机为例)。如果需要更精准的角度信息,则需要配合设备sensor获取。

展开

含义

0

显示设备屏幕顺时针旋转为0°

1

显示设备屏幕顺时针旋转为90°

2

显示设备屏幕顺时针旋转为180°

3

显示设备屏幕顺时针旋转为270°。

display.Orientation与window.Orientation的区别

  • display的Orientation表示屏幕当前横竖显示方向,屏幕的横竖显示方向只能获取,不能设置,客观体现了当前屏幕的显示状态。
  • window.Orientation表示窗口旋转策略,窗口旋转策略可以由开发者设置,系统会根据开发者的预设策略进行相应的旋转。

对于开发者而言,控制应用的显示方向应该通过设置window.Orientation实现,详情请参考了解窗口旋转策略

display.Orientation与display.rotation的关系

display.Orientation 为屏幕当前的朝向状态,display.rotation 为屏幕相对自然方向的物理旋转角度。display.Orientation 为和 display.rotation 均为只读属性,且用于描述屏幕当前旋转状态,但二者定义逻辑不同,在各类设备形态下不存在固定对应关系,开发过程中不可相互替代。若混用接口,在折叠屏等多形态设备适配场景中极易引发兼容性问题。以三折叠设备为例:当 display.rotation 取值为 0° 时,display.Orientation 既可能为竖屏状态,也可能为反向横屏状态。

window.getLastWindow的方式获取窗口出现延迟

  1. 由于getLastWindow底层原因,需要经过查找获取实例,一定程度上会有性能损耗,可能会出现已经发生横屏或者竖屏切换的情况下,状态栏还没切换的情况。
  2. 使用windowStage.getMainWindowSync的同步方法获取窗口实例。
收起
自动换行
深色代码主题
复制
  1. onWindowStageCreate(windowStage: window.WindowStage): void {
  2. // ...
  3. try {
  4. this.windowUtil = new WindowUtil(windowStage.getMainWindowSync());
  5. } catch (error) {
  6. let err = error as BusinessError;
  7. hilog.error(0x0000, 'TestLog', `Failed to get main window. Code: ${err.code}, message: ${err.message}`);
  8. }
  9. AppStorage.setOrCreate('windowUtil', this.windowUtil);
  10. windowStage.loadContent('pages/Index', (err) => {
  11. // ...
  12. this.windowUtil!.setUIContext();
  13. this.windowUtil!.setImmersiveType(ImmersiveType.IMMERSIVE);
  14. this.windowUtil!.updateWindowInfo();
  15. });
  16. }

竖屏时进入任务中心,进入横屏的应用,在onPageShow时获取的display信息不符合预期

目前display接口规则还不够清晰,建议使用window的getWindowProperties()接口处理。

如何获取屏幕的宽度、高度、分辨率和横竖屏等信息

引入屏幕属性模块,可以通过调用display.getDefaultDisplaySync()方法获取display对象后,从而获取到屏幕的宽度、高度、分辨率和横竖屏等信息。

如何通过日志查看应用当前设置的窗口旋转策略

在多模块多团队共同开发过程中,页面窗口旋转策略的设置可能导致预期之外的窗口旋转问题。开发者需通过查询日志的方式自行排查是否设定了非预期的窗口旋转策略。查询方法如下:

  1. 连接并推包到当前设备。
  2. 打开Log页面,依次在筛选框中选择“当前的连接设备”、“No filters”、“当前的调试应用”、“Debug”或“Info”,最后在关键字栏填写“SetRequestedOrientation”。
  3. 操作问题页面后,在日志中查看系统日志,找到应用包名一行的日志,lastReqOrientation表示应用最后的窗口旋转策略,target表示目标窗口旋转策略,后面的数字可参考下方对照表。

日志中查看窗口方向对照表

展开

window.orientation

target

UNSPECIFIED

0

PORTRAIT

1

LANDSCAPE

2

PORTRAIT_INVERTED

3

LANDSCAPE_INVERTED

4

AUTO_ROTATION

5

AUTO_ROTATION_PORTRAIT

6

AUTO_ROTATION_LANDSCAPE

7

AUTO_ROTATION_RESTRICTED

8

AUTO_ROTATION_PORTRAIT_RESTRICTED

9

AUTO_ROTATION_LANDSCAPE_RESTRICTED

10

LOCKED

11

FOLLOW_RECENT

12

AUTO_ROTATION_UNSPECIFIED

13

USER_ROTATION_PORTRAIT

14

USER_ROTATION_LANDSCAPE

15

USER_ROTATION_PORTRAIT_INVERTED

16

USER_ROTATION_LANDSCAPE_INVERTED

17

FOLLOW_DESKTOP

18

示例代码

在 最佳实践 多设备开发 中进行搜索
请输入您想要搜索的关键词