元素自适应常用的解决方案,大致有两种:
- 使用
vh
、vw
来设置容器的宽高 - 使用js控制容器的缩放
第一种:使用vh
、vw
来设置容器的宽高:这种方案,他只能控制容器的宽高比例,不能保证缩放后元素的空间大小
而且即时用了rem
、em
等字体像素,也不能控制元素极小、或极大缩放,因为rem
、em
受父级的字体大小限制,而字体大小受浏览器厂商限制,有最小字体大小限制(例如谷歌,最小只能12像素)
所以此方案并不适用容器的自适应缩放
第二种:使用JS 控制容器缩放:上文遇到的问题是,在缩放后不能控制容器的空间大小,所以容器的实际宽高肯定是不能动的,而且要使用px
作为单位
那么又要缩放,又要宽高不能动,那要使用什么进行缩放呢?
- 那就是
transform
样式属性,它可以对元素进行旋转、位移、缩放,且不需要改变元素的实际宽高
所以我们只需要监听父级容器的宽高变化,计算需要缩放比和位移距离,即可实现容器的自适应缩放,那么我们监听父级容器变化怎么监听呢
onResize
:他的作用是监听window 的大小变化,但缺点是不能监听任意子元素的变化ResizeObserver
:他可以监听document
中任意一个元素的大小变化(推荐
)
缩放和监听都解决了,那就是具体代码了,如下文代码:
import { type ObjectPlugin } from "vue";
interface PageDirectiveOption {
width?: number;
height?: number;
}
class PageDirective {
private target: HTMLElement;
private options: PageDirectiveOption;
private observer?: ResizeObserver;
private scale = 1;
private offset = [0, 0];
private static instances: PageDirective[] = [];
constructor(el: HTMLElement, options: PageDirectiveOption) {
this.target = el;
this.options = options;
//初始化容器缩放
this.resetStyle();
PageDirective.instances.push(this);
}
private getParentSize() {
const parentNode = this.target.parentElement;
const width = parentNode?.offsetWidth || 0,
height = parentNode?.offsetHeight || 0;
return [width, height];
}
private resetStyle() {
//设置容器宽高
if (this.options.width) {
this.target.style.width = this.options.width + "px";
}
if (this.options.height) {
this.target.style.height = this.options.height + "px";
}
this.target.style.transform = `scale(${this.scale}) translate(${this.offset[0]}px,${this.offset[1]}px)`;
}
private onResizeByWidth() {
if (!this.options.width) return;
const containerSize = this.getParentSize();
const diffWidth = (containerSize[0] - this.options.width) / 2;
this.scale = containerSize[0] / this.options.width;
const diffHeight =
(this.target.offsetHeight * this.scale - this.target.offsetHeight) /
2;
this.offset = [diffWidth / this.scale, diffHeight / this.scale];
}
private onResizeByHeight() {
if (!this.options.height) return;
const containerSize = this.getParentSize();
const diffHeight = (containerSize[1] - this.options.height) / 2;
this.scale = containerSize[1] / this.options.height;
const diffWidth =
(this.target.offsetWidth * this.scale - this.target.offsetWidth) /
2;
this.offset = [diffWidth / this.scale, diffHeight / this.scale];
}
private onResizeByWidthAndHeight() {
if (!this.options.width || !this.options.height) return;
const containerSize = this.getParentSize();
//计算容器比例和视图比例
const containerRadio = containerSize[0] / containerSize[1];
const viewRadio = this.options.width / this.options.height;
// 如果容器比例大于视图比例,说明高度比发生了变化,则只需要计算高度比,相反,只需要计算宽度比
if (containerRadio > viewRadio) {
this.scale = containerSize[1] / this.options.height;
} else if (containerRadio < viewRadio) {
this.scale = containerSize[0] / this.options.width;
} else {
this.scale = 1;
}
//获取比例差值,用来计算位移距离
const diffRadio = 1 == this.scale ? 0 : (1 - this.scale) / 2;
const offset =
diffRadio == 0
? [0, 0]
: [
~(this.options.width * diffRadio),
~(this.options.height * diffRadio),
];
//如果容器比例大于视图比,那么说明高度发生了变化 高度自适应后的显示高度对于容器而言,肯定是100%,所以这里只需要计算宽度的位移距离即可
if (containerRadio > viewRadio) {
offset[0] =
offset[0] +
(containerSize[0] - this.options.width * this.scale) / 2;
}
//如果容器比例小于视图比,那么说明宽度发生了变化 宽度自适应后的显示宽度对于容器而言,肯定是100%,所以这里只需要计算高度的位移距离即可
if (containerRadio < viewRadio) {
offset[1] =
offset[1] +
(containerSize[1] - this.options.height * this.scale) / 2;
}
//这里计算计算真实位移像素是因为:transform-scale 缩放是相对于视图容器的实际宽高缩放,不是相对于容器的实际宽高,所以这里需要计算偏移后的实际距离
if (offset[0] != 0) {
offset[0] /= this.scale;
}
if (offset[1] != 0) {
offset[1] /= this.scale;
}
this.offset = offset;
}
onResie() {
//根据配置的宽高,计算缩放比例、位移距离
if (this.options.width && !this.options.height) {
this.onResizeByWidth();
} else if (this.options.height && !this.options.width) {
this.onResizeByHeight();
} else {
this.onResizeByWidthAndHeight();
}
this.resetStyle();
}
watch() {
//监听容器大小变化
this.observer = new ResizeObserver(() => {
this.onResie();
});
this.observer.observe(this.target.parentNode as HTMLElement, {});
}
destory() {
this.observer?.disconnect();
PageDirective.instances = PageDirective.instances.filter(
(item) => item.target != this.target,
);
}
static getInstance(el: HTMLElement) {
return PageDirective.instances.find((item) => item.target == el);
}
}
/**
* 指令式
*/
export default {
install(app) {
app.directive("scale", {
mounted(el: HTMLElement, binding) {
new PageDirective(el, {
width: binding.value?.width,
height: binding.value?.height,
}).watch();
},
updated(el) {
PageDirective.getInstance(el)?.onResie();
},
beforeUnmount(el) {
PageDirective.getInstance(el)?.destory();
},
});
},
} as ObjectPlugin;