- 屏幕旋轉與文檔方向?如何監聽文檔方向變化,如何兼容橫屏/豎屏模式下的樣式佈局以及實現強制橫屏展示canvas手寫電子簽名頁?
基於實踐問題,本文主要覆蓋以下至少點:
- 文檔方向與屏幕方向
- 不同端APP瀏覽器的橫屏預覽支持情況
- 移動端適配
- 橫屏佈局快速兼容
- canvas響應式適配屏幕旋轉帶來的問題以及兼容方法
移動端橫/豎屏模式下的電子簽名
屏幕方向於頁面方向
- 屏幕方向:手機的屏幕方向,一般未設置鎖定🔒屏幕方向情況下,屏幕方向會隨拿手機屏幕對應的方向(豎、橫、倒等)自動感應觸發旋轉
- 頁面方向:即網頁的方向,取決於app的webview是否設置了跟隨系統屏幕旋轉方向
一、背景
- 默認應用所有頁面只支持豎屏模式,未兼容豎屏模式的瀏覽和操作
- 有個電子簽名頁面,為方便用户操作,需要做了橫屏模式的佈局展示(兼容橫豎屏,無論橫豎屏下,簽字圖都需要是橫向顯示)
- 未考慮到橫屏模式,版本一的處理形式是基於固定的豎屏模式做了橫屏形式的佈局(絕大多數瀏覽器的網頁瀏覽並不會跟隨系統橫屏)
二、遇到的問題
- 在實際表現中發現,ios啓動自動旋轉屏幕模式時,在微信等瀏覽器中用户簽名操作時,會導致自動旋轉,導致樣式異常
- 在安卓中,啓動旋轉正常,網頁並不會默認進入橫屏瀏覽模式😯
三、網頁方向跟隨屏幕方向表現探索(忽略的冷知識)
- 針對不同的機型中部分app瀏覽器在手機橫屏模式下的網頁方向變化表現得出如下:
-
安卓(各app規則不一)
-
不會跟隨系統自動旋轉、不支持橫屏模式(可能我沒找到設置的地方)
- i.百度等大部分瀏覽器
-
可自定義設置
- 華為自帶瀏覽器、微信等其他瀏覽器
-
-
默認可隨系統設置進入橫屏模式
- 企業微信等
- IOS(比較統一):
- 微信、Safari等絕大部分瀏覽器都會跟隨系統,暫時未發現可自定義和不支持的
-
✧總結(各瀏覽器app在處理支持橫屏模式下獨立其行):
- ○在ios中基本上所有的app瀏覽器都支持自動跟隨系統橫屏預覽模式
- ○在安卓中不同app有不同的處理形式,比如微信內不支持橫屏、華為自帶瀏覽則是提供設置是否要跟隨系統
四、解決方案探索
(一)思路分析
- 強制橫屏:移動H5雖可以判斷橫豎屏,但考慮到多設備訪問問題(安卓多數app需要開啓橫屏模式,但入口比較深),所以強制橫屏顯示只能放棄;
- 強制豎屏模式:書寫簽名時必須要橫向輸入,除非能夠關閉手機橫屏模式(依賴用户操作-可兜底考慮彈窗提示)或者鎖定🔒網頁方向
- 橫豎屏兼容:通過視覺旋轉,或者橫豎屏樣式佈局兼容
(二)方案一鎖定網頁方向
1、針對特定的瀏覽器提供了設置可支持
<meta name=”screen-orientation” content=”portrait”> <!--uc強制豎屏-->
<meta name=”x5-orientation” content=”portrait”> <!--QQ強制豎屏-->
2、 screenOrientation API
- ✧文檔地址:https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation
- ✧對應JS中提供了相應監聽文檔方向變化的API,(其中包含鎖定與取消鎖定方向佈局)
-
✧lock() 與 unlock()
const oppositeOrientation = screen.orientation.type.startsWith("portrait") ? "landscape" : "portrait"; screen.orientation .lock(oppositeOrientation) .then(() => { log.textContent = `Locked to ${oppositeOrientation}\n`; }) .catch((error) => { log.textContent += `${error}\n`; }); - ✧可惜兼容性不容樂觀(❌)
(三)方案二:橫屏模式兼容
-
橫豎屏的樣式兼容包括普通DOM以及canvas畫布處理
通過rotate視覺旋轉方式,在測試過程中由於項目本身vw/vh移動適配,以及旋轉後畫布落筆精度等問題,相比直接佈局適配橫屏更為複雜,所以放棄此形式。
1. DOM的橫屏適配兼容
- ✧項目頁面採用的是VW方案進行移動端適配,由於 vw 單位的特性,適配換算大小是根據屏幕寬度而言的,因此屏幕寬度越大導致容器、文字會越大,還可能導致 DOM 元素超出屏幕外,並不是我們所想要的用户體驗
##### 1.1 換用移動端適配方案,如Flexible? - 對整體應用影響太大,並且Flexible方案實質主要是藉助JavaScript控制viewport的能力,使用rem模擬vw的特性從而達到適配目的的一套解決方案
- rem是基於根字體大小計算,其存在需要處理橫屏兼容問題
1.2 針對橫屏通過媒體查詢單獨設置vw\vh值的換算?
- ✓vw 單位的特點是適配換算大小時是根據屏幕寬度而定的,那麼在強制橫屏顯示時,就可以同理轉換為屏幕高度來而定,也就是 vw 單位替換成 vh 單位;
- ✓或者,基於當前使用的375 667 @2x設計稿尺寸(2倍圖7501334px),默認vw是按750px進行換算,在橫屏時使用1334px進行換算
-
✓插件postcss-px-to-viewport 發現是支持配置橫屏模式的換算規則
module.exports = { plugins: { 'postcss-px-to-viewport': { unitToConvert: 'px', // 需要轉換的單位 viewportWidth: 750, // 視口寬度,等同於設計稿寬度 unitPrecision: 5, // 精確到小數點後幾位 landscape: true, // 是否自動加入 @media (orientation: landscape),其中的屬性值是通過橫屏寬度來轉換的 - 開啓 landscapeUnit: 'vw', // 橫屏單位 landscapeWidth: 1334 // 橫屏寬度 } } } - ✓配置後所有的換算過的屬性都會同步加上橫屏模式的媒體查詢樣式:
- ✓樣式效果(修改後橫屏樣式好了很多,但部分模塊與豎屏相比還是未換算得體比較突兀):
-
進一步排查發現,上圖為轉化過來的部分是使用的第三方組件,組件庫在樣式抽離編譯時未配置
postcss-px-to-viewport的landscape模式適配或者使用的viewportWidth基準不一樣導致-
方案一:針對不同的文件做不同的配置
.postcssrc.jsconst path = require('path'); module.exports = ({ file }) => { const designWidth = file.dirname.includes(path.join('node_modules', 'vant')) ? 375 : 750; const designHeight = file.dirname.includes(path.join('node_modules', 'vant')) ? 667 : 1334; return { plugins: { 'postcss-px-to-viewport': { unitToConvert: 'px', // 需要轉換的單位 viewportWidth: designWidth, // 視口寬度,等同於設計稿寬度 unitPrecision: 5, // 精確到小數點後幾位 landscape: true, // 是否自動加入 @media (orientation: landscape),其中的屬性值是通過橫屏寬度來轉換的 - 開啓 landscapeUnit: 'vw', // 橫屏單位 landscapeWidth: designHeight // 橫屏寬度 } } } } -
方案二:針對尺寸屬性進行樣式覆蓋,以使用項目自定義的單位轉換規則,如
.page-product-sign { --alert-font-size: 24px; --alert-padding: 14px 30px; --button-font-size: 36px; .we-alert-body { min-height: 56px; } }
-
- 覆蓋適配後橫屏模式時樣式
2. 解決 Canvas 的橫屏適配問題
簽字板是響應式的,即canvas的css樣式寬高全屏鋪滿100%,而畫布尺寸width/height在初始化是確定(默認300*150),為了保持一致加載時會讀取容器尺寸複製給畫布大小進行初始化。
2.1 問題一:旋轉屏幕後簽名內容出現扭曲?
canvas元素可以使用CSS來定義大小,但在繪製時圖像會伸縮以適應它的框架尺寸:如果 CSS 的尺寸與初始畫布的比例不一致,它會出現扭曲;
![]()
-
處理手段:監聽屏幕旋轉,在網頁方向發生變化後重新更新canvas畫布的大小
// 設置 canvas 寬高 -- 重新計算 const { width, height } = this.$refs.canvasWrap.getBoundingClientRect() this.$refs.canvas.width = width this.$refs.canvas.height = height // 或者寬高轉換 const { width, height } = this.$refs.canvas this.$refs.canvas.width = height this.$refs.canvas.height = width2.2 問題二:旋轉後畫布內容被清空了?
改變canvas寬高時,發現畫布上的內容被清空了,這是因為canvas的大小改變後會自動清除內容的
2.2.1 方案一:在改變畫布大小前將內容保存,更新大小後重新繪製畫布
-
圖片旋轉參考:https://blog.csdn.net/frgod/article/details/106055830
/* 屏幕旋轉在更新畫布大小前處理 */ const { width, height } = canvas // 保存繪製的畫布內容 const imageData = context.getImageData(0, 0, width, height) /* 屏幕旋轉在更新畫布大小後處理 */ this.$refs.signatureBoard._initialize() // 旋轉圖片 -- 由於寬高不一樣需要旋轉後才能重繪進當前畫布 rotateImg = getRotateImage({ imageData }) // 重繪畫布 context.putImageData(rotateImg, 0, 0, 0, 0, height, width) // 圖片旋轉方法 function getRotateImage({ imageData }) { const { data, width, height } = imageData let rotateImg = new ImageData(height, width) let r = 0 let r1 = 0 // index of red pixel in old and new ImageData, respectively for (let y = 0, lenH = height; y < lenH; y++) { for (let x = 0, lenW = width; x < lenW; x++) { r = (x + lenW * y) * 4 r1 = (y + lenH * x) * 4 rotateImg.data[r1 + 0] = data[r + 0] rotateImg.data[r1 + 1] = data[r + 1] rotateImg.data[r1 + 2] = data[r + 2] rotateImg.data[r1 + 3] = data[r + 3] } } return rotateImg }2.2.2 方案二:繪製路徑時記錄path,圖片旋轉後對x/y進行替換並重新繪製
/** 觸摸移動繪製 --- 簽名組件內調整 */ onTouchMove(event) { const point = { isStart: false, x: event.srcEvent.clientX - this.rect.left, y: event.srcEvent.clientY - this.rect.top } this.paths.push(point) // 記錄下此時的繪製路徑 this.context.lineTo(point.x, point.y) this.context.stroke() } // 返回實例 getCurrentContext() { return Promise.resolve({ canvas: this.$refs.canvas, context: this.context, paths: this.paths }) } /** 業務項目 */ // 屏幕旋轉監聽函數 orientationChangeFn({ orientationType }) { // 旋轉後的圖片 this.$refs.signatureBoard.getCurrentContext().then(({ context, paths }) => { this.$refs.signatureBoard._initialize() // 重繪畫布 paths.forEach((point) => { // x/y進行替換 const temp = point.y point.y = point.x point.x = temp // 初始觸點 if (point.isStart) { context.moveTo(point.x, point.y) } else { // 移動觸點 context.lineTo(point.x, point.y) context.stroke() } }) } -
✧兩種方案都可行,針對簽名場景出於實現的複雜度以及數據處理性能,相對來説方案二較優(並且putImageData處理起來也不如直接繪製Why is putImageData so slow?)
2.2.3 問題三:旋轉更新canvas後簽名移動觸點錯位
-
觸摸是通過hammer.js實現,再加載時進行事件綁定和初始化
// 事件綁定 const hammertime = new Hammer(this.$refs.canvas) hammertime.get('pan').set({ direction: Hammer.DIRECTION_ALL, threshold: 1 }) hammertime.get('swipe').set({ enable: false }) hammertime.on('panstart', this.onTouchStart.bind(this)) hammertime.on('panmove', this.onTouchMove.bind(this)) -
猜測試由於canvas的大小變化,導致屏幕旋轉後未重新初始化綁定導致觸點出現與實際繪製錯位
- 解決方法,旋轉屏幕大小變化後對hammer.js重新進行綁定和初始化
五、文檔方向監聽
1、媒體查詢API(MediaQueryList)
- 定義和用法: matchMedia() 返回一個新的 MediaQueryList 對象,表示指定的媒體查詢字符串解析後的結果。matchMedia() 方法的值可以是任何一個 CSS @media 規則 的特性, 如 min-height, min-width, orientation 等。
-
MediaQueryList 對象有以下兩個屬性:
- media:查詢語句的內容。
- matches:用於檢測查詢結果,如果文檔匹配 media query 列表,值為 true,否則為 false
-
基本使用
this.mediaQueryList = window.matchMedia('screen and (orientation: portrait)') // 識別豎屏媒體查詢特性 this.mediaQueryList.addListener(() => { this.orientationType = this.mediaQueryList.matches ? 'portrait' : 'landscape' }) - 兼容性
- 文檔:https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList
2、設備方向監聽(orientationchange)
orientationchange事件在設備的縱橫方向改變時觸發
-
基本使用
/** 90或 - 90橫屏; 0或180豎屏 */ function orientationChange(event) { this.orientationType = [0, 180].includes(event?.orientation || window.orientation) ? 'portrait' : 'landscape' }; window.addEventListener("orientationchange",orientationChange); - 兼容性
- 文檔:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/orientationchange_event
3、resize 配合 ( window.innerWidth, window.innerHeight )
-
基本使用
window.addEventListener("resize", (event) => { const orientation = (window.innerWidth > window.innerHeight) ? "landscape" : "portrait"; if(orientation === 'portrait'){ console.log('豎屏'); } else { console.log('橫屏'); } }, false); -
缺點❌: 鍵盤等彈出都會受到干擾,且通過window.innerWidth > window.innerHeight只是一種偽檢測,有點不可靠
4、最終方案
-
考慮大部分主移動設備瀏覽器,優先orientationchange,不支持則使用matchMedia()
class OrientationChangeClass { constructor() { ... this._init() } _init() { if ('orientation' in window && 'onorientationchange' in window) { this.supportType = 'orientation' } else if ('matchMedia' in window) { this.supportType = 'matchMedia' this.mediaQueryList = window.matchMedia('screen and (orientation: portrait)') } this._listenChangeFn() } _listenChangeFn(event = {}) { switch (this.supportType) { case 'orientation': this.orientationType = [0, 180].includes(event?.orientation || window.orientation) ? 'portrait' : 'landscape' break case 'matchMedia': this.orientationType = this.mediaQueryList.matches ? 'portrait' : 'landscape' break } } addListener(callbackFn) { ... switch (this.supportType) { case 'orientation': window.addEventListener('orientationchange', this.changeBackfn, false) break case 'matchMedia': if (this.mediaQueryList?.addListener) { this.mediaQueryList.addListener(this.changeBackfn) } break } } removeListener() {} }六、遇到的異常
(一)設置橫屏模式轉化編譯報錯
- 報錯內容
-
報錯原因與解決方案
- 原因:不支持postcss 8.0+版本
- 解決:降低postcss版本為7.0或者使用
postcss-px-to-viewport-8-plugin
七、總結
-
大多數應用我們都不會考慮橫屏模式的兼容,默認豎屏瀏覽;針對需要兼容的頁面場景需要重點注意考慮以下
- 不同的移動端設備,以及APP瀏覽器是否支持橫屏以及是否支持跟隨系統旋轉模式表現不一(所以提示用户鎖定啥的有點不好找,ps-安卓有些瀏覽器沒找到設置項)
- 鎖定文檔方向,兼容性很差——起碼目前API等做不到
-
移動端屏幕旋轉佈局兼容:
- DOM兼容:可使用CSS媒體查詢單獨兩套樣式(postcss-px-to-viewport的橫屏配置實際也是這種模式);監聽橫屏變化做響應式處理
- canvas兼容:樣式尺寸以及畫布尺寸的區別、尺寸變化對繪製內容的還原、觸點的變化等需要考慮的細節較多
啓程新篇章~~