博客 / 詳情

返回

移動端橫/豎屏模式下的電子簽名兼容

  • 屏幕旋轉與文檔方向?如何監聽文檔方向變化,如何兼容橫屏/豎屏模式下的樣式佈局以及實現強制橫屏展示canvas手寫電子簽名頁?
  • 基於實踐問題,本文主要覆蓋以下至少點:

    • 文檔方向與屏幕方向
    • 不同端APP瀏覽器的橫屏預覽支持情況
    • 移動端適配
    • 橫屏佈局快速兼容
    • canvas響應式適配屏幕旋轉帶來的問題以及兼容方法

移動端橫/豎屏模式下的電子簽名

屏幕方向於頁面方向

  • 屏幕方向:手機的屏幕方向,一般未設置鎖定🔒屏幕方向情況下,屏幕方向會隨拿手機屏幕對應的方向(豎、橫、倒等)自動感應觸發旋轉
  • 頁面方向:即網頁的方向,取決於app的webview是否設置了跟隨系統屏幕旋轉方向

一、背景

  • 默認應用所有頁面只支持豎屏模式,未兼容豎屏模式的瀏覽和操作
  • 有個電子簽名頁面,為方便用户操作,需要做了橫屏模式的佈局展示(兼容橫豎屏,無論橫豎屏下,簽字圖都需要是橫向顯示)
  • 未考慮到橫屏模式,版本一的處理形式是基於固定的豎屏模式做了橫屏形式的佈局(絕大多數瀏覽器的網頁瀏覽並不會跟隨系統橫屏)

二、遇到的問題

  • 在實際表現中發現,ios啓動自動旋轉屏幕模式時,在微信等瀏覽器中用户簽名操作時,會導致自動旋轉,導致樣式異常
  • 在安卓中,啓動旋轉正常,網頁並不會默認進入橫屏瀏覽模式😯

三、網頁方向跟隨屏幕方向表現探索(忽略的冷知識)

  • 針對不同的機型中部分app瀏覽器在手機橫屏模式下的網頁方向變化表現得出如下:
  • 安卓(各app規則不一)

    • 不會跟隨系統自動旋轉、不支持橫屏模式(可能我沒找到設置的地方)

      • i.百度等大部分瀏覽器
    • 可自定義設置

      • 華為自帶瀏覽器、微信等其他瀏覽器
  • 默認可隨系統設置進入橫屏模式

    • 企業微信等
  • IOS(比較統一):
  • 微信、Safari等絕大部分瀏覽器都會跟隨系統,暫時未發現可自定義和不支持的
  • ✧總結(各瀏覽器app在處理支持橫屏模式下獨立其行):

    • ○在ios中基本上所有的app瀏覽器都支持自動跟隨系統橫屏預覽模式
    • ○在安卓中不同app有不同的處理形式,比如微信內不支持橫屏、華為自帶瀏覽則是提供設置是否要跟隨系統

四、解決方案探索

(一)思路分析

  1. 強制橫屏:移動H5雖可以判斷橫豎屏,但考慮到多設備訪問問題(安卓多數app需要開啓橫屏模式,但入口比較深),所以強制橫屏顯示只能放棄;
  2. 強制豎屏模式:書寫簽名時必須要橫向輸入,除非能夠關閉手機橫屏模式(依賴用户操作-可兜底考慮彈窗提示)或者鎖定🔒網頁方向
  3. 橫豎屏兼容:通過視覺旋轉,或者橫豎屏樣式佈局兼容

(二)方案一鎖定網頁方向

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-viewportlandscape模式適配或者使用的viewportWidth基準不一樣導致

    • 方案一:針對不同的文件做不同的配置.postcssrc.js

      const 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 = width
    
    2.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() {}
    }

    六、遇到的異常

    (一)設置橫屏模式轉化編譯報錯

  • 報錯內容
    image.png
  • 報錯原因與解決方案

    • 原因:不支持postcss 8.0+版本
    • 解決:降低postcss版本為7.0或者使用postcss-px-to-viewport-8-plugin

    七、總結

  • 大多數應用我們都不會考慮橫屏模式的兼容,默認豎屏瀏覽;針對需要兼容的頁面場景需要重點注意考慮以下

    1. 不同的移動端設備,以及APP瀏覽器是否支持橫屏以及是否支持跟隨系統旋轉模式表現不一(所以提示用户鎖定啥的有點不好找,ps-安卓有些瀏覽器沒找到設置項)
    2. 鎖定文檔方向,兼容性很差——起碼目前API等做不到
    3. 移動端屏幕旋轉佈局兼容:

      1. DOM兼容:可使用CSS媒體查詢單獨兩套樣式(postcss-px-to-viewport的橫屏配置實際也是這種模式);監聽橫屏變化做響應式處理
      2. canvas兼容:樣式尺寸以及畫布尺寸的區別、尺寸變化對繪製內容的還原、觸點的變化等需要考慮的細節較多

啓程新篇章~~
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.