本教學操作環境:Windows10系統、react18.0.0版、Dell G3電腦。
react怎麼實作縮放?
react圖片縮放、平移(position、transform實作)
很多網頁都會給文案附上一些圖片補充描述,例如在說到地址時,會在旁邊附上一張地圖,並在地圖上標註該地址。如果附上的圖片很小,很難看清楚地址的具體訊息,有些產品經理會為圖片設計一個可以平移、放大縮小的功能。本文將一一實現上述功能。
廢話不多說,先給上效果圖:
主要三個功能點:
- 圖片平移
- 圖片縮放
- 車站標註
圖片平移
圖片平移可以監聽這三個事件實現:onMouseDown、onMouseMove、onMouseUp
。 onMouseDown
事件記錄每次滑鼠按下的座標位置;onMouseMove
事件計算出每次平移的距離,該距離加上拖曳前圖片距離父元素的距離就等於拖曳後圖片相對於父元素的距離;onMouseUp
事件觸發時,登出或不讓執行onMouseDown、onMouseMove
事件,防止只要滑鼠移入圖片就會平移。
這三個事件需要阻止瀏覽器的預設行為,不然在移動時會自動開啟圖片。
const WIDTH = 1200;const HEIGHT = 900;const DynamicStyle= () => { const imgRef = React.createRef<HTMLImageElement>(); /** 图片样式 */ const [imgStyle, setImgStyle] = useState<React.CSSProperties>({}); /** 记录鼠标是否按下 */ const [mouseDowmFlag, setMouseDowmFlag] = useState(false); /** 记录鼠标按下的坐标 */ const [mouseDowmPos, setMouseDowmPos] = useState<{x: number, y: number}>({x: 0, y: 0}) /** 鼠标可视区域时,重置鼠标按下的布尔值为false */ useEffect(() => { document.onmouseover = () => { if (mouseDowmFlag) { setMouseDowmFlag(false); } }; return () => { document.onmouseover = null; }; }, [mouseDowmFlag]) /** 平移 */ const handleMouseDown = (event: React.MouseEvent<HTMLImageElement>) => { const { clientX, clientY } = event; event.stopPropagation(); event.preventDefault(); // 阻止浏览器默认行为,拖动会打开图片 setMouseDowmFlag(true); // 控制只有在鼠标按下后才会执行mousemove setMouseDowmPos({ x: clientX, y: clientY, }); }; const handleMouseMove = (event: React.MouseEvent<HTMLImageElement>) => { event.stopPropagation(); event.preventDefault(); const { clientX, clientY } = event; const diffX = clientX - mouseDowmPos.x; const diffY = clientY - mouseDowmPos.y; if (!mouseDowmFlag || (diffX === 0 && diffY === 0)) return; const { offsetLeft, offsetTop } = imgRef.current as HTMLImageElement; const offsetX = parseInt(`${diffX + offsetLeft}`, 10); const offsetY = parseInt(`${diffY + offsetTop}`, 10); setMouseDowmPos({ x: clientX, y: clientY, }); setImgStyle({ ...imgStyle, left: offsetX, top: offsetY, }); }; const handleMouseUp = (event: React.MouseEvent<HTMLImageElement>) => { event.stopPropagation(); event.preventDefault(); setMouseDowmFlag(false); }; return ( <div className={styles.imgArea}> <img src={mapImg} alt='part' ref={imgRef} height={HEIGHT} style={imgStyle} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} > </img> </div> ) }
圖片縮放
圖片縮放可以監聽onWheel
事件,事件物件event
有一個記錄滾輪滾動的屬性deltaY
,當向上捲動時deltaY,向下捲動時<code>deltaY>0
。每次滾動修改其縮放的比例,同時更改transform
樣式按比例進行縮放。
const WIDTH = 1200;const HEIGHT = 900;const SCALE = 0.2;const DynamicStyle= () => { const imgRef = React.createRef<HTMLImageElement>(); /** 初始化缩放比例,默认为1 */ const [rate, setRate] = useState(1); /** 图片样式 */ const [imgStyle, setImgStyle] = useState<React.CSSProperties>({}); /** 记录鼠标是否按下 */ const [mouseDowmFlag, setMouseDowmFlag] = useState(false); /** 记录鼠标按下的坐标 */ const [mouseDowmPos, setMouseDowmPos] = useState<{x: number, y: number}>({x: 0, y: 0}) /** 图片现在大小 */ const [initial, setInitial] = useState<{width: number, height: number}>({width: WIDTH, height: HEIGHT}); useEffect(() => { const { naturalWidth, naturalHeight, width, height } = imgRef.current as HTMLImageElement; setInitial({ width, height }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // console.log(natural, initial) useEffect(() => { document.onmouseover = () => { if (mouseDowmFlag) { setMouseDowmFlag(false); } }; return () => { document.onmouseover = null; }; }, [mouseDowmFlag]) /** 缩放 */ const handleWheelImage = (event: React.WheelEvent<HTMLImageElement>) => { // 向上为负,向下为正 const bigger = event.deltaY > 0 ? -1 : 1; // transform偏移量 const transformX = -initial.width / 2; const transformY = -initial.height / 2; if (bigger > 0 && rate < 2) { const enlargeRate = rate + SCALE; setImgStyle({ ...imgStyle, transform: `matrix(${enlargeRate}, 0, 0, ${enlargeRate}, ${transformX}, ${transformY})`, // 默认以图片中心为原点进行缩放 }); setRate(enlargeRate); } else if (bigger < 0 && rate > 1) { const shrinkRate = rate - SCALE; setImgStyle({ ...imgStyle, transform: `matrix(${shrinkRate}, 0, 0, ${shrinkRate}, ${transformX}, ${transformY})`, }); setRate(shrinkRate); } } /** 平移 */ const handleMouseDown = (event: React.MouseEvent<HTMLImageElement>) => { const { clientX, clientY } = event; event.stopPropagation(); event.preventDefault(); // 阻止浏览器默认行为,拖动会打开图片 setMouseDowmFlag(true); // 控制只有在鼠标按下后才会执行mousemove setMouseDowmPos({ x: clientX, y: clientY, }); }; const handleMouseMove = (event: React.MouseEvent<HTMLImageElement>) => { event.stopPropagation(); event.preventDefault(); const { clientX, clientY } = event; const diffX = clientX - mouseDowmPos.x; const diffY = clientY - mouseDowmPos.y; if (!mouseDowmFlag || (diffX === 0 && diffY === 0)) return; const { offsetLeft, offsetTop } = imgRef.current as HTMLImageElement; const offsetX = parseInt(`${diffX + offsetLeft}`, 10); const offsetY = parseInt(`${diffY + offsetTop}`, 10); setMouseDowmPos({ x: clientX, y: clientY, }); setImgStyle({ ...imgStyle, left: offsetX, top: offsetY, }); }; const handleMouseUp = (event: React.MouseEvent<HTMLImageElement>) => { event.stopPropagation(); event.preventDefault(); setMouseDowmFlag(false); }; return ( <div className={styles.imgArea}> <img src={mapImg} alt='part' height={HEIGHT} style={imgStyle} ref={imgRef} onWheel={handleWheelImage} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} > </img> </div> ) }
.imgArea { position: relative; width: 1200px; height: 900px; margin: auto; border: 1px solid #da2727; overflow: hidden; & > img { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); cursor: move; } }
如果沒有設定transformOrigin
,預設是相對於圖片中心進行縮放,但是在初始為了讓圖片在可視區域內水平垂直居中,使用了transform: translate(-50%, -50%);
,因此為了縮放時相對於圖片中心點,需要設定matrix
的第5、6個參數矯正transformOrigin
,transform: matrix(${shrinkRate}, 0, 0, ${shrinkRate}, ${transformX}, ${transformY})
車站標註
首先,定義一個常數表示圖示的座標,這個座標是相對於原始圖片左上角的定位。
const imgInfo = { lableLeft: "1900", lableTop: "2000",}
這裡,解釋一下原始圖的概念:
隨便在網路上查看一個圖片元素,例如上面。 1200 x 900是頁面定的圖片大小,但圖片還有一個真實大小4535 x 3402。
要計算圖示在沒有平移縮放時的初始座標之前,需要算出圖片的縮放比例(不是上面的rate
):
/** 图片原始大小,默认设置为1是防止计算图片原始大小与初始大小比例出现无穷大 */const [natural, setNatural] = useState<{width: number, height: number}>({width: 1, height: 1});/** 图片现在大小 */const [initial, setInitial] = useState<{width: number, height: number}>({width: WIDTH, height: HEIGHT});useEffect(() => { const { naturalWidth, naturalHeight, width, height } = imgRef.current as HTMLImageElement; setNatural({ width: naturalWidth, height: naturalHeight }); setInitial({ width, height }); // eslint-disable-next-line react-hooks/exhaustive-deps}, []) // 初始图片缩放比例(图片有原始的图片大小)const imgScaleRateX = initial.width / natural.width;const imgScaleRateY = initial.height / natural.height;
圖示初始的座標就可以計算出:
const labelLeft = parseInt(`${imgInfo.lableLeft}`, 10) * imgScaleRateX;const labelTop = parseInt(`${imgInfo.lableTop}`, 10) * imgScaleRateY;
當圖片平移時,圖示也需要跟著平移,這是的座標計算:
// 图标相对父元素坐标 = 图标位置坐标 + 图片坐标const labelLeft = parseInt(`${imgInfo.lableLeft}`, 10) * imgScaleRateX + Number(imgStyle.left || WIDTH / 2); const labelTop = parseInt(`${imgInfo.lableTop}`, 10) * imgScaleRateY + Number(imgStyle.top || HEIGHT / 2);
當圖片縮放時,圖示需要隨著圖片一起縮放。如果沒有設定圖示transformOrigin
,預設時相對圖示的中心縮放的。為了確保圖示隨著圖片一起縮放,那就必須讓圖片和圖示的縮放參考原點相同,圖示的transformOrigin
應該設定為相對於圖片原點的距離。
const labelTransformOrigin = () => { return `${initial.width / 2 - Number(imgInfo.lableLeft) * imgScaleRateX}px ${ initial.height / 2 - Number(imgInfo.lableTop) * imgScaleRateY }px`; }
整體程式碼範例:
const imgInfo = { lableLeft: "1900", lableTop: "2000", } const WIDTH = 1200; const HEIGHT = 900; const SCALE = 0.2; const DynamicStyle= () => { const imgRef = React.createRef<HTMLImageElement>(); /** 初始化缩放比例,默认为1 */ const [rate, setRate] = useState(1); /** 图片样式 */ const [imgStyle, setImgStyle] = useState<React.CSSProperties>({}); /** 记录鼠标是否按下 */ const [mouseDowmFlag, setMouseDowmFlag] = useState(false); /** 记录鼠标按下的坐标 */ const [mouseDowmPos, setMouseDowmPos] = useState<{x: number, y: number}>({x: 0, y: 0}) /** 图片原始大小,默认设置为1是防止计算图片原始大小与初始大小比例出现无穷大 */ const [natural, setNatural] = useState<{width: number, height: number}>({width: 1, height: 1}); /** 图片现在大小 */ const [initial, setInitial] = useState<{width: number, height: number}>({width: WIDTH, height: HEIGHT}); useEffect(() => { const { naturalWidth, naturalHeight, width, height } = imgRef.current as HTMLImageElement; setNatural({ width: naturalWidth, height: naturalHeight }); setInitial({ width, height }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { document.onmouseover = () => { if (mouseDowmFlag) { setMouseDowmFlag(false); } }; return () => { document.onmouseover = null; }; }, [mouseDowmFlag]) /** 缩放 */ const handleWheelImage = (event: React.WheelEvent<HTMLImageElement>) => { // 向上为负,向下为正 const bigger = event.deltaY > 0 ? -1 : 1; // transform偏移量 const transformX = -initial.width / 2; const transformY = -initial.height / 2; if (bigger > 0 && rate < 2) { const enlargeRate = rate + SCALE; setImgStyle({ ...imgStyle, transform: `matrix(${enlargeRate}, 0, 0, ${enlargeRate}, ${transformX}, ${transformY})`, // 默认以图片中心为原点进行缩放 }); setRate(enlargeRate); } else if (bigger < 0 && rate > 1) { const shrinkRate = rate - SCALE; setImgStyle({ ...imgStyle, transform: `matrix(${shrinkRate}, 0, 0, ${shrinkRate}, ${transformX}, ${transformY})`, }); setRate(shrinkRate); } } /** 平移 */ const handleMouseDown = (event: React.MouseEvent<HTMLImageElement>) => { const { clientX, clientY } = event; event.stopPropagation(); event.preventDefault(); // 阻止浏览器默认行为,拖动会打开图片 setMouseDowmFlag(true); // 控制只有在鼠标按下后才会执行mousemove setMouseDowmPos({ x: clientX, y: clientY, }); }; const handleMouseMove = (event: React.MouseEvent<HTMLImageElement>) => { event.stopPropagation(); event.preventDefault(); const { clientX, clientY } = event; const diffX = clientX - mouseDowmPos.x; const diffY = clientY - mouseDowmPos.y; if (!mouseDowmFlag || (diffX === 0 && diffY === 0)) return; const { offsetLeft, offsetTop } = imgRef.current as HTMLImageElement; const offsetX = parseInt(`${diffX + offsetLeft}`, 10); const offsetY = parseInt(`${diffY + offsetTop}`, 10); setMouseDowmPos({ x: clientX, y: clientY, }); setImgStyle({ ...imgStyle, left: offsetX, top: offsetY, }); }; const handleMouseUp = (event: React.MouseEvent<HTMLImageElement>) => { event.stopPropagation(); event.preventDefault(); setMouseDowmFlag(false); }; // 初始图片缩放比例(图片有原始的图片大小) const imgScaleRateX = initial.width / natural.width; const imgScaleRateY = initial.height / natural.height; const labelTransformOrigin = () => { return `${initial.width / 2 - Number(imgInfo.lableLeft) * imgScaleRateX}px ${ initial.height / 2 - Number(imgInfo.lableTop) * imgScaleRateY }px`; } /** 图标位置计算 */ const labelStyle = (): React.CSSProperties => { const transformX = -initial.width / 2; const transformY = -initial.height / 2; // 图标相对父元素坐标 = 图标初始位置坐标 + 平移量 const labelLeft = parseInt(`${imgInfo.lableLeft}`, 10) * imgScaleRateX + Number(imgStyle.left || WIDTH / 2); const labelTop = parseInt(`${imgInfo.lableTop}`, 10) * imgScaleRateY + Number(imgStyle.top || HEIGHT / 2); return { left: labelLeft, top: labelTop, transformOrigin: labelTransformOrigin(), transform: `matrix(${rate}, 0, 0, ${rate}, ${transformX}, ${transformY})`, } } return ( <div className={styles.imgArea}> <img src={mapImg} alt='part' height={HEIGHT} style={imgStyle} ref={imgRef} onWheel={handleWheelImage} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} > </img> <span className={styles.label} style={labelStyle()}></span> </div> ) }
推薦學習:《react影片教學》