大屏项目适配你怎么看

512 阅读3分钟

适配什么?

让内容适配屏幕大小

比如:

  • 1920 * 1080 的设计稿,要在: 1440 * 900 的屏幕显示
  • 1920 * 1080 的设计稿,要在: 4k屏幕显示

如何适配

对原始内容进行放大或缩小,使其符合当前屏幕的分辨率

如: 设计稿的宽度是 1920,屏幕的宽度是 1440, 1440/1920 = 0.75, 那将宽度缩小到原始大小的0.75倍,就是1440的大小

设计稿的高度是 1080,屏幕的宽度是 900, 900/1080 = 0.8333333, 那将宽度缩小到原始大小的0.8333333倍,就是900的大小

适配的两种形式

等比缩放

所谓等比缩放,就是宽高都按照同一个比例缩放,特点是,如果屏幕比例和设计稿比例一致,那内容就能充满屏幕,否则,可能会上下留白或左右留白

拉伸充满

所谓拉伸充满,就是分别计算高度和宽度的缩放比,然后宽高用各自的缩放比去缩放,特点是,无论屏幕比例是否与设计稿一致,内容都能充满屏幕,而不会留白,但内容可能会被拉伸

具体实现代码

scale-view.ts

import { debounce } from 'lodash-unified'
export interface ScaleViewOption {
  /**
   * 将哪个html的dom元素设置为缩放元素. 默认:body
   */
  el?: string | HTMLElement
  /**
   * el的定位方式. 默认: absolte
   */
  position?: 'absolute' | 'fixed'
  /**
   * 设计稿尺寸
   * 默认: width: 1920 height: 1080
   */
  designSize?: {
    /**
     * 设计稿宽度
     */
    width: number
    /**
     * 设计稿高度
     */
    height: number
  }
  /**
   * 视口比例与设计稿比例不一致时,是否强制使内容拉伸铺满(不会留白,但内容会被拉伸变形). 默认: true
   */
  fitFull?: boolean
}
export class ScaleView {
  private el: HTMLElement = document.body
  private designSize = { width: 1920, height: 1080 }
  private fitFull = true
  private position: 'absolute' | 'fixed' = 'absolute'
  constructor(option: ScaleViewOption = {}) {
    const {
      el,
      designSize = { width: 1920, height: 1080 },
      fitFull = true,
      position = 'absolute'
    } = option
    if (typeof el === 'string') {
      const dom = document.querySelector<HTMLElement>(el)
      if (!dom) {
        throw `通过 【${el}】 未匹配到dom元素`
      }
      this.el = dom
    } else if (el) {
      this.el = el
    }
    this.position = position
    this.designSize = designSize
    this.fitFull = fitFull
    this.init()
  }
  destory() {
    window.removeEventListener('resize', this.updateScale)
    this.el.style.width = ''
    this.el.style.height = ''
    this.el.style.left = ''
    this.el.style.top = ''
    this.el.style.position = ''
    this.el.style.transform = ''
    this.el.style.transformOrigin = ''
  }
  private init() {
    // 必须设置body的overflow = 'hidden',否则,body会因为临时的窗口大小变化而出现滚动条(最终实际不应该出现,但中间过程出现了),导致后续document.documentElement.clientWidth,document.documentElement.clientHeight不包含滚动条的大小
    document.body.style.overflow = 'hidden'
    // 将元素的大小定义为设计稿的大小
    this.el.style.height = `${this.designSize.height}px`
    this.el.style.width = `${this.designSize.width}px`
    // 将transfrom变化的源点设置为左上顶点(默认为:中心点)
    this.el.style.transformOrigin = '0 0'
    this.el.style.position = this.position
    // this.el.style.overflow = 'hidden'

    this.updateScale()
    const updateScaleDebounce = debounce(this.updateScale, 300, { trailing: true })
    window.addEventListener('resize', updateScaleDebounce.bind(this))
  }
  private updateScale() {
    console.log('updateScale')
    const { widthScale, heightScale, minScale } = this.computeScale()
    if (this.fitFull) {
      // 为了使内容充满视口, 需要分别对宽高进行缩放
      this.el.style.transform = `scale(${widthScale},${heightScale})`
      // 因为是拉伸铺满模式,所以将元素的起始位置设置为视口的左上顶点
      this.el.style.left = '0'
      this.el.style.top = '0'
    } else {
      // 因为是等比缩放模式,所以将元素的位置设置为上下都居中的位置
      this.el.style.left = '50%'
      this.el.style.top = '50%'
      // 等比缩放则只要使用宽度缩放比和高度缩放比中较小的那个设置为宽高的公共缩放比即可,translate3d(-50%, -50%, 0) 用于实现元素居中
      this.el.style.transform = `scale(${minScale}) translate3d(-50%, -50%, 0)`
    }
  }
  computeScale() {
    const { width: viewPortWidth, height: viewPortHeight } = this.getViewPortSize()
    // 计算宽度缩放比
    const widthScale = this.computedScale(viewPortWidth, this.designSize.width)
    // 计算高度缩放比
    const heightScale = this.computedScale(viewPortHeight, this.designSize.height)
    // 如果是等比缩放,则需要使用最小的缩放比进行缩放,避免内容被拉伸
    const minScale = Math.min(widthScale, heightScale)
    return { widthScale, heightScale, minScale }
  }
  private computedScale(realSize: number, designSize: number) {
    return realSize / designSize
  }
  private getViewPortSize() {
    const width = document.documentElement.clientWidth
    const height = document.documentElement.clientHeight
    console.log('视口大小', width, height)
    return { width, height }
  }
}

使用

<template>
  <div class="h-full flex flex-row flex-wrap">
    <div class="w-1/2 h-1/2 p-5 bg-orange-300">
      <EchartView :echartsOptionBuilder="barEchartsOptionBuilder"></EchartView>
    </div>
    <div class="w-1/2 h-1/2 p-5 bg-green-300 text-2xl">烧结合格率</div>
    <div class="w-1/2 h-1/2 p-5 bg-blue-300">autofit</div>
    <div class="w-1/2 h-1/2 p-5 bg-pink-300">4</div>
  </div>
</template>

<script setup lang="ts">
import { ScaleView } from '@/components/scale-view'
import { barEchartsOptionBuilder } from '@/echarts-option-builder/BarEchartsOptionBuilder'

new ScaleView()
</script>

<style scoped></style>

适配只有这一种方案吗?

上面是通过transfrom: scale(x)方案适配,特点是,你页面内容元素的大小,可以完全用设计稿的px值设置,无需额外转换,也没有浏览器字体最小12px的限制(但建议保证缩放之后,浏览器实际最小字体大于等于12px,否则会看不清),该方案的缺点也是有的,就是有时候会导致界面字体模糊,部分地图组件存在热点事件偏移问题

其他方案:

  • 通过vw/vh适配:原理类似,但实现繁琐,但也有优点,就是不会导致字体模糊,也不会有热点事件偏移问题
  • rem适配:原理类似,但实现繁琐,且只能进行等比适配,屏幕比例与设计稿比例不一致,会留白,但也有优点,就是不会导致字体模糊,也不会有热点事件偏移问题

优秀的开源组件

autofit.js:该组件算法略有不同,但原理一致,但感觉该组件的算法不如本文提到的算法直观。