欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

针对大屏幕可视化的响应式处理,与 Goldmap 兼容

最编程 2024-04-02 14:29:19
...

最近开发一个大屏的项目,本来只考虑要投放的一块儿固定屏幕的分辨率,于是css单位直接写的px,后来有演示需求,要将屏幕投放到不同分辨率的屏幕里,需要做响应式处理。

flexible + px2rem-loader

首先尝试了flexible + px2rem-loader的方案,但是存在两个问题:

  • 文字被缩放太小后无效
    在原本7000px宽度下设置的50px的文字,如果在1000px屏幕上显示,理论上文字应该被压缩到50/7 = 7px,显示才是正常的。
    但是一般PC浏览器默认文字最小显示像素为12px,所以文字看起来就被放大了。

  • 在非css文件内写的px无法被转换
    行间样式、某些组件接收的px参数,无法被转换,显示也有问题。

transform: scale()

进入应用时监听屏幕变化,计算实际尺寸与设计尺寸的比例,使用transform给容器设置对应的缩放。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
      * {
        padding: 0;
        margin: 0;
        box-sizing: border-box;
      }
      #app {
        width: 7000px;
        height: 4000px;
        border: 10px solid red;
        padding: 30px;
        font-size: 100px;
        background-color: blue;
        margin: 0;
        color: #fff;
      }
    </style>
  </head>

  <body>
    <div id="app">hello world</div>

    <script>
      app.style.transformOrigin = '0 0'
      const resize = () => {
        const pageWidth = document.documentElement.clientWidth
        const defaultWidth = 7000 // 设计宽度
        const scale = pageWidth / 7000
        app.style.transform = `scale(${scale})`
      }
      window.addEventListener('resize', resize)
      resize()
    </script>
  </body>

</html>

设置之后感觉效果不错,直接实现了响应式。但是进一步使用还是有写问题要处理。

地图问题

大屏上一般都会放地图,我这里使用的高德地图,发现高德地图被scale缩放后,与地图交互时的位置会错乱,比如点击地图某个位置,地图实际响应的是另一个位置;使用滚轮缩放地图时中心点也会偏移。

高德地图是用canvas展示的,这个是在交互时获取鼠标位置的问题,在网上搜了一下地图scale后的点位偏差问题,试了些解决方案都无效。

最终我的解决方式是:

  • 将地图容器按照全局的scale再还原回去,位置偏移问题解决,但是地图显示较大
  • 获取地图宽度,根据scale计算地图当前实际应该显示的宽高像素值,直接修改地图的宽高属性。

即修改地图真实的宽高,来适配其他部分scale之后的大小。

2023-04-14 16.39.45.gif

观察上图响应式的过程,父容器app与地图容器mapscale值是反向变化的,地图容器的scale一直在抵消父容器的scale变化,同时不断改变自身宽高,来适应页面大小变化。在线示例

image.pngimage.png 可以看到,窗口变小时,父级容器appscale在变小,而地图容器mapscale在变大进行抵消。

源码地址

关键代码:

<body>
  <div id="app">
    <div class="text">调整页面窗口大小,文字通过父级scale被自动缩放,地图自动调整宽高自适应</div>
    <div id="map"></div>
  </div>

  <script>
    new AMap.Map('map');
    const map = document.getElementById('map')
    const app = document.getElementById('app')

    app.style.transformOrigin = '0 0'
    map.style.transformOrigin = '0 0'

    let scale = 1
    let preScale = 1 // 记录上次scale值,以便map做处理

    const resize = () => {
      const pageWidth = document.documentElement.clientWidth
      const defaultWidth = 7000 // 设计宽度
      preScale = scale
      scale = pageWidth / 7000
      app.style.transform = `scale(${scale})` // 使用scale对父级进行响应式
      resizeMap()
    }

    function resizeMap () {
      const {
        offsetWidth,
        offsetHeight
      } = map

      // 每次响应式,根据上次缩放比例计算是地图的设计宽高 offsetWidth / preScale
      const newWidth = (offsetWidth / preScale) * scale;
      const newHeight = (offsetHeight / preScale) * scale;

      map.style.transform = `scale(${1 / scale})` // 抵消父级的scale,还原map容器
      // 改变map容器真实宽高来适应屏幕变化
      map.style.width = newWidth + "px"
      map.style.height = newHeight + "px"
    }

    window.addEventListener('resize', resize)
    resize()
  </script>
</body>

我的项目是使用的vue,这里我封装了一个指令:

import type { App } from 'vue'
import { useMainStore } from '@/stores'

interface ScaleElement extends HTMLElement {
  oldValue?: number
}

function handleScale(el: ScaleElement, scale: number) {
  const oldScale = el.oldValue || 1
  el.oldValue = scale
  if (!scale || scale === oldScale) return

  const { offsetWidth, offsetHeight } = el

  const newWidth = (offsetWidth / oldScale) * scale
  const newHeight = (offsetHeight / oldScale) * scale

  el.style.transform = `scale(${1 / scale})`
  el.style.transformOrigin = '0 0'
  el.style.width = newWidth + 'px'
  el.style.height = newHeight + 'px'
}

export default {
  install(app: App) {
    app.directive('fixScale', {
      mounted: (el) => {
        const store = useMainStore()
        handleScale(el, store.scale)
        store.$subscribe(() => {
          // 将屏幕resize时计算出的scale值存到store中,监听scale值变化
          const scale = store.scale
          handleScale(el, scale)
        })
      }
    })
  }
}

给地图容器添加指令:

<template>
  <div v-fixScale></div>
</template>

另外需要注意的是,此时地图因为没有了整体缩放,所以内部的设置了绝对像素的内容就需要手动根据scale进行处理了。

需要插入到body下的组件处理

某些组件为了避免父级层级较低的影响,会有将组件插入到body下的需求,如Dialog组件的append-to-body, 如果直接插入到body下,就脱离了缩放容器的控制,无法响应式了,因此我们将这种需要传送的组件插入到应用的根节点(#app)下,而不是body下。

这种传送操作在React(createPortal)Vue3(Portal)都是直接支持的,我的项目是vue2,采用了portal-vue的方案。

  1. 安装并注册portal-vue
import PortalVue from "portal-vue";
Vue.use(PortalVue);

2. 在根节点下标记传送的目标位置:

<div id="app">
  <portal-target name="app-container" multiple></portal-target>
  <router-view />
</div>

3. 传送组件到根节点:

<portal to="app-container">
  <Dialog></Dialog>
</portal>