针对大屏幕可视化的响应式处理,与 Goldmap 兼容
最近开发一个大屏的项目,本来只考虑要投放的一块儿固定屏幕的分辨率,于是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之后的大小。
观察上图响应式的过程,父容器app
与地图容器map
的scale
值是反向变化的,地图容器的scale
一直在抵消父容器的scale变化,同时不断改变自身宽高,来适应页面大小变化。在线示例
可以看到,窗口变小时,父级容器app
的scale
在变小,而地图容器map
的scale
在变大进行抵消。
源码地址
关键代码:
<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的方案。
- 安装并注册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>
上一篇: 比例积分调节