平常我們做過的需求里,主要是以豎屏式為主,而橫屏式較少。對于豎屏式場景來說,大家的經驗會比較豐富,因此,此次主要式探討下橫屏式場景下的一些需要注意的點,特別是怎樣去做橫屏適配。
對于 H5 橫屏頁面來說,要實現橫屏的話,主要是解決兩點:
1.無論用戶手持方向如何,都需要保證屏幕橫向顯示。
2.由于屏幕分辨率的多樣化,因此就算是橫屏下也是需要進行橫屏適配,保證頁面在所有分辨率下都能夠合理適配。
強制橫屏顯示
在橫屏式 H5橫屏頁面中可以采取簡單的措施進行處理,在頁面內容按豎排方向顯示時,開發者進行對用戶提示其保持橫屏體驗。
但是,這對用戶體驗并不友好,因為這對于那些習慣于打開鎖定為豎排方向功能的 iOS 平臺用戶,或者是關閉屏幕旋轉功能的 Android 平臺用戶來說,他們需要多一個處理步驟——先關閉豎排方向鎖定或是開啟屏幕旋轉,然后再橫向手持設備。
因此,更好的做法是強制橫屏顯示,對屏幕 resize 事件進行監聽,當判斷為豎屏時將整個根容器進行逆時針 CSS3 旋轉 90 度即可,代碼如下所示。
// 利用 CSS3 旋轉 對根容器逆時針旋轉 90 度 var detectOrient = function() { var width = document.documentElement.clientWidth, height = document.documentElement.clientHeight, $wrapper = document.getElementById("J_wrapper"), style = "";if( width >= height ){ // 橫屏 style += "width:" + width + "px;"; // 注意旋轉后的寬高切換 style += "height:" + height + "px;"; style += "-webkit-transform: rotate(0); transform: rotate(0);"; style += "-webkit-transform-origin: 0 0;"; style += "transform-origin: 0 0;"; } else{ // 豎屏 style += "width:" + height + "px;"; style += "height:" + width + "px;"; style += "-webkit-transform: rotate(90deg); transform: rotate(90deg);"; // 注意旋轉中點的處理 style += "-webkit-transform-origin: " + width / 2 + "px " + width / 2 + "px;"; style += "transform-origin: " + width / 2 + "px " + width / 2 + "px;"; } $wrapper.style.cssText = style; } window.onresize = detectOrient; detectOrient();
但是!這里有坑:如果你是采用 CreateJS 框架進行開發,那么就不能通過 CSS3 途徑對包含 Canvas 的根容器進行旋轉處理,因為旋轉后會導致 Canvas 內的舞臺元素的事件響應位置錯亂。
解決辦法是,換成利用 CreateJS 框架內的 Stage 的?rotation
?屬性對整個舞臺旋轉處理,代碼如下:
if(self.isPortrait) { // 豎屏 // 舞臺旋轉 self.stage.x = self.canvasHeight; // 注意:x偏移相當于旋轉中點處理,更簡單 self.stage.rotation = 90; // more... }else { // 橫屏 self.stage.x = 0; self.stage.rotation = 0; // more... }
橫屏適配處理
面對移動端多分辨率繁復冗雜的情況,我們對于一般情況下(也就是常見的豎屏式)頁面適配處理可以說是爛熟于心,但是切換到橫屏式場景下,同樣的頁面適配方法可以直接應用嗎?會不會有什么問題呢?
下面分別從 DOM 和 Canvas 兩方面去著手闡述如何做橫屏適配處理。
解決 DOM 的橫屏適配問題
在移動端,常見的移動端適配方案是 REM 方案,而為了減少 JS 與 CSS 的耦合,采用的是 VW + REM 方法。
因為頁面適配的場景往往是豎屏式的,因此 VW + REM 方案表現得十分完美。但是遇上橫屏式,它的缺點就暴露了出來。
由于響應斷點的限制最大寬度處理,會導致頁面兩側留白,當然這可以通過去掉最大寬度限制來解決。而真正的缺點在于,由于 vw 單位的特性,適配換算大小是根據屏幕寬度而言的,因此屏幕寬度越大導致容器、文字會越大,還可能導致 DOM 元素超出屏幕外,且文字過大并不是我們所想要的用戶體驗。
那么,換成 px 單位的固定布局如何?
但 px 單位的固定布局只適合于部分場景,對于需要內容全屏覆蓋的場景,就可能存在這樣的不理想的用戶體驗:絕對定位的元素之間空隙過大,導致布局不美觀,又或者空隙過小,導致元素疊放被遮擋。
了解到,vw 單位的特點是適配換算大小時是根據屏幕寬度而定的,那么在強制橫屏顯示時,我們就可以同理轉換為屏幕高度來而定,也就是 vw 單位替換成 vh 單位。這樣進一步改良之后就會得到滿意的適配效果。
具體實現可參考如下 SCSS 代碼:
$vw_base: 375; $vw_fontsize: 20; html { font-size: 20px; //不支持vw單位時,回退到px單位 font-size: ($vw_fontsize / $vw_base) * 100vw; } @media screen and (orientation: landscape) { html { font-size: 20px; font-size: ($vw_fontsize / $vw_base) * 100vh; } }
?
解決 Canvas 的橫屏適配問題
解決 Canvas 的橫屏適配問題,目前在實際應用中有兩種主流的方案:
- 通過做兩套Canvas的方案。
- 采用縮放的手段進行適配的方案。
兩套 Canvas 的方案的做法是,頁面包含兩個 Canvas 分別用于橫豎屏時的相應顯示,但是它們的數據是打通的。但是,該方案難免會有局限性,比較適合邏輯數據處理簡單、且舞臺元素少且居中的場景;
而縮放適配方案做法是,采用的最為常見的縮放手段——利用 CSS3 Transform 的?scale
?屬性,達到“一種設計尺寸適配多種分辨率屏幕”的目的。
對于常用的 CreateJS、PixiJS 框架來說,它們并沒有配套的現成的橫屏適配解決方案可以被采用的,尤其是我們如果采用原生 Javascript 去開發一個橫屏項目的時候。
因此,下面我們來研究下如何解決 Canvas 橫屏適配問題。
注意:下面文中示例代碼都是在 CreateJS 框架的基礎上進行編寫的。
選用合適的縮放模式
橫屏適配的核心是縮放,通過?scale
?屬性等手法將Canvas縮放至適合屏幕窗口大小。類似于?background-size
?屬性的表現,縮放適配也可以有很多種模式,或有裁剪或無裁剪,或根據長邊縮放或根據短邊縮放等等。根據一些常見的實際應用場景,有比較常用的五種縮放模式:Contain、Cover、Fill、Fixed-Width、Fixed-Height。根據游戲的不同的實際場景需求,我們可以選其中一種縮放模式進行適配。
下面,我們逐一解釋以上五種縮放模式的定義、實現與其適用的場景。
a. Contain模式
Canvas可以類比為一張圖,而圖片的適配,我們可以聯想到經常用以適配背景圖片的屬性?background-size
?,其屬性值包括?contain
、cover
。
借助?contain
?的概念,我們把縮放的其中一種模式稱為 Contain 模式。因為在這種模式下,舞臺內容(gameArea)會保持寬高比進行縮放適配瀏覽器可視窗口(window),縮放至其能顯示完整的舞臺內容。
根據推導結論,簡單代碼實現如下:
// Contain模式核心原理函數 CONTAIN: function(){ var self = this; self.radioX = self.radioY = Math.min((self.winWidth / self.designWidth) , (self.winHeight / self.designHeight)); self.canvasWidth = self.designWidth; self.canvasHeight = self.designHeight; }
可以看出,在 Contain 模式下,如果舞臺內容寬高比與瀏覽器可視窗口的寬高比不相等時,舞臺內容并沒有填滿整個瀏覽器可視窗口,此時就會出現上下或左右兩側會存在留空部分。
對于這種 Contain 模式,會比較適合舞臺背景為純色或者是漸變類型的H5輕互動,舞臺內容與窗口的緊鄰處得以自然過渡銜接,不會突兀。
b. Cover模式
同樣地,借助?cover
?的概念把其中一種模式稱為 Cover 模式。在這種模式下,舞臺內容(gameArea)會保持寬高比進行縮放適配瀏覽器可視窗口(window),縮放至舞臺內容填滿窗口。
根據推導結論,簡單代碼實現如下:
// Cover模式核心原理函數 COVER: function(){ var self = this; self.radioX = self.radioY = Math.max((self.winWidth / self.designWidth) , (self.winHeight / self.designHeight)); self.canvasWidth = self.designWidth; self.canvasHeight = self.designHeight; }
在 Cover 模式下,如果舞臺內容寬高比與瀏覽器可視窗口的寬高比不相等時,由于舞臺內容需要填滿整個瀏覽器可視窗口,此時就會出現上下或者左右兩側被裁剪的情況。
那么,如果能保證游戲場景內的重點顯示內容全部顯示,被裁剪內容無關緊要時,那么這種 H5 輕互動類型就可以考慮采用 Cover 模式。
怎么做到保證想要重點顯示的內容可以不被裁剪呢?這時要談到一個“安全區域”的概念,指的是絕對不會被裁剪的內容區域,它應該是由最小的屏幕可視窗口(目前應該是 iPhone 4 )與最大的屏幕可視窗口(目前應該是 iPhone 7 Plus)疊加后得出的重疊區域.
c. Fill模式
Fill 模式,可以類比為?backgrouns-size: 100% 100%
?的表現,在這種模式下,不會保持寬高比,舞臺內容(gameArea)的寬高分別按照舞臺內容與瀏覽器可視窗口(window)的寬度比與高度比進行縮放,縮放至舞臺內容拉伸鋪滿窗口。
根據推導結論,簡單代碼實現如下:
// Fill模式核心原理函數 FILL: function(){ var self = this; self.radioX = (self.winWidth / self.stageWidth); self.radioY = (self.winHeight / self.stageHeight); self.canvasWidth = self.designWidth; self.canvasHeight = self.designHeight; }
這種模式下既不會留空,也不會被裁剪,但是在舞臺內容寬高比與瀏覽器可視窗口的寬高比不相等時,顯示的內容會有一定程度的拉伸形變。
這種暴力的處理方式雖然免去了留空和裁剪的煩惱,但是會存在拉伸形變,這就得看是否能夠被接受了。
d. Fixed-Width模式
區別于圖像,Canvas 是可以進行動態繪制大小的。所以,我們可以考慮根據屏幕窗口大小變化來動態繪制 Canvas。
從保持舞臺橫向內容不變的角度考慮,我們提出這樣的模式:舞臺內容(gameArea)等比進行縮放至與瀏覽器可視窗口的一致的寬度大小,而舞臺的高度(Canvas高度)進行重新繪制其高度為瀏覽器可視窗口的高度,稱之為 Fixed-Width 模式。
根據推導結論,簡單代碼實現如下:
// Fixed-Width模式核心原理函數 FIXED_WIDTH: function(){ var self = this; self.radioX = self.radioY = self.winWidth / self.designWidth; self.canvasWidth = self.designWidth; self.canvasHeight = self.winHeight / self.radioY; }
在 Fixed-Width 模式下,無論在什么分辨率下,舞臺橫向內容保持不變,而縱向高度則會動態裁補,這就會比較適用于那些場戲場景可以縱向拓展的 H5 輕互動類型。
e. Fixed-Height模式
說完 Fixed-Width 模式,換個角度考慮便得出 Fixed-Height 模式,舞臺內容(gameArea)等比進行縮放至與瀏覽器可視窗口的一致的高度大小,而舞臺的寬度(Canvas寬度)進行重新繪制其寬度為瀏覽器可視窗口的寬度。
根據推導結論,簡單代碼實現如下:
// Fixed-Height模式核心原理函數 FIXED_HEIGHT: function(){ var self = this; self.radioX = self.radioY= self.winHeight / self.designHeight; self.canvasWidth = self.winWidth / self.radioX; self.canvasHeight = self.designHeight; }
加入重定位和重繪制策略
綜合以上五種縮放模式,我們可以看到對于 Cover、Fixed-Width、Fixed-Height 模式而言,有存在被裁剪的可能性。特別是 Fixed-Height 模式,對于橫屏游戲來說這是比較常用的模式,但是在屏幕較小的時候難免會被裁剪,而且我們是不希望貼邊元素被裁剪掉的,譬如位于右上角的音樂圖標。而對于 Fixed-Width、Fixed—Height 模式,它們還存在舞臺區域需要補充繪制的情況,因此對某些舞臺元素來說需要重新設定其渲染大小。
所以,除了基本的縮放適配模式實現之外,為了解決貼邊元素不被裁剪以及對一些舞臺元素重繪制的需求,我們還需要加入兩個策略:重定位和重繪制。
a. 重定位
貼邊元素重定位策略的實現原理很簡單,對需要重新定位的元素對象額外設置?top
、left
、right
、bottom
?的自定義屬性(當然你可以命名為其他屬性名),這樣我們就可以在適配的時候根據這些自定義屬性以及實際顯示的 Canvas 大小進行重新計算位置。
為了保證性能,下面是策略里需要注意的地方:
- 在舞臺里,并不是所有游戲元素都是需要被重定位的,因此我們只需要創建一個數組記錄需要被重定位的元素。
- 適當控制重定位次數,我們不需要在每一幀 tick 繪制的時候都進行重定位,只需要在 Canvas 大小改變的時候進行處理。
以下是重定位策略相關的代碼:
// halfCutHeight、halfCutWidth是根據適配后的實際Canvas大小計算出來的相對距離 _setSize: function(){ // ... if(self.isPortrait) { // ... self.halfCutWidth = (self.canvasWidth * self.radioY - this.winWidth ) / 2 / self.radioY; self.halfCutHeight = (self.canvasHeight * self.radioX - this.winHeight) / 2 / self.radioX; }else { // ... self.halfCutWidth = (self.canvasWidth * self.radioX - this.winWidth ) / 2 / self.radioX; self.halfCutHeight = (self.canvasHeight * self.radioY - this.winHeight) / 2 / self.radioY; } // ... }, // 貼邊元素重定位核心處理函數 _adjustPosition: function(item){ var self = this; item && self.adjustPositionArr.push(item); self.adjustPositionArr.map(function(item, index, arr){ (typeof item.top == "number") && (item.y = item.top + self.halfCutHeight >= 0 ? self.halfCutHeight : 0); (typeof item.left == "number") && (item.x = item.left + self.halfCutWidth >= 0 ? self.halfCutWidth : 0); (typeof item.bottom == "number") && (item.y = self.canvasHeight - item.getBounds().height - item.bottom + self.halfCutHeight >= 0 ? self.halfCutHeight : 0); (typeof item.right == "number") && (item.x = self.canvasWidth - item.getBounds().width - item.right - self.halfCutWidth); }); }, // 暴露方法:提供給開發者記錄需要重定位的貼邊元素 adjustPosition: function(item){ var self = this; self._adjustPosition(item); }
b. 重繪制
對于一些以舞臺區域(gameArea)作為其大小設置的參考標準的元素,在適配時遇到需要補全繪制區域時,舞臺區域大小發生變化,相應地,該元素就需要進行重新繪制,這就是重繪制策略的存在意義。
同樣地,為了保證性能,重繪制策略也是同樣需要保證:
- 創建對應的數組記錄全顯圖形對象。
- 不在每一幀 tick 時進行重繪制,只在適配的時候重繪制。
以下是重繪制策略的相關代碼:
// 全顯圖形重繪制核心處理函數 _adjustFullSize: function(item){ var self = this; item && self.adjustFullSizeArr.push(item); self.adjustFullSizeArr.map(function(item, index, arr){ item.drawRect(0, 0, self.canvasWidth, self.canvasHeight); }); }, // 暴露方法:提供給開發者記錄需要重繪制的全顯圖形 adjustPosition: function(item){ var self = this; self._adjustPosition(item); }
至此,Canvas 橫屏適配問題才得以完全解決。
內容篇幅較長,簡單總結下,一個簡單的解決 Canvas 橫屏適配問題的方案至少需要包括兩點實現:
-
選用合適的縮放模式。
方案內置五種縮放模式,在實際應用中根據場景不同而采用不同的縮放進行適配。 -
加入重定位和重繪制策略。
為了保證貼邊元素不被裁剪以及舞臺元素動態渲染大小以適應舞臺區域的動態變化。
移動端判斷手機橫豎屏狀態
在做移動端開發的時候,在HTML頁面head中加入如下代碼
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1"/>
設置屏幕寬度為設備寬度,禁止用戶手動調整縮放,頁面初始縮放程度為1
CSS判斷橫屏還是豎屏
1.寫在同一個css文件中
@media screen and (orientation: portrait) { /*豎屏 css*/ } @media screen and (orientation: landscape) { /*橫屏 css*/ }
2.分開寫在2個CSS中
<!--豎屏--> <link rel="stylesheet" media="all and (orientation:portrait)" href="portrait.css"> <!--橫屏--> <link rel="stylesheet" media="all and (orientation:landscape)" href="landscape.css">
js判斷橫屏還是豎屏
//判斷手機橫豎屏狀態: window.addEventListener("onorientationchange" in window ? "orientationchange" : "resize", function() { if (window.orientation === 180 || window.orientation === 0) { //alert('豎屏狀態!'); } if (window.orientation === 90 || window.orientation === -90 ){ //alert('橫屏狀態!'); } }, false); //移動端的瀏覽器一般都支持window.orientation這個參數,通過這個參數可以判斷出手機是處在橫屏還是豎屏狀態。
?