【随记】实现元素自适应缩放

元素自适应常用的解决方案,大致有两种:

  1. 使用vhvw 来设置容器的宽高
  2. 使用js控制容器的缩放

第一种:使用vhvw 来设置容器的宽高:这种方案,他只能控制容器的宽高比例,不能保证缩放后元素的空间大小

而且即时用了remem等字体像素,也不能控制元素极小、或极大缩放,因为remem受父级的字体大小限制,而字体大小受浏览器厂商限制,有最小字体大小限制(例如谷歌,最小只能12像素)

所以此方案并不适用容器的自适应缩放

第二种:使用JS 控制容器缩放:上文遇到的问题是,在缩放后不能控制容器的空间大小,所以容器的实际宽高肯定是不能动的,而且要使用px作为单位

那么又要缩放,又要宽高不能动,那要使用什么进行缩放呢?

  • 那就是transform 样式属性,它可以对元素进行旋转、位移、缩放,且不需要改变元素的实际宽高

所以我们只需要监听父级容器的宽高变化,计算需要缩放比和位移距离,即可实现容器的自适应缩放,那么我们监听父级容器变化怎么监听呢

  1. onResize:他的作用是监听window 的大小变化,但缺点是不能监听任意子元素的变化
  2. 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;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Space Chars

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值