一、概述
橫豎屏切換功能即實(shí)現(xiàn)應(yīng)用內(nèi)既支持豎屏顯示也支持橫屏顯示的效果。對(duì)于應(yīng)用內(nèi)不同頁面顯示方向不同的情況,需要在應(yīng)用邏輯中,動(dòng)態(tài)修改窗口方向,來實(shí)現(xiàn)該效果,例如包含視頻播放功能的應(yīng)用,首頁內(nèi)容是采用豎屏方式,而視頻詳情頁則采用橫屏方式展示。
本文主要介紹橫豎屏功能的開發(fā)過程中需要關(guān)注的內(nèi)容,包括如下部分:
- 窗口旋轉(zhuǎn)策略的選擇
- 常用應(yīng)用類型的橫豎屏開發(fā)
- 常見的橫豎屏開發(fā)問題
二、窗口旋轉(zhuǎn)說明
目前在HarmonyOS系統(tǒng)中,窗口的旋轉(zhuǎn)形態(tài)包括以下四種,窗口的狀態(tài)對(duì)應(yīng)真機(jī)實(shí)際狀態(tài)如下:

有兩種設(shè)置窗口旋轉(zhuǎn)策略的方式:
通過module.json5文件中“orientation”字段進(jìn)行設(shè)置
在代碼中通過調(diào)用窗口window的setPreferredOrientation方法進(jìn)行設(shè)置
這兩種方式觸發(fā)設(shè)置旋轉(zhuǎn)的時(shí)機(jī)不同,總的來說,module.json5文件中的字段在窗口啟動(dòng)時(shí)就會(huì)生效,對(duì)于應(yīng)用啟動(dòng)時(shí)就需要設(shè)置橫屏或者豎屏的應(yīng)用,需要進(jìn)行配置。而setPreferredOrientation是在調(diào)用該方法時(shí)進(jìn)行窗口方向的設(shè)置,用于在應(yīng)用啟動(dòng)之后,還需要改變顯示方向的場(chǎng)景。
1、配置module.json5的orientation字段
此字段配置的是應(yīng)用啟動(dòng)時(shí)的窗口顯示狀態(tài),對(duì)于開屏?xí)r就需要以默認(rèn)的橫屏或者豎屏方式顯示,需要在此字段進(jìn)行相應(yīng)的配置:
{
"module": {
// ...
"abilities": [
{
"name": "EntryAbility",
// ...
"orientation": "portrait"
}
]
}
}
其支持的參數(shù)可以參考module.json5配置項(xiàng)中orientation字段相關(guān)配置的orientation字段說明:
根據(jù)應(yīng)用默認(rèn)的旋轉(zhuǎn)行為進(jìn)行相應(yīng)的配置:
如果應(yīng)用是豎屏應(yīng)用,建議配置portrait為默認(rèn)旋轉(zhuǎn)策略。
如果應(yīng)用是橫屏應(yīng)用,例如游戲類應(yīng)用,進(jìn)入游戲時(shí),默認(rèn)就是橫屏,此時(shí)有兩種情況:
僅支持橫屏,建議配置landscape為默認(rèn)旋轉(zhuǎn)策略。
支持在橫屏和反向橫屏中切換,建議設(shè)置為auto_rotation_landscape。
如果應(yīng)用為可旋轉(zhuǎn)應(yīng)用,建議應(yīng)用配置auto_rotation_restricted為默認(rèn)旋轉(zhuǎn)策略。
如果一個(gè)應(yīng)用,在直板機(jī)和折疊機(jī)折疊態(tài)是豎屏應(yīng)用,在平板和折疊機(jī)展開態(tài)默認(rèn)是可旋轉(zhuǎn)應(yīng)用,推薦配置follow_desktop為默認(rèn)旋轉(zhuǎn)策略。
注意
對(duì)于需要通過控制中心進(jìn)行旋轉(zhuǎn)鎖定控制的,可以選擇字段后方帶有restricted字段的旋轉(zhuǎn)策略,此字段表示旋轉(zhuǎn)行為受到控制中心按鈕控制,開關(guān)打開情況下,不隨設(shè)備方向旋轉(zhuǎn),關(guān)閉情況下,則會(huì)發(fā)生跟隨設(shè)備旋轉(zhuǎn)。
以如下文件管理應(yīng)用為例,在系統(tǒng)關(guān)閉了旋轉(zhuǎn)鎖定后,應(yīng)用的頁面都會(huì)隨著手機(jī)旋轉(zhuǎn)而發(fā)生展示上的切換,而打開時(shí)則不會(huì)發(fā)生旋轉(zhuǎn)行為,此時(shí)就需要配置為auto_rotation_restricted。

2、調(diào)用窗口的setPreferredOrientation方法
對(duì)于需要進(jìn)入應(yīng)用后,修改應(yīng)用窗口顯示橫豎屏狀態(tài)的情況下,可以調(diào)用setPreferredOrientation 方法進(jìn)行設(shè)置,典型場(chǎng)景如一些視頻類應(yīng)用、圖片類應(yīng)用等。

此類應(yīng)用在進(jìn)入時(shí)為豎屏,而在視頻播放頁面可以顯示為橫屏,則需要支持用戶臨時(shí)修改窗口方向。由于setPreferredOrientation 方法調(diào)用的是窗口的顯示方向,是整個(gè)應(yīng)用窗口級(jí)別都發(fā)生了旋轉(zhuǎn),窗口將一直保持最后一次設(shè)置窗口方向的效果,即使發(fā)生頁面跳轉(zhuǎn)等行為窗口方向也不會(huì)發(fā)生變化。
四、性能優(yōu)化
由于在窗口旋轉(zhuǎn)時(shí),屏幕的尺寸會(huì)發(fā)生變化,界面會(huì)發(fā)生重新布局,為了提高橫豎屏切換時(shí)的流暢度,需要進(jìn)行相應(yīng)的性能優(yōu)化。
1、使用自定義組件凍結(jié)
旋轉(zhuǎn)時(shí),由于整窗一起旋轉(zhuǎn),會(huì)導(dǎo)致頁面重新布局,但是實(shí)際上需要展示的可能只有播放內(nèi)容,對(duì)于其他的組件可以使用自定義組件凍結(jié)功能,避免由于旋轉(zhuǎn)導(dǎo)致的UI更新操作。例如視頻播放底下的詳情內(nèi)容,可能是單獨(dú)的組件。
@Component({ freezeWhenInactive: true }) // 添加自定義組件凍結(jié)功能
struct VideoDetailView {
build() {
Scroll() {
// ... 詳情內(nèi)容
}
}
}
2、對(duì)圖片使用autoResize
如果當(dāng)前旋轉(zhuǎn)頁面存在一些圖片,未經(jīng)合理的裁剪,圖片過大,可以對(duì)圖片設(shè)置autoResize屬性,使圖片裁剪到合適的大小進(jìn)行繪制。該屬性的作用是將組件顯示區(qū)域作為繪制的圖源尺寸,可以減少內(nèi)存占用,例如原圖是19201080,但是顯示區(qū)域是200100,則在解碼時(shí)會(huì)降低采樣編碼到200*100尺寸。
@Builder
function ImageItem(imageSrc:ResourceStr) {
Stack({}) {
Image(imageSrc)
.width('100%')
.height('100%')
.autoResize(true) // 對(duì)圖片使用auto_resize屬性
.borderRadius(8)
.objectFit(ImageFit.Fill)
.backgroundColor('#1AFFFFFF')
}
}
3、排查一些耗時(shí)操作
排查當(dāng)前頁面是否存在一些冗余的OnAreaChange事件、blur模糊或者一些線性變化linearGradient的屬性,這些都比較耗時(shí),可以根據(jù)是否必須使用來決定是否進(jìn)行優(yōu)化。
四、常見場(chǎng)景示例
1、視頻類應(yīng)用橫豎屏開發(fā)
- 監(jiān)聽窗口變化
由于傳感器變化或者用戶手動(dòng)設(shè)置窗口方向時(shí),窗口的顯示會(huì)發(fā)生變化,對(duì)應(yīng)窗口的尺寸也會(huì)發(fā)生改變,此時(shí)可以通過拿到窗口的寬高,并對(duì)寬高進(jìn)行對(duì)比,判斷當(dāng)前顯示是豎屏還是橫屏狀態(tài),并利用該數(shù)據(jù)對(duì)布局進(jìn)行適配。
監(jiān)聽窗口尺寸的變化可以通過window.on('windowSizeChange')進(jìn)行實(shí)現(xiàn)。具體的措施如下,在需要進(jìn)行橫豎屏切換的頁面進(jìn)行以下窗口的監(jiān)聽,一般建議是在aboutToAppear中執(zhí)行:
aboutToAppear(): void {
// ...
this.windowClass.on('windowSizeChange', (size) => {
// ...
});
// ...
}
并在aboutToDisappear中取消監(jiān)聽:
aboutToDisappear(): void {
// ...
this.windowClass.off('windowSizeChange');
}
需要注意的是,當(dāng)用戶手動(dòng)觸發(fā)setOrientation設(shè)置為橫屏狀態(tài)時(shí),即使當(dāng)前手機(jī)處于垂直方向,窗口的狀態(tài)也是橫屏方向,即如下所示:

此時(shí),窗口的寬是豎屏狀態(tài)下的高,高變?yōu)樨Q屏狀態(tài)的寬。所以在監(jiān)聽窗口變化時(shí),可以通過窗口的寬高大小關(guān)系,來確定當(dāng)前窗口的方向,并決定實(shí)際的橫豎屏狀態(tài)。
this.windowClass.on('windowSizeChange', (size) => {
let viewWidth = px2vp(size.width);
let viewHeight = px2vp(size.height);
if (viewWidth > viewHeight) {
// ...
} else {
// ...
}
});
- 進(jìn)行布局適配
對(duì)應(yīng)視頻播放這類應(yīng)用,屬于只有播放窗口需要進(jìn)行橫豎屏,所以只需要對(duì)視頻播放的組件內(nèi)容進(jìn)行橫屏并進(jìn)入全屏,所以可以利用UI狀態(tài)更新的特點(diǎn),來讓播窗變?yōu)槿粒瑢⒉ゴ暗某叽缍x為@State狀態(tài),并設(shè)置到Xcomponent組件上。
@State xComponentWidth: number = px2vp(display.getDefaultDisplaySync().width);
@State xComponentHeight: number = px2vp(display.getDefaultDisplaySync().width * this.aspect);
將狀態(tài)變量與播窗綁定。
XComponent({ id: 'video_player_id', type: XComponentType.SURFACE, controller: this.xComponentController })
.onLoad(() => {
// ...
})
.width(this.xComponentWidth)
.height(this.xComponentHeight)
并且在之前監(jiān)聽窗口變化的回調(diào)中,對(duì)XComponentWidth和XComponentHeight進(jìn)行動(dòng)態(tài)修改,完成窗口變化時(shí)橫屏和豎屏的視頻窗口布局。需要注意的是,在橫屏?xí)r,視頻播放的寬高應(yīng)該和窗口的寬高一樣,并且需要進(jìn)入全屏狀態(tài)。而豎屏?xí)r,視頻播放的寬應(yīng)該等于窗口的寬,但是高度應(yīng)該是按照播窗比例乘以窗口的寬進(jìn)行設(shè)置,并退出全屏狀態(tài)。

具體實(shí)現(xiàn)如下,進(jìn)入視頻詳情頁面內(nèi)需要監(jiān)聽窗口尺寸的變化,并根據(jù)當(dāng)前狀態(tài),實(shí)現(xiàn)對(duì)橫豎屏狀態(tài)的監(jiān)聽,根據(jù)狀態(tài)變化修改對(duì)應(yīng)XComponent的寬高,實(shí)現(xiàn)全屏或者隱藏狀態(tài)欄和導(dǎo)航條的邏輯。
this.windowClass.on('windowSizeChange', (size) => {
let viewWidth = px2vp(size.width);
let viewHeight = px2vp(size.height);
if (viewWidth > viewHeight) {
// 實(shí)現(xiàn)橫屏邏輯
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 {
// 實(shí)現(xiàn)豎屏邏輯
this.xComponentHeight = viewWidth * this.aspect;
this.xComponentWidth = viewWidth;
this.windowClass.setSpecificSystemBarEnabled('navigationIndicator', true); // show bottom navigation bar
this.isLandscape = false;
}
});
2、游戲類應(yīng)用橫屏開發(fā)
對(duì)于游戲類應(yīng)用,以橫屏游戲居多,此類應(yīng)用不需要在應(yīng)用內(nèi)進(jìn)行開關(guān)控制,所以只需要在module.json5配置文件中進(jìn)行相應(yīng)的配置即可。一般有以下幾種情況:
- 默認(rèn)僅為橫屏
如果該應(yīng)用默認(rèn)為橫屏狀態(tài),那么則需要在module.json5中的“orientation”字段進(jìn)行配置為landscape。

- 支持橫屏和反向橫屏
如果應(yīng)用需要根據(jù)設(shè)備方向,決定是橫屏還是反向橫屏,則可以對(duì)module.json5配置中“orientation”設(shè)置為auto_rotation_landscape。

注意 如果需要跟隨控制中心的旋轉(zhuǎn)鎖定,則可以選擇配置為"auto_rotation_landscape_restricted"
- 支持豎屏切換橫屏
此類適用于一些游戲大廳等應(yīng)用,由于游戲大廳內(nèi),主頁可能為豎屏,部分應(yīng)用為橫屏。那么從豎屏進(jìn)入橫屏?xí)r,則需要調(diào)用設(shè)置窗口旋轉(zhuǎn)方法進(jìn)行窗口控制。
五、其他常見問題
1、Tabs欄中的視頻橫屏播放,無法隱藏Tabs欄
對(duì)于首頁中有部分視頻可以直接播放,并且不會(huì)跳轉(zhuǎn)至詳情頁播放的,需要支持直接在首頁發(fā)生旋轉(zhuǎn),當(dāng)前可以通過設(shè)置XComponent的寬高實(shí)現(xiàn),但是會(huì)發(fā)現(xiàn)即使全屏后,Tabs欄并不會(huì)消失。而是會(huì)一起發(fā)生旋轉(zhuǎn)并存在于頁面上。解決方案如下:
進(jìn)入全屏?xí)r,隱藏Tabs,退出全屏?xí)r,展示Tabs欄。
@Component
struct TabsView {
@State isLayoutFullScreen:boolean = false;
build() {
Tabs() {
// ... 省略布局內(nèi)容
}
.barHeight(this.isLayoutFullScreen ? 0 : 50) // 通過用戶是否需要點(diǎn)擊進(jìn)入全屏,隱藏Tabs標(biāo)簽欄的高度,可以實(shí)現(xiàn)隱藏
}
}
2、如何解決直板機(jī)和平板上默認(rèn)旋轉(zhuǎn)行為不一致的問題?
對(duì)于部分應(yīng)用,在直板手機(jī)上默認(rèn)是采用豎屏顯示的策略,但是在平板或者折疊屏上,是需要支持自動(dòng)旋轉(zhuǎn)的,如果在Ability的生命周期中調(diào)用setPreferredOrientation,可能會(huì)出現(xiàn)在應(yīng)用啟動(dòng)時(shí),出現(xiàn)旋轉(zhuǎn)動(dòng)畫的情況,所以可以將module.json5文件中的“orientation”字段設(shè)置為“follow_desktop”。
3、通過調(diào)用window.getLastWindow的方式獲取窗口實(shí)例出現(xiàn)延遲,如何解決?
由于getLastWindow底層原因,需要經(jīng)過查找獲取實(shí)例,一定程度上會(huì)有性能損耗,可能會(huì)出現(xiàn)已經(jīng)發(fā)生橫屏或者豎屏切換的情況下,狀態(tài)欄還沒切換的情況。對(duì)此類場(chǎng)景的同步要求較高情況下,可以使用windowStage.getMainWindowSync的同步方式獲取窗口實(shí)例。
4、自動(dòng)旋轉(zhuǎn)和旋轉(zhuǎn)鎖定按鈕的關(guān)系是什么?與Orientation字段的關(guān)系如何判斷?
控制中心的旋轉(zhuǎn)開關(guān)控制的是當(dāng)前是否可以跟隨屏幕旋轉(zhuǎn),“旋轉(zhuǎn)鎖定”高亮狀態(tài)是鎖定,不可旋轉(zhuǎn)?!靶D(zhuǎn)鎖定”灰色是解鎖狀態(tài),可以旋轉(zhuǎn)。對(duì)于希望跟隨控制中心的旋轉(zhuǎn)開關(guān)的行為,可以選擇帶RESTRICTED后綴字段的旋轉(zhuǎn)方式。
例如想實(shí)現(xiàn)跟隨控制中心的自動(dòng)旋轉(zhuǎn),如果想旋轉(zhuǎn)到橫屏,豎屏,反向橫屏,反向豎屏。則可以設(shè)置為AUTO_ROTATION_RESTRICTED。
如果不想跟隨控制中心的控制開關(guān),那么只需要設(shè)置為AUTO_ROTATION,此時(shí)應(yīng)用的旋轉(zhuǎn)不受到控制中心的鎖定控制。其他的旋轉(zhuǎn)方式同理。
其他更詳細(xì)內(nèi)容可參見官網(wǎng)《最佳實(shí)踐》