概述
横竖屏切换功能即实现应用内既支持竖屏显示也支持横屏显示的效果。对于应用内不同页面显示方向不同的情况,需要在应用逻辑中,动态修改窗口方向,来实现该效果,例如包含视频播放功能的应用,首页内容是采用竖屏方式,而视频详情页则采用横屏方式展示。
本文主要介绍横竖屏功能的开发过程中需要关注的内容,包括如下部分:
- 窗口旋转策略的选择
- 常用应用类型的横竖屏开发
- 常见的横竖屏开发问题
窗口旋转说明
目前在HarmonyOS系统中,窗口的旋转形态包括以下四种,窗口的状态对应真机实际状态如下:
图1 窗口形态示意图
有两种设置窗口旋转策略的方式:
一、通过module.json5文件中“orientation”字段进行设置
二、在代码中通过调用窗口window的setPreferredOrientation方法进行设置
这两种方式触发设置旋转的时机不同,总的来说,module.json5文件中的字段在窗口启动时就会生效,对于应用启动时就需要设置横屏或者竖屏的应用,需要进行配置。而[setPreferredOrientation]是在调用该方法时进行窗口方向的设置,用于在应用启动之后,还需要改变显示方向的场景。
配置module.json5的orientation字段
此字段配置的是应用启动时的窗口显示状态,对于开屏时就需要以默认的横屏或者竖屏方式显示,需要在此字段进行相应的配置:
{
"module": {
// ...
"abilities": [
{
"name": "EntryAbility",
// ...
"orientation": "portrait"
}
]
}
}
其支持的参数可以参考module.json5配置项中[orientation字段相关配置]的[orientation字段]说明:
根据应用默认的旋转行为进行相应的配置:
-
如果应用是竖屏应用,建议配置portrait为默认旋转策略。
-
如果应用是横屏应用,例如游戏类应用,进入游戏时,默认就是横屏,此时有两种情况:
一、仅支持横屏,建议配置landscape为默认旋转策略。
二、支持在横屏和反向横屏中切换,建议设置为auto_rotation_landscape。
-
如果应用为可旋转应用,建议应用配置auto_rotation_restricted为默认旋转策略。
-
如果一个应用,在直板机和折叠机折叠态是竖屏应用,在平板和折叠机展开态默认是可旋转应用,推荐配置follow_desktop为默认旋转策略。
注意
对于需要通过控制中心进行旋转锁定控制的,可以选择字段后方带有restricted字段的旋转策略,此字段表示旋转行为受到控制中心按钮控制,开关打开情况下,不随设备方向旋转,关闭情况下,则会发生跟随设备旋转。
以如下文件管理应用为例,在系统关闭了旋转锁定后,应用的页面都会随着手机旋转而发生展示上的切换,而打开时则不会发生旋转行为,此时就需要配置为auto_rotation_restricted。
图2 应用随系统旋转切换横竖屏
调用窗口的setPreferredOrientation方法
对于需要进入应用后,修改应用窗口显示横竖屏状态的情况下,可以调用[setPreferredOrientation ]方法进行设置,典型场景如一些视频类应用、图片类应用等。
图3 视频播窗横竖屏切换
此类应用在进入时为竖屏,而在视频播放页面可以显示为横屏,则需要支持用户临时修改窗口方向。由于[setPreferredOrientation ]方法调用的是窗口的显示方向,是整个应用窗口级别都发生了旋转,窗口将一直保持最后一次设置窗口方向的效果,即使发生页面跳转等行为窗口方向也不会发生变化。
视频类应用横竖屏开发
为了实现应用的横竖屏功能,我们需要从以下技术方面进行考虑:
1、设置窗口的旋转策略
2、监听屏幕的窗口变化
3、进行布局适配
设置窗口的旋转策略
首先需要对应用启动时的旋转策略进行设置,具体可以参考[配置module.json5的Orientation字段],这里我们以实现了一多开发为例,满足直板机和平板设备不同的策略,设置为follow_desktop,这个字段主要解决在不同设备上默认旋转策略不同的问题。
在具体需要实现横竖屏切换的页面上,例如视频播放页面支持横屏,但是首页的内容是支持仅竖屏的,那么就需要在进入对应的页面时,采用window窗口提供的设置窗口方向的能力,通过[setPreferredOrientation ]将窗口显示的方向修改为横屏、竖屏的状态。在使用时,需根据应用自身的旋转策略选择相应的参数,可以封装如下方法,进行旋转策略的设置。
具体如下:通过getContext拿到对应的UIAbilityContext,并通过context拿到对应的windowStage实例,然后通过windowStage.getMainWindowSync同步方法拿到对应的窗口实例win,然后调用[setPreferredOrientation ]方法设置窗口方向。
setOrientation(orientation: number) {
this.windowClass.setPreferredOrientation(orientation).then(() => {
console.info('setWindowOrientation: ' + orientation + ' Succeeded.');
}).catch((err: BusinessError) => {
console.info('setWindowOrientation: ' + orientation + ' Failed. Cause: ' + JSON.stringify(err));
});
}
以视频播放为例,不仅需要可以通过系统控制横竖屏,也支持用户在系统锁定旋转的情况下,手动设置横屏状态,即需要满足以下条件:
1、应用跟随传感器旋转。
2、受到控制中心的旋转锁定按钮控制。
3、支持用户使用应用时,在页面中临时调用设置方向能力,如点击全屏按钮进行切换。
要实现以上效果,可以使用窗口的能力设置orientation的枚举类型进行相应的旋转,由于要提供临时设置方向的能力,用户在手动点击全屏按钮时,相当于需要手动触发横竖屏切换,如果此时关闭了旋转锁定,那么窗口需要能够跟随传感器一起发生旋转,所以可以使用以下枚举中的能力,临时调用旋转,并让其后续也能支持跟随传感器。
orientation枚举值 | 枚举数值 | 效果描述 |
---|---|---|
USER_ROTATION_PORTRAIT | 13 | 调用时临时旋转到竖屏,之后跟随传感器自动旋转,受控制中心的旋转开关控制,且可旋转方向受系统判定。 |
USER_ROTATION_LANDSCAPE | 14 | 调用时临时旋转到横屏,之后跟随传感器自动旋转,受控制中心的旋转开关控制,且可旋转方向受系统判定。 |
然后来分析旋转方向:对于视频播放的应用,一般旋转方向不会旋转到反向竖屏(由UX需求决定),即只旋转到竖屏、横屏和反向横屏。并且用户在使用时,点击切换按钮时,根据用户习惯,一般默认是旋转到横屏状态,然后支持跟随传感器旋转到反向横屏。
图4 窗口旋转状态示意图
在需要用户点击的地方,需要根据进入全屏和退出全屏分别执行相应的逻辑,需要使用的方向状态如下:
设置为横屏时,对应窗口方向为横屏状态:this.setOrientation(window.Orientation.USER_ROTATION_LANDSCAPE),例如进入播放页时,进行竖屏 -> 横屏切换;
Image($r('app.media.icon_zoom_in'))
// ...
.onClick(() => {
if (display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_EXPANDED || display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_HALF_FOLDED) {
this.isLandscape = true;
} else {
this.setOrientation(window.Orientation.USER_ROTATION_LANDSCAPE);
}
})
设置为竖屏时,对应窗口方向为竖屏状态:this.setOrientation(window.Orientation.USER_ROTATION_PORTRAIT),例如在返回竖屏状态时,进行横屏 -> 竖屏切换;
Image($r('app.media.icon_back'))
// ...
.onClick(() => {
// ...
this.setOrientation(window.Orientation.USER_ROTATION_PORTRAIT);
// ...
})
注意
需要注意的是,由于setPreferredOrientation方法调用改变的是窗口的显示方向,所以如果在进入视频播放页时,手动调用了旋转到横屏,那么在退出时也需要手动调用该接口,使窗口回到之前的竖屏状态。
监听窗口变化
由于传感器变化或者用户手动设置窗口方向时,窗口的显示会发生变化,对应窗口的尺寸也会发生改变,此时可以通过拿到窗口的宽高,并对宽高进行对比,判断当前显示是竖屏还是横屏状态,并利用该数据对布局进行适配。
监听窗口尺寸的变化可以通过window.[on(‘windowSizeChange’)]进行实现。具体的措施如下,在需要进行横竖屏切换的页面进行以下窗口的监听,一般建议是在aboutToAppear中执行:
aboutToAppear(): void {
// ...
this.windowClass.on('windowSizeChange', (size) => {
// ...
});
// ...
}
并在aboutToDisappear中取消监听:
aboutToDisappear(): void {
// ...
this.windowClass.off('windowSizeChange');
}
需要注意的是,当用户手动触发setOrientation设置为横屏状态时,即使当前手机处于垂直方向,窗口的状态也是横屏方向,即如下所示:
图5 临时设置窗口方向时,窗口宽高示意图
此时,窗口的宽是竖屏状态下的高,高变为竖屏状态的宽。所以在监听窗口变化时,可以通过窗口的宽高大小关系,来确定当前窗口的方向,并决定实际的横竖屏状态。
this.windowClass.on('windowSizeChange', (size) => {
let viewWidth = px2vp(size.width);
let viewHeight = px2vp(size.height);
if (viewWidth > viewHeight) {
// ...
} else {
// ...
}
});
进行布局适配
对应视频播放这类应用,属于只有播放窗口需要进行横竖屏,所以只需要对视频播放的组件内容进行横屏并进入全屏,所以可以利用UI状态更新的特点,来让播窗变为全屏,将播窗的尺寸定义为@State状态,并设置到Xcomponent组件上。
@State xComponentWidth: number = px2vp(display.getDefaultDisplaySync().width);
@State xComponentHeight: number = px2vp(display.getDefaultDisplaySync().width * this.aspect);
将状态变量与播窗绑定。
XComponent({ id: 'video_player_id', type: XComponentType.SURFACE, controller: this.xComponentController })
.onLoad(() => {
// ...
})
.width(this.xComponentWidth)
.height(this.xComponentHeight)
并且在之前监听窗口变化的回调中,对XComponentWidth和XComponentHeight进行动态修改,完成窗口变化时横屏和竖屏的视频窗口布局。需要注意的是,在横屏时,视频播放的宽高应该和窗口的宽高一样,并且需要进入全屏状态。而竖屏时,视频播放的宽应该等于窗口的宽,但是高度应该是按照播窗比例乘以窗口的宽进行设置,并退出全屏状态。
图6 旋转过程宽高变化示意图
具体实现如下,进入视频详情页面内需要监听窗口尺寸的变化,并根据当前状态,实现对横竖屏状态的监听,根据状态变化修改对应XComponent的宽高,实现全屏或者隐藏状态栏和导航条的逻辑。
注意
需要注意的是,对于视频全屏效果,建议采用沉浸式开发,沉浸式效果的实现。
this.windowClass.on('windowSizeChange', (size) => {
let viewWidth = px2vp(size.width);
let viewHeight = px2vp(size.height);
if (viewWidth > viewHeight) {
// 实现横屏逻辑
if (display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_EXPANDED || display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_HALF_FOLDED) {
this.xComponentHeight = viewWidth * this.aspect;
this.xComponentWidth = viewWidth;
} else {
this.xComponentWidth = viewHeight / this.aspect;
this.xComponentHeight = viewHeight;
this.isLandscape = true;
}
this.windowClass.setSpecificSystemBarEnabled('navigationIndicator', false); // hide bottom navigation bar
} else {
// 实现竖屏逻辑
this.xComponentHeight = viewWidth * this.aspect;
this.xComponentWidth = viewWidth;
this.windowClass.setSpecificSystemBarEnabled('navigationIndicator', true); // show bottom navigation bar
this.isLandscape = false;
}
});
说明
上述代码完成的是直板机上的旋转逻辑,在进入页面时进行窗口变化的监听,需要在退出页面时关闭监听功能。另外,对于在平板下,如果旋转行为只会触发页面旋转,不触发视频播窗的全屏,需要进行特殊处理。
所以这里还需要对旋转行为进行一个限制,只有在直板机设备上才能让发生旋转时,将播窗全屏,对应平板上,部分情况下并不需要视频播窗全屏而是直接将布局进行旋转。
- 可以采用[deviceInfo]设备类型进行判断
- 采用响应式布局中[断点]的方式进行判断
锁定屏幕功能
部分视频应用支持屏幕锁定功能,在全屏状态下,实现对功能按钮的隐藏,并临时锁定屏幕的旋转,避免用户误触其他操作按钮。屏幕锁定后,应用可以在横屏和反向横屏之间翻转,不可由横屏旋转为竖屏,解锁后,如果当前屏幕处于竖屏,则应该恢复到竖屏显示。
图7 屏幕锁定功能
针对上述功能可以考虑如下实现:
1、判断当前控制中心的开关状态,如果是已经锁定情况下,则不需要进行额外处理
2、锁定时设置旋转策略为AUTO_ROTATION_LANDSCAPE,即支持横屏和反向横屏旋转,不受控制中心控制
3、解锁时需要回到支持三向旋转,即支持横屏、竖屏、反向横屏,并受到控制中心控制
可以由上述推断功能实现得到如下代码:
Image(this.isVideoLock ? $r('app.media.icon_lock') : $r('app.media.icon_lock_open'))
// ...
.onClick(() => {
this.isVideoLock = !this.isVideoLock;
// ...
if (this.isVideoLock) {
this.setOrientation(window.Orientation.AUTO_ROTATION_LANDSCAPE);
} else {
this.setOrientation(window.Orientation.AUTO_ROTATION_UNSPECIFIED);
}
})
对于需要在折叠屏状态下对旋转逻辑进行单独处理的情况,可以封装如下方法isExpandedOrHalfFolded方法,来判断当前设备是否为折叠屏展开态,处于折叠屏展开态情况下,不会触发旋转逻辑。
isExpandedOrHalfFolded(): boolean {
return display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_EXPANDED ||
display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_HALF_FOLDED;
}
当要对设备控制中心状态进行监听时,可以通过setting.registerKeyObserver,监听控制中心变化,其中orientationLockState是当前设备控制中心的状态,当设备控制中心的状态为“0”时,即代表旋转功能被系统锁定,此时在处理锁定功能代码中不需要进行旋转逻辑的处理,因为在控制中心旋转开关关闭的情况下,屏幕的旋转行为会收到限制,所以首先要考虑对控制中心的旋转锁定状态进行判断,可以参考如下代码实现对设备控制中心的状态监听:
settings.registerKeyObserver(context, settings.general.ACCELEROMETER_ROTATION_STATUS,
settings.domainName.DEVICE_SHARED, () => {
this.orientationLockState =
settings.getValueSync(getContext(this), settings.general.ACCELEROMETER_ROTATION_STATUS,
settings.domainName.DEVICE_SHARED);
});
排除了当前设备后,即可以进行通过应用内锁定按钮进行逻辑判断,锁定状态下,调用setOrientation设置为Landscape,此时可以在横屏和反向横屏旋转,解锁后,恢复到三向(横屏、竖屏、反向横屏)旋转策略:
if (this.isVideoLock) {
this.setOrientation(window.Orientation.AUTO_ROTATION_LANDSCAPE);
} else {
this.setOrientation(window.Orientation.AUTO_ROTATION_UNSPECIFIED);
}
游戏类应用横屏开发
对于游戏类应用,以横屏游戏居多,此类应用不需要在应用内进行开关控制,所以只需要在module.json5配置文件中进行相应的配置即可。一般有以下几种情况:
默认仅为横屏
如果该应用默认为横屏状态,那么则需要在module.json5中的“orientation”字段进行配置为landscape。
图8 应用仅支持横屏状态
支持横屏和反向横屏
如果应用需要根据设备方向,决定是横屏还是反向横屏,则可以对module.json5配置中“orientation”设置为auto_rotation_landscape。
图9 应用支持横屏和反向横屏
注意
如果需要跟随控制中心的旋转锁定,则可以选择配置为"auto_rotation_landscape_restricted"
支持竖屏切换横屏
此类适用于一些游戏大厅等应用,由于游戏大厅内,主页可能为竖屏,部分应用为横屏。那么从竖屏进入横屏时,则需要调用设置窗口旋转方法进行窗口控制,具体案例可以参考视频播放的横竖屏切换。
性能优化
由于在窗口旋转时,屏幕的尺寸会发生变化,界面会发生重新布局,为了提高横竖屏切换时的流畅度,需要进行相应的性能优化。
使用自定义组件冻结
旋转时,由于整窗一起旋转,会导致页面重新布局,但是实际上需要展示的可能只有播放内容,对于其他的组件可以使用自定义组件冻结功能,避免由于旋转导致的UI更新操作。例如视频播放底下的详情内容,可能是单独的组件。
@Component({ freezeWhenInactive: true }) // 添加自定义组件冻结功能
struct VideoDetailView {
build() {
Scroll() {
// ... 详情内容
}
}
}
对图片使用autoResize
如果当前旋转页面存在一些图片,未经合理的裁剪,图片过大,可以对图片设置autoResize属性,使图片裁剪到合适的大小进行绘制。该属性的作用是将组件显示区域作为绘制的图源尺寸,可以减少内存占用,例如原图是19201080,但是显示区域是200100,则在解码时会降低采样编码到200*100尺寸。
@Builder
function ImageItem(imageSrc:ResourceStr) {
Stack({}) {
Image(imageSrc)
.width('100%')
.height('100%')
.autoResize(true) // 对图片使用auto_resize属性
.borderRadius(8)
.objectFit(ImageFit.Fill)
.backgroundColor('#1AFFFFFF')
}
}
排查一些耗时操作
排查当前页面是否存在一些冗余的OnAreaChange事件、blur模糊或者一些线性变化linearGradient的属性,这些都比较耗时,可以根据是否必须使用来决定是否进行优化。
其他常见问题
Tabs栏中的视频横屏播放,无法隐藏Tabs栏
对于首页中有部分视频可以直接播放,并且不会跳转至详情页播放的,需要支持直接在首页发生旋转,当前可以通过设置XComponent的宽高实现,但是会发现即使全屏后,Tabs栏并不会消失。而是会一起发生旋转并存在于页面上。解决方案如下:
进入全屏时,隐藏Tabs,退出全屏时,展示Tabs栏。
@Component
struct TabsView {
@State isLayoutFullScreen:boolean = false;
build() {
Tabs() {
// ... 省略布局内容
}
.barHeight(this.isLayoutFullScreen ? 0 : 50) // 通过用户是否需要点击进入全屏,隐藏Tabs标签栏的高度,可以实现隐藏
}
}
如何解决直板机和平板上默认旋转行为不一致的问题?
对于部分应用,在直板手机上默认是采用竖屏显示的策略,但是在平板或者折叠屏上,是需要支持自动旋转的,如果在Ability的生命周期中调用setPreferredOrientation,可能会出现在应用启动时,出现旋转动画的情况,所以可以将module.json5文件中的“orientation”字段设置为“follow_desktop”。
通过调用window.getLastWindow的方式获取窗口实例出现延迟,如何解决?
由于getLastWindow底层原因,需要经过查找获取实例,一定程度上会有性能损耗,可能会出现已经发生横屏或者竖屏切换的情况下,状态栏还没切换的情况。对此类场景的同步要求较高情况下,可以使用[windowStage.getMainWindowSync]的同步方式获取窗口实例。
自动旋转和旋转锁定按钮的关系是什么?与Orientation字段的关系如何判断?
控制中心的旋转开关控制的是当前是否可以跟随屏幕旋转,“旋转锁定”高亮状态是锁定,不可旋转。“旋转锁定”灰色是解锁状态,可以旋转。对于希望跟随控制中心的旋转开关的行为,可以选择带RESTRICTED后缀字段的旋转方式。
例如想实现跟随控制中心的自动旋转,如果想旋转到横屏,竖屏,反向横屏,反向竖屏。则可以设置为AUTO_ROTATION_RESTRICTED。
如果不想跟随控制中心的控制开关,那么只需要设置为AUTO_ROTATION,此时应用的旋转不受到控制中心的锁定控制。其他的旋转方式同理。