开头
我最早接触HarmonyOS ArkUI窗口旋转,是被一个蛮尴尬的场景逼着去研究的:应用只是从竖屏转了一下横屏,结果页面直接“重排”得面目全非,底部按钮跑到了屏幕中间,列表的滚动位置也丢了,用户切回来之后直呼“手机是不是坏了”。后来认真查了一圈才发现,窗口旋转这个功能在ArkUI里不只是“屏幕转一下”那么简单,它牵扯到配置文件里的方向声明、窗口事件回调、布局的重新计算,还有应用状态怎么在旋转前后保住。当时网上能找到的资料大多是零散的接口截图,没有一篇能让我从“完全不懂”到“能落地跑通”的文章。这篇文章就按我自己的学习路径来写:先把旋转背后的逻辑讲清楚,再给你一套可以直接抄的配置和代码,最后把踩过的坑和调试心得都倒出来。适合刚接触ArkUI的开发者,也适合那些已经写了几个页面、但一直没搞明白旋转到底该怎么处理的开发者。
1. 旋转不是“转一下屏幕”那么简单:先理解ArkUI窗口旋转的运作层级
很多人一开始都会把“窗口旋转”理解成“让屏幕方向变一下”,其实真实情况要复杂一截。在HarmonyOS这套体系里,旋转至少涉及三个层级:系统设置、应用配置、页面布局。
系统设置决定设备允不允许旋转,比如某些折叠屏和平板会有“自动旋转”开关;应用配置决定你的应用在什么情况下响应旋转,是在module.json5里声明方向,还是让应用跟随传感器;页面布局则决定旋转之后UI怎么重新排布。这三个层级只要有一层没配合好,旋转起来就会出现我们常说的“布局崩了”“状态丢了”“动画卡顿”这类问题。
ArkUI和传统的移动端开发有个很不一样的地方:它更多是声明式的,界面长什么样由你描述的UI状态决定,而不是一帧一帧手工去改。窗口旋转本质上是窗口尺寸发生了变化——竖屏变成横屏,宽高比例交换——而ArkUI拿到新的窗口尺寸后,会重新触发布局计算、刷新UI。所以你要做的并不是“在旋转发生的那一刻手动搬控件”,而是把布局规则写得足够健壮,让它在任意宽高比下都能合理排布。
这里还要提一下Stage模型和Ability的关系。在HarmonyOS里,窗口是由UIAbility来承载的,窗口的方向属性就在UIAbility的配置里。也就是说,你希望某个页面横屏、某个页面竖屏,一般要拆成不同的Ability去分别声明,而不是在同一个页面内部靠代码生硬切换。我在前期就吃过这个亏:想在同一个Ability里针对不同子页面改方向,结果发现窗口方向一旦被设置,整条Ability链路上的页面都会受影响。
理解了这三个层级,后面所有配置和代码才有落脚点。
1.1 ArkUI窗口旋转的设计边界
ArkUI在窗口旋转这件事上,能力边界大致是这几条:读取当前窗口方向、监听窗口方向或尺寸变化、主动设置窗口方向、控制是否跟随系统旋转。日常项目里用到的基本就是这些。
但你要留意,ArkUI的“窗口旋转”和“页面内某个组件旋转”是两码事。组件旋转用rotation属性,是纯视觉上的变化;窗口旋转则是整个应用界面的坐标系和布局参照都变了。如果混在一起想,容易把实现方案搞乱。
还有一点,旋转触发时系统会有一定的动画过渡过程,窗口尺寸并不是瞬间切换的。如果你的代码在旋转回调里做了比较重的操作,比如重新拉起网络请求、重新构建大列表,用户会明显感觉到卡顿或者白屏闪烁。所以旋转回调里做“轻量更新”和“状态保存”是基本原则。
1.2 旋转相关的核心概念:方向、尺寸、密度
要聊窗口旋转,有三个概念必须拎出来对齐一下:方向(orientation)、尺寸(size)、密度(density)。
方向就是屏幕当前的朝向,通常有竖屏、横屏、反向竖屏、反向横屏这些状态。尺寸比较好理解,旋转会导致宽高互换,但要注意,在ArkUI里尺寸单位是vp(虚拟像素),它和物理像素之间隔着密度换算。密度这个变量在某些场景会坑你,比如不同设备的默认密度不一样,同样的宽高布局在不同设备上旋转后的表现也会有差异。
实操中我一般建议把布局尽可能用相对单位和弹性布局去写,避免写死宽高。这样做的好处是:旋转发生时,窗口尺寸变了,布局能自动适应,不需要你在代码里针对每个尺寸都写一套分支判断。
2. 从零跑通最简窗口旋转:方向配置与两种主流控制方式
最基础的窗口旋转实现,其实不需要写一行JavaScript/ArkTS代码。HarmonyOS的Stage模型允许你直接在module.json5里声明Ability的方向属性。先看一个最简配置。
2.1 在module.json5中声明窗口方向
项目里找到entry/src/main/module.json5,在abilities数组对应的Ability节点下添加orientation字段:
{ "module": { "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "orientation": "autoRotation", "exported": true, "skills": [ { "entities": ["entity.system.home"], "actions": ["action.system.home"] } ] } ] } }orientation支持的值在文档里能查到,我实际用下来比较常用的有这么几个:
| 取值 | 含义 | 适用场景 |
|---|---|---|
| portrait | 锁定竖屏 | 阅读类、列表类应用 |
| landscape | 锁定横屏 | 视频、游戏类应用 |
| sensorLandscape | 跟随传感器,但只在横屏方向间切换 | 横屏为主的工具类应用 |
| sensorPortrait | 跟随传感器,但只在竖屏方向间切换 | 竖屏为主的工具类应用 |
| autoRotation | 跟随系统自动旋转 | 大多数普通应用 |
需要注意,autoRotation和unspecified并不完全一样。unspecified更多是“交给系统决定”,而autoRotation会跟随用户的系统旋转开关。如果你希望应用默认跟随用户设置,就用autoRotation。
2.2 代码动态控制窗口方向
声明式配置适合“整个Ability统一方向”。但如果你的应用需要在运行过程中动态切换方向,比如用户点了按钮进入全屏播放视频,那就得用代码控制。
在UIAbility的onWindowStageCreate阶段拿窗口对象,然后设置首选方向:
import { window } from '@kit.ArkUI'; export default class EntryAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage): void { const mainWindow = windowStage.getMainWindowSync(); // 让窗口跟随传感器自动旋转 mainWindow.setPreferredOrientation('autoRotation', (err) => { if (err) { console.error(`setPreferredOrientation failed, code is ${err.code}`); } }); } }这里有个细节:setPreferredOrientation是异步接口,第二个参数是回调。实际产品里如果方向切换后有埋点需求,可以在回调里做统计。另外,这个设置是“偏好”性质的,极端情况下系统可能会拒绝,比如某些特殊窗口模式下。
2.3 动态锁定方向的策略
视频播放类应用常用的一个交互是:进去时横屏,退出时恢复原来的方向。我处理这种需求时,习惯先把当前方向存起来,再设置目标方向:
import { window } from '@kit.ArkUI'; class OrientationManager { private currentOrientation: string = 'portrait'; saveOrientation(windowObj: window.Window) { this.currentOrientation = windowObj.getPreferredOrientation(); } lockLandscape(windowObj: window.Window) { // 横屏锁定,两个方向都支持但以横屏为主 this.saveOrientation(windowObj); windowObj.setPreferredOrientation('sensorLandscape'); } restoreOrientation(windowObj: window.Window) { windowObj.setPreferredOrientation(this.currentOrientation); } }这样处理的好处是:用户之前如果是竖屏锁定状态,退出全屏后能恢复竖屏;如果之前是自动旋转,退出后也能恢复自动旋转,不会出现“退出全屏后发现应用再也不转了”的问题。
3. 旋转发生那一刻应用在干什么:生命周期回调与窗口事件监听
配置搞定之后,才是真正考验工程能力的地方。前面说过,窗口旋转会导致窗口尺寸变化、触发页面重新布局。但很多业务场景需要在旋转的那一刻做点事:保存表单数据、暂停视频、更新图表、收起键盘等。要做到这些,你得搞清楚应用在旋转时经历了什么。
3.1 ConfigurationUpdate回调是首选入口
在Stage模型中,UIAbility提供了onConfigurationUpdate回调。当系统配置发生变化,包括方向变化、语言切换、深浅色切换时,系统都会触发这个回调。
import { AbilityConstant, ConfigurationConstant, UIAbility } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit'; export default class EntryAbility extends UIAbility { onConfigurationUpdate(newConfig: common.Configuration): void { if (newConfig.direction === ConfigurationConstant.Direction.DIRECTION_HORIZONTAL) { console.info('窗口方向切换到横屏'); // 这里可以通知页面组件做布局优化,比如切换列表样式 } else if (newConfig.direction === ConfigurationConstant.Direction.DIRECTION_VERTICAL) { console.info('窗口方向切换到竖屏'); // 做一些竖屏特有的UI调整 } } }需要提醒一下:onConfigurationUpdate回调只是“通知”你这个事发生了,它不保证窗口尺寸已经切换完成。如果你要在回调里读取窗口的最新宽高,最好在下一帧或者等窗口事件到了再取。
3.2 监听窗口的尺寸与方向变化事件
比onConfigurationUpdate更精确的,是窗口对象自带的尺寸变化监听。在ArkUI里,Window对象支持windowSizeChange事件。竖屏转横屏本质上是窗口宽度和高度交换,所以监听windowSizeChange也能感知旋转,而且能拿到实时的宽高数值。
import { window } from '@kit.ArkUI'; function listenWindowSizeChange(windowObj: window.Window) { windowObj.on('windowSizeChange', (newSize: window.Size) => { console.info(`新的窗口尺寸:width=${newSize.width}, height=${newSize.height}`); // 判断宽高比,宽大于高视为横屏 const isLandscape = newSize.width > newSize.height; }); }不过我自己用下来,宽度大于高度这个判断方式在折叠屏上会有一点意外,因为折叠屏的展开态可能会有一个接近方形的中间状态。如果你的应用要兼容折叠屏,建议不要只用“宽大于高”这单一条件判断横竖屏,还要结合密度和设备的折叠状态来看。
3.3 应用退到后台再转屏的坑
还有一种情况容易被忽略:用户先按Home键把应用退到后台,然后在系统设置里改了方向,再切回来。这时候应用可能不会走完整的旋转回调链路,但UI实际上已经不一样了。所以在onForeground恢复的时候,最好重新读取一次当前窗口方向,并主动刷新需要方向信息的UI部分。
我遇到过的问题是:弹窗类的浮层在后台被旋转后,回到前台时还保留着旧的布局参数,结果位置歪了。后来在onForeground里重新计算浮层位置才治好。这个经验可能冷门,但遇到类似浮层、弹窗问题的同学应该深有体会。
4. 横竖屏布局适配的实用策略:别只想着写两套UI
旋转的代码逻辑其实简单,真正让开发者头疼的是布局适配。很多新手的思路是“横屏一套、竖屏一套”,写两个界面,然后根据方向切换。这在简单页面下能跑,但页面一多、组件一复杂,两套UI的维护成本会迅速膨胀。
ArkUI的思路更倾向于“一套布局,自适应变化”。下面几个策略是我在实际项目里验证过比较有效的。
4.1 优先使用弹性布局与栅格布局
ArkUI提供了一系列布局容器,Row、Column、Flex、GridRow等。你要做的就是把布局写得“能伸缩”,而不是写死。比如一个列表页,竖屏时占满整宽,横屏时通过GridRow把内容区限制在中间固定宽度,就是一种很自然的适配。
@Entry @Component struct GridRowDemo { build() { GridRow({ columns: { sm: 4, md: 8, lg: 12 }, gutter: { x: 8, y: 8 } }) { GridCol({ span: { sm: 4, md: 4, lg: 6 } }) { Text('主内容区域') .width('100%') .height(200) .backgroundColor(0xeeeeee) } GridCol({ span: { sm: 4, md: 4, lg: 6 } }) { Text('侧边信息区域') .width('100%') .height(200) .backgroundColor(0xcccccc) } } .width('100%') } }上面这个GridRow示例,小屏(sm)下一行4列、两个子项各占4列,相当于堆叠;大屏(lg)下两个子项各占6列,正好并排。这样旋转前后你根本不用写方向判断,系统会根据窗口尺寸自动选列数。
4.2 资源限定词适配横屏资源
如果某些资源确实需要区分横竖屏——比如启动图、宣传图、背景图——HarmonyOS的资源限定词可以帮你按方向加载对应资源。在resources/base/element、resources/base/media同级目录下,可以创建带方向限定词的资源目录,比如:
resources/base/media/:默认资源resources/landscape/media/:横屏资源resources/portrait/media/:竖屏资源
这样,同一资源名在不同方向下会自动加载不同的文件。我用这个特性处理过横屏启动页的适配,比在代码里判断方向再切图干净得多。
4.3 善用属性变化绑定,减少方向分支代码
ArkUI的LayoutWeight、aspectRatio等属性也能显著减少方向分支。比如视频播放器的容器,我希望横屏时铺满全屏、竖屏时保持16:9,用aspectRatio就可以很优雅地实现:
VideoPlayerContainer() { // 内容实现 } .aspectRatio(this.isLandscape ? 1.0 : 16 / 9) .width('100%')这里isLandscape可以取自窗口尺寸或配置更新回调。比起写两套布局,这种“改几个属性值”的做法维护起来轻松太多。
5. 避坑笔记:方向锁定、应用内旋转与窗口尺寸的纠缠
做窗口旋转期间,我踩过的坑能凑一箩筐。挑几个典型的说一说,每一个都是我花了不少时间才定位到原因的。
5.1 方向锁定后,应用内页面无法单独旋转
这个坑出现得很隐蔽:某个页面不需要旋转,但另一个页面需要。我一开始直接在需要旋转的页面里调用setPreferredOrientation('landscape'),结果发现整条Ability的窗口都带过去了,后面无论怎么跳转,窗口都保持横屏。
后来才理解,方向是窗口级别的属性,不是页面级别的属性。如果你想做“应用大部分页面竖屏、视频页横屏”,要么把视频页拆成单独的Ability并提前在module.json5里声明横屏,要么在页面进入和退出时用代码主动切方向,并且保证退出时恢复。没有哪种方案能做到“页面A声明竖屏、页面B声明横屏”而不影响彼此。
5.2 setPreferredOrientation的回调时机不能用来做UI重构
setPreferredOrientation的回调只表示“设置命令已经执行”,不代表窗口尺寸已经变成目标尺寸。如果你在回调里立刻读取窗口宽度并修改布局,大概率拿到的是旧值。
正确的做法是结合windowSizeChange事件,在事件回调里读取最新的Size再做UI重构。这两者之间大概差一个动画过渡周期,虽然时间不长,但对用户体验和逻辑正确性影响不小。
5.3 键盘弹起时转屏导致的布局错乱
键盘和旋转同时发生时,布局错乱概率非常大。竖屏状态下输入框弹出键盘,这时候转横屏,窗口可用高度剧烈变化,键盘状态可能导致输入框被遮挡或者动画卡顿。
我的处理方案是:转屏前主动收起键盘,或者延迟一小段时间再做滚动定位。有些场景下还需要重算keyboardAvoid模式。这里没有万能公式,只能靠多测不同输入场景。
5.4 传感器旋转与应用内旋转的冲突
autoRotation是跟随系统传感器的,而代码里setPreferredOrientation('landscape')是应用层设置。两者冲突时,以最近的一次设置为准。如果你先设置了autoRotation,又立刻设置landscape,可能得到的结果是“看起来锁定了,但传感器偶尔能转过来然后被拉回去”。
避免这种抖动的方法很简单:不要同时混用这两种方式。要有明确的状态管理,比如用一个currentOrientationMode变量,统一控制所有方向设置的入口,避免多处代码各自调用设置接口。
5.5 折叠屏和平板的旋转差异
折叠屏设备上,“展开”不一定会触发旋转。有些设备展开后是从竖屏变横屏,有些是保持竖屏但宽度变大。这就导致一个问题:你用方向来判断页面上的布局,在折叠屏上完全不可靠。更好的判断标准是窗口宽高比和可用宽度,而不是方向枚举值。
平板的情况更复杂,因为平板横向使用比例很高,而且用户对“横屏”的预期跟手机上完全不同。如果应用要做平板适配,建议单独走一套大屏布局规范,别只指望旋转适配逻辑。
6. 实际调试经验:真机、模拟器与不同形态设备的旋转差异
代码写完了,能不能跑好看,还得看调试。我调试旋转功能的设备组合一般是“手机真机 + 模拟器 + 平板/折叠屏”,因为这三类设备的旋转行为差异真的很大。
6.1 模拟器和真机的差异
模拟器上旋转通常非常“规矩”,方向一切就切,几乎没有中间过渡;真机上则带有系统动画、密度换算、刘海区避让等额外处理。所以如果你在模拟器上发现布局没问题,不能直接下“适配完成”的结论,务必在真机上再走一遍。
还有一个很实际的问题:模拟器对传感器的模拟并不可靠。你设置autoRotation后,模拟器上的表现未必能还原真机的翻转效果。我自己习惯用“方向快捷键”来测试,但最终验收一定在真机。
6.2 检查旋转相关日志与状态
调试旋转问题时,日志非常有用。我一般会做这几件事:
- 在
onConfigurationUpdate、windowSizeChange回调里打日志,确认回调顺序和数值。 - 在布局刷新前后打印窗口尺寸和关键组件的宽高,对比是否是预期值。
- 如果出现旋转后组件位置偏移,优先检查父容器的对齐方式,其次是检查有没有写死尺寸。
如果回调根本不打日志,那问题可能出在上游,比如配置没生效、窗口对象没绑定成功,先排查这些基础项。
6.3 一个自查清单
最后分享一个我每次发布前都会过一遍的旋转自查清单。别嫌简单,真能把这几项都做对,旋转相关的线上问题至少能少掉七八成:
| 检查项 | 说明 |
|---|---|
| 方向配置符合产品预期 | 竖屏类应用不要漏配orientation导致意外横屏 |
| 旋转回调里没有耗时操作 | 不在回调里发网络请求、不做大量列表刷新 |
| 输入框键盘状态正常 | 横竖屏切换后输入框不被遮挡 |
| 浮层、弹窗位置正确 | 旋转后浮层重新计算位置,不残留旧布局 |
| 折叠屏展开状态下布局正常 | 不能只按方向判断,要按宽度和宽高比判断 |
| 退出全屏后方向能恢复 | 恢复用户进入前的方向状态 |
| 状态数据不丢失 | 列表滚动位置、表单输入内容在旋转后仍保留 |
写在最后的个人体会:窗口旋转这种东西,看着不起眼,做起来全是细节。花一个下午把配置和回调机制读透,远远比在页面上写各种魔法数字切来切去要值得多。我的习惯是,每个新项目开工第一周就把旋转适配的基础能力搭好,后面所有页面都默认具备这个能力。否则等项目里几十个页面写完再回头补,那才叫真正的灾难。