原文鏈接: 手寫一個 React 圖片預覽組件
前幾天打算給博客添加一個圖片預覽的效果,可在網上找了半天也沒找到合適的庫,于是自己干脆自己手寫了個,
最終實作效果如下:

實作原理
當滑鼠點擊圖片時生成一個半透明遮罩,并添加一個與點擊圖片位置大小都相同的圖片,之后通過 CSS 實作圖片的放大和居中,當再次點擊時,通過洗掉樣式實作圖片的回傳,
具體操作
添加遮罩和圖片
此處需要用到 ReactDom 的 createPortal() 方法,它可以將元素渲染到網頁中的指定位置,因為要考慮到圖片的回傳,所以圖片的位置不能用 getBoundingClientRect() 提供的相對于視圖視窗的坐標,而是要用到 offsetTop 和 offsetLeft 提供的相對于 offsetParent 的坐標,所以需要將遮罩和圖片渲染到 body 元素中,并且二者需要為同一級,具體實作代碼如下:
import { createPortal } from 'react-dom';
import { useState, useRef } from 'react';
function Mask({ props, setStatus, imgRef }) {
const close = () => {
setStatus(false);
};
return createPortal(
<div onClick={close} className='cursor-zoom-out'>
<div className='fixed bottom-0 left-0 right-0 top-0 bg-black/75'></div>
<img
{...props}
className='absolute'
style={{
top: imgRef.current.offsetTop,
left: imgRef.current.offsetLeft,
width: imgRef.current.offsetWidth,
height: imgRef.current.offsetHeight,
}}
/>
</div>,
document.body
);
}
export default function Img(props) {
const [status, setStatus] = useState(false);
const imgRef = useRef(null);
return (
<>
<img
{...props}
ref={imgRef}
className={`cursor-zoom-in ${status ? 'invisible' : ''}`}
onClick={() => {
setStatus(true);
}}
loading='lazy'
/>
{status && <Mask props={props} setStatus={setStatus} imgRef={imgRef} />}
</>
);
}
此時點擊圖片便會在 body 下生成一個遮罩和處在相同位置的圖片,再次點擊時則會關閉,

添加影片效果
影片效果主要由 CSS 中的 transition 和 transform 實作,而 transform 主要用到了其中的 scale() 和 translate 函式,
scale() 的數值為圖片縮放的倍數,我們需要將圖片盡量縮放到原先尺寸,但不能超出螢屏,所以要分別求出圖片寬度和高度的最大縮放倍數,之后對比取最小值,但在計算圖片目標尺寸時,需要與螢屏尺寸對比取最小值,
const scaleX = Math.min(naturalWidth, viewportWidth) / width;
const scaleY = Math.min(naturalHeight, viewportHeight) / height;
const scale = Math.min(scaleX, scaleY);
translate() 的數值為圖片在 X 和 Y 軸上的偏移量,我們需要將圖片偏移到螢屏中心,所以要求出圖片中心點距螢屏中心點的橫縱距離

const translateX = ((viewportWidth - width) / 2 - left) / scale;
const translateY = ((viewportHeight - height) / 2 - top) / scale;
具體計算函式如下
const calcFitScale = imgRef => {
const { top, left, width, height } = imgRef.current.getBoundingClientRect();
const { naturalWidth, naturalHeight } = imgRef.current;
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
const scaleX = Math.min(Math.max(width, naturalWidth), viewportWidth) / width;
const scaleY = Math.min(Math.max(height, naturalHeight), viewportHeight) / height;
const scale = Math.min(scaleX, scaleY);
const translateX = ((viewportWidth - width) / 2 - left) / scale;
const translateY = ((viewportHeight - height) / 2 - top) / scale;
return `scale(${scale}) translate(${translateX}px, ${translateY}px)`;
};
這里講一下為什么要在生成偏移量的時候除以縮放倍數,因為 CSS 中 transform 的執行是有先后順序的,圖片進行 scale() 縮放后其 translate() 的偏移距離也會發生變化,所以需要在計算時提前考慮,倘若要先進行偏移后進行縮放,則可以不考慮此因素,
const translateX = (viewportWidth - width) / 2 - left;
const translateY = (viewportHeight - height) / 2 - top;
return `translate(${translateX}px, ${translateY}px) scale(${scale})`;
最終代碼
最后加上一點滾動監聽,螢屏監聽,遮罩透明度變化即可得到最終函式
import { createPortal } from 'react-dom';
import { useState, useRef, useEffect } from 'react';
function Mask({ props, setStatus, imgRef }) {
const [transform, setTransform] = useState('');
const [opacity, setOpacity] = useState(0.7);
const close = () => {
setOpacity(0);
setTransform('');
setTimeout(() => {
setStatus(false);
}, 300);
};
useEffect(() => {
const handleResize = () => {
setTransform(calcFitScale(imgRef));
};
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
window.addEventListener('scroll', close);
return () => window.removeEventListener('scroll', close);
}, []);
return createPortal(
<div onClick={close} className="cursor-zoom-out">
<div
className="fixed bottom-0 left-0 right-0 top-0 bg-black"
style={{
opacity,
transition: 'opacity 300ms cubic-bezier(0.4, 0, 0.2, 1)',
}}
></div>
<img
{...props}
className="absolute"
style={{
transition: 'transform 300ms cubic-bezier(.2, 0, .2, 1)',
top: imgRef.current.offsetTop,
left: imgRef.current.offsetLeft,
width: imgRef.current.offsetWidth,
height: imgRef.current.offsetHeight,
transform: transform,
}}
/>
</div>,
document.body
);
}
export default function Img(props) {
const [status, setStatus] = useState(false);
const imgRef = useRef(null);
return (
<>
<img
{...props}
ref={imgRef}
className={`cursor-zoom-in ${status ? 'invisible' : ''}`}
onClick={() => {
setStatus(true);
}}
loading="lazy"
/>
{status && <Mask props={props} setStatus={setStatus} imgRef={imgRef} />}
</>
);
}
/**
* 計算圖片縮放比例
*/
const calcFitScale = imgRef => {
const margin = 5;
const { top, left, width, height } = imgRef.current.getBoundingClientRect();
const { naturalWidth, naturalHeight } = imgRef.current;
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
const scaleX = Math.min(Math.max(width, naturalWidth), viewportWidth) / width;
const scaleY = Math.min(Math.max(height, naturalHeight), viewportHeight) / height;
const scale = Math.min(scaleX, scaleY) - margin / Math.min(width, height) + 0.002;
const translateX = ((viewportWidth - width) / 2 - left) / scale;
const translateY = ((viewportHeight - height) / 2 - top) / scale;
return `scale(${scale}) translate3d(${translateX}px, ${translateY}px, 0)`;
};
transform 的初始值并沒有直接從 calcFitScale() 中獲取,而是通過在 useEffect() 進行賦值,因為如果一開始就給圖片定義了 transform ,則不會產生影片效果,
參考鏈接
Understanding translate after scale in CSS transforms
Why does order of transforms matter? rotate/scale doesn't give the same result as scale/rotate
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/555013.html
標籤:其他
下一篇:返回列表
