本文是《前端大屏原理系列》第五篇:流畅的自适应预览。
本系列所有技术点,均经过本人开源项目 react-big-screen 实际应用,欢迎 star (*´▽`)ノノ.
一、效果演示
二、原理简介
使用前端可拖拽大屏开发应用,一般期望所见即所得。所以需要固定页面的宽高比,在页面大小改变时同步缩放页面组件,以保持预览区域整体布局不变。我们可以设计这样一个自适应容器,专门负责处理预览区域的缩放和定位,用来支持在不同尺寸屏幕上显示。即:页面布局不变,撑满容器。总共有3种显示情景:
1比1缩放
,直接计算宽度与设计宽度的比例即可(或高度与设计高度的比例)。
内部容器两边先触底
,计算宽度与设计宽度的比例。
内部容器上下先触底
,计算高度与设计高度的比例。
上述可以做到布局保持不变并撑满容器。但想要实现 流畅过渡,需要尽量减小节流(throttle)间隔或完全取消,并且开启GPU加速。例如:transform: translate3d(...)、will-change 等。
三、源码实现
首先封装 React-Hook useResizeDom
,负责处理容器元素的resize监听、卸载,并执行callback回调函数。此处使用ResizeObserver
来实现,ResizeObserver 很适合用来监视元素盒或边框盒或SVG元素边界尺寸的变化。
// 监听dom大小变化
import { RefObject, useEffect, useRef } from "react";
function useResizeDom(domRef: RefObject<HTMLElement>, callback: ResizeObserverCallback) {
// 保存实时回调函数,避免闭包
const callbackRef = useRef<ResizeObserverCallback>(callback);
callbackRef.current = callback;
useEffect(() => {
if (!domRef.current) {
return;
}
const resizeObserver = new ResizeObserver(
(entries: ResizeObserverEntry[], observer: ResizeObserver) => {
callbackRef?.current?.(entries, observer);
},
);
resizeObserver.observe(domRef.current);
return () => {
resizeObserver.disconnect();
};
}, []);
}
如何实现自适应容器?我们可以通过比较内部元素宽高比
、容器宽高比
来计算缩放比例。两者相等,1:1缩放,此时缩放比率为 内部元素宽度 / 设计宽度(或 内部元素高度 / 设计高度)。如果内部元素宽高比较大,则其宽度撑满容器,缩放比例为 内部元素宽度 / 设计宽度。反之,高度撑满容器。缩放比例为 内部元素高度 / 设计高度。(小技巧:哪边先撑满就用哪边计算)
// 大屏适配容器
import React, { useMemo, useRef } from "react";
import styles from "./index.module.less";
import classNames from "classnames";
import { useResizeDom } from "@/hooks";
export interface FitScreenProps {
dw?: number; // 设计宽度 (例如:1920)
dh?: number; // 设计高度 (例如:1080)
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
bodyClassName?: string;
bodyStyle?: React.CSSProperties;
}
export default function FitScreen(props: FitScreenProps) {
const { dw = 1920, dh = 1080 } = props;
const containerDomRef = useRef<HTMLDivElement>(null); // 容器dom
const domRef = useRef<HTMLDivElement>(null); // 画布 dom
const widthHeightRate = useMemo(() => dw / dh, [dw, dh]); // 画布宽高比
useResizeDom(containerDomRef, () => {
if (!containerDomRef.current || !domRef.current) {
return;
}
const containerRect = containerDomRef.current.getBoundingClientRect();
const containerWidthHeightRate = containerRect.width / containerRect.height;
// 如果容器宽高比 > 画布宽高比,则画布高度占满容器。
// 如果容器宽高比 < 画布宽高比,则画布宽度占满容器。
if (containerWidthHeightRate > widthHeightRate) {
// 高度对齐
const scale = containerRect.height / dh;
const width = scale * dw;
// 子元素距离左侧距离
const left = (containerRect.width - width) / 2;
domRef.current.style.transform = `scale(${scale}) translate3d(${left / scale}px, 0, 0)`;
} else {
// 宽度对齐
const scale = containerRect.width / dw;
domRef.current.style.transform = `scale(${scale})`;
}
domRef.current.style.width = `${dw}px`;
domRef.current.style.height = `${dh}px`;
});
return (
<div className={props?.className} ref={containerDomRef} style={props?.style}>
<div
className={classNames(props?.bodyClassName, styles.fitScreenBody)}
style={props?.bodyStyle}
ref={domRef}
>
{props?.children}
</div>
</div>
);
}
// index.module.less
.fitScreenBody {
transform-origin: left top; // 转换点(左上角)
position: relative;
background: white;
}
使用示例:
export default function Page() {
return (
<FitScreen
dw={1920}
dh={1080}
style={{
top: 0,
left: 0,
width: '100%',
height: '100%',
position: 'fixed',
background: 'black'
}}
>
{/* 此处渲染预览组件 */}
<div style={{ width: '100%', height: '100%', background: 'white' }}>
Hello Screen!
</div>
</FitScreen>
)
}
【前端大屏原理系列】
react-big-screen 是一个从0到1使用React开发的前端拖拽大屏开源项目。此系列将对大屏的关键技术点一一解析。包含了:拖拽系统实现、自定义组件、收藏夹、快捷键、可撤销历史记录、加载远程组件/本地组件、自适应预览页、布局容器组件、多组件联动(基于事件机制)、成组/取消成组、多子页面切换、i18n国际化语言、鼠标范围框选、... ... 等等。
演示地址:点击访问