头图
  • 屏幕旋转与文档方向?如何监听文档方向变化,如何兼容横屏/竖屏模式下的样式布局以及实现强制横屏展示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事件在设备的纵横方向改变时触发

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兼容:样式尺寸以及画布尺寸的区别、尺寸变化对绘制内容的还原、触点的变化等需要考虑的细节较多

启程新篇章~~

keywords
4.8k 声望2.1k 粉丝

新篇章,起航!