React實現(xiàn)圖片縮放的示例代碼
前言
用 React 實現(xiàn)圖片縮放的功能。當(dāng)拖動圖片上四個角會沿著 x 軸放大和縮小圖片。其中圖片縮放的核心功能抽成 hook 來使用,需要把 hook 提供的 ref 設(shè)置到需要改變大小的節(jié)點的,當(dāng)寬度改變時會改變 width 的 state 。
流程圖
hook 代碼
useImageResizer.tsx
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; // 改成一個期望的最小圖片寬度常量 import { FLOAT_IMAGE_MINI_WIDTH } from 'src/constants/view'; import './image-resizer.less'; export enum ResizeDirection { LeftTop, LeftBottom, RightTop, RightBottom, } export interface ImageResizerProps { isActive: boolean; handleResizeStart?: (direction: ResizeDirection) => void; handleResizeEnd?: () => void; } export const useImageResizer = ({ isActive, handleResizeStart, handleResizeEnd }: ImageResizerProps) => { const [imageWith, setImageWidth] = useState(0); const [divElement] = useState<HTMLDivElement>(document.createElement('div')); const isDraggingRef = useRef(false); // 是否在拖動 const [isDragging, setIsDragging] = useState(false); const imageContainerRef = useRef<HTMLDivElement>(null); const handleMouseMove = useCallback( (e: MouseEvent, resizeDirection: ResizeDirection, startX: number, imageWidth: number) => { // 計算移動距離 const moveX = (e.clientX - startX) * 1; // 根據(jù) resize 方向計算新的寬度 let newWidth = imageWidth; switch (resizeDirection) { case ResizeDirection.LeftTop: case ResizeDirection.LeftBottom: newWidth -= moveX; break; case ResizeDirection.RightTop: case ResizeDirection.RightBottom: newWidth += moveX; break; default: break; } // 設(shè)置新的寬度 setImageWidth(Math.max(newWidth, FLOAT_IMAGE_MINI_WIDTH)); }, [] ); const handleMouseDown = useCallback( (e: React.MouseEvent, resizeDirection: ResizeDirection) => { isDraggingRef.current = true; const startX = e.clientX; const imageWidth = imageContainerRef.current?.offsetWidth || 0; setIsDragging(true); handleResizeStart?.(resizeDirection); const handleMouseMoveFun = (e: MouseEvent) => { handleMouseMove(e, resizeDirection, startX, imageWidth); }; const handleMouseUpFun = () => { isDraggingRef.current = false; setIsDragging(false); handleResizeEnd?.(); // 移除監(jiān)聽 window.removeEventListener('mousemove', handleMouseMoveFun); window.removeEventListener('mouseup', handleMouseUpFun); }; window.addEventListener('mousemove', handleMouseMoveFun); window.addEventListener('mouseup', handleMouseUpFun); }, [handleResizeStart, handleMouseMove, handleResizeEnd] ); const Resizer = useMemo( () => ( <> <div className="image-block__resizer-left-top" onMouseDown={e => handleMouseDown(e, ResizeDirection.LeftTop)} ></div> <div className="image-block__resizer-left-bottom" onMouseDown={e => handleMouseDown(e, ResizeDirection.LeftBottom)} ></div> <div className="image-block__resizer-right-top" onMouseDown={e => handleMouseDown(e, ResizeDirection.RightTop)} ></div> <div className="image-block__resizer-right-bottom" onMouseDown={e => handleMouseDown(e, ResizeDirection.RightBottom)} ></div> </> ), [handleMouseDown] ); useEffect(() => { // 把 resizer 渲染到 divElement 上 divElement.setAttribute('class', 'image-block__resizer'); ReactDOM.render(Resizer, divElement); // 初始化 imageWidth setImageWidth(imageContainerRef.current?.offsetWidth || 0); }, [Resizer, divElement]); useEffect(() => { if (!isActive) { if (imageContainerRef.current?.contains(divElement)) { // 移除 imageContainerRef 上的 resizer imageContainerRef.current?.removeChild(divElement); } } else { setImageWidth(imageContainerRef.current?.offsetWidth || 0); // 往 imageContainerRef 上添加 resizer imageContainerRef.current?.appendChild(divElement); } }, [Resizer, divElement, imageContainerRef, isActive]); return { imageContainerRef, imageWith, isDraggingRef, isDragging }; };
image-resizer.less
.image-block__resizer { div { box-sizing: border-box; position: absolute; width: 12px; height: 12px; border: 2px solid rgb(255, 255, 255); box-shadow: rgba(0, 0, 0, .2) 0px 0px 4px; border-radius: 50%; z-index: 1; touch-action: none; background: rgb(30, 111, 255); } .image-block__resizer-left-top { left: 0px; top: 0px; margin-left: -6px; margin-top: -6px; cursor: nwse-resize; } .image-block__resizer-right-top { right: 0px; top: 0px; margin-right: -6px; margin-top: -6px; cursor: nesw-resize; } .image-block__resizer-right-bottom { right: 0px; bottom: 0px; margin-right: -6px; margin-bottom: -6px; cursor: nwse-resize; } .image-block__resizer-left-bottom { left: 0px; bottom: 0px; margin-left: -6px; margin-bottom: -6px; cursor: nesw-resize; } } .image-resize-dragging { cursor: move; user-select: none; }
使用
index.tsx
因為圖片縮放是在某個特定區(qū)域內(nèi)進行的,所以會有圖片最大寬高檢測的邏輯,保證圖片放大到特定寬度后不再放大。還增加了 delete 鍵的監(jiān)聽。 imagePosition 是圖片對于固定原點的位置信息。
import React, { useState, useCallback, useRef, useMemo, CSSProperties, useEffect, KeyboardEvent } from 'react'; import { useImageResizer, ResizeDirection } from './useImageResizer'; import { PAGE_WIDTH } from 'src/constants/view'; import './index.less'; export interface FloatImage { image: string; width?: number; height?: number; top?: number; left?: number; } export interface FloatImagesProps { data: FloatImage[]; pageHeight: number; } interface FloatImageBlockProps { data: FloatImage; pageHeight: number; } interface ImagePosition { top?: number; left?: number; bottom?: number; right?: number; } export const FloatImageBlock: React.FC<FloatImageBlockProps> = ({ data, pageHeight }) => { const { image, width, top = 0, left = 0 } = data; const [isActive, setIsActive] = useState(false); // 圖片位置 const [imagePosition, setImagePositionState] = useState<ImagePosition>({ top, left, }); const imagePositionRef = useRef<ImagePosition>(imagePosition); // 圖片最大寬高 const [imageMaxSize, setImageMaxSize] = useState<{ maxWidth?: number; maxHeight?: number }>({}); const imageRef = useRef<HTMLImageElement>(null); const setImagePosition = useCallback((position: ImagePosition) => { setImagePositionState(position); imagePositionRef.current = position; }, []); const getImageInfo = useCallback(() => { const imageWidth = imageRef.current?.offsetWidth || 0; const imageHeight = imageRef.current?.offsetHeight || 0; return { imageWidth, imageHeight }; }, []); const getPositionInfo = useCallback( ({ top, left, bottom = 0, right = 0 }: { top?: number; left?: number; bottom?: number; right?: number }) => { const { imageWidth, imageHeight } = getImageInfo(); const newTop = top === undefined ? pageHeight - bottom - imageHeight : top; const newLeft = left === undefined ? PAGE_WIDTH - right - imageWidth : left; return { top: newTop, left: newLeft }; }, [getImageInfo, pageHeight] ); const updateImageData = useCallback(() => { const { imageWidth, imageHeight } = getImageInfo(); const { top, left, right = 0, bottom = 0 } = imagePositionRef.current; // 根據(jù) imagePosition 和圖片寬高獲取最新的 top 和 left const newTop = top === undefined ? pageHeight - bottom - imageHeight : top; const newLeft = left === undefined ? PAGE_WIDTH - (right + imageWidth) : left; setImagePosition({ top: newTop, left: newLeft }); }, [getImageInfo, pageHeight, setImagePosition]); const handleResizeStart = useCallback( (direction: ResizeDirection) => { // 根據(jù) imagePosition 計算 top 和 left const { top: newTop, left: newLeft } = getPositionInfo(imagePositionRef.current); const { imageWidth, imageHeight } = getImageInfo(); let maxWidth: number | undefined; let maxHeight: number | undefined; let newImagePosition: ImagePosition = {}; // 根據(jù) direction 設(shè)置 imagePosition switch (direction) { case ResizeDirection.LeftTop: newImagePosition = { bottom: pageHeight - newTop - imageHeight, right: PAGE_WIDTH - newLeft - imageWidth }; maxWidth = imageWidth + newLeft; maxHeight = imageHeight + newTop; break; case ResizeDirection.LeftBottom: newImagePosition = { top: newTop, right: PAGE_WIDTH - newLeft - imageWidth }; maxWidth = imageWidth + newLeft; maxHeight = pageHeight - newTop; break; case ResizeDirection.RightTop: newImagePosition = { bottom: pageHeight - newTop - imageHeight, left: newLeft }; maxWidth = PAGE_WIDTH - newLeft; maxHeight = imageHeight + newTop; break; case ResizeDirection.RightBottom: newImagePosition = { top: newTop, left: newLeft }; maxWidth = PAGE_WIDTH - newLeft; maxHeight = pageHeight - newTop; break; default: break; } if (maxWidth === undefined || maxHeight === undefined) { return; } setImagePosition(newImagePosition); // 按照圖片寬高比計算最大寬高 maxWidth = Math.min((maxHeight / imageHeight) * imageWidth, maxWidth); maxHeight = Math.min(pageHeight, (maxWidth / imageWidth) * imageHeight); if (maxWidth < maxHeight) { setImageMaxSize({ maxWidth, maxHeight: maxWidth * (imageHeight / imageWidth) }); } else { setImageMaxSize({ maxHeight, maxWidth: maxHeight * (imageWidth / imageHeight) }); } }, [getImageInfo, getPositionInfo, pageHeight, setImagePosition] ); const handleResizeEnd = useMemo( () => // 設(shè)置 active 為 true () => { const timer = setTimeout(() => { setIsActive(true); updateImageData(); clearTimeout(timer); }, 0); }, [updateImageData] ); const { imageContainerRef, imageWith, isDragging, } = useImageResizer({ isActive, handleResizeStart, handleResizeEnd }); const handleClick = useCallback(() => { setIsActive(true); }, []); const style = useMemo( (): CSSProperties => ({ position: 'absolute', zIndex: isActive ? 101 : 100, ...imagePosition, }), [imagePosition, isActive] ); // 增加 delete 事件 const deleteHandler = (e: KeyboardEvent<HTMLDivElement>) => { if (isActive && (e.key === 'Backspace' || e.key === 'Delete')) { // 刪除圖片 } }; useEffect(() => { const handler = (e: MouseEvent) => { // 點擊的區(qū)域不在圖片上 if (!imageContainerRef.current?.contains(e.target as Node)) { setIsActive(false); } }; window.addEventListener('mousedown', handler); return () => { window.removeEventListener('mousedown', handler); }; }, [isActive, imageContainerRef]); return ( <> {( <div className={'image-block-container'} style={style} onClick={handleClick} onKeyDown={deleteHandler} tabIndex={0} > <div ref={imageContainerRef} className="image-block"> <img ref={imageRef} src={image} style={{ width: `${imageWith || width}px`, ...imageMaxSize }} alt="" /> <div className={`image-block-mask ${isActive ? 'image-block-mask-active' : ''} ${isDragging ? 'image-block-mask-dragging' : '' }`} ></div> </div> </div> )} </> ); };
index.less
.image-block-container { display: flex; justify-content: center; align-items: center; .image-block { position: relative; display: flex; user-select: none; max-width: 100%; max-height: 100%; img { max-width: 100%; max-height: 100%; } .image-block-mask { position: absolute; height: 100%; width: 100%; top: 0; left: 0; } .image-block-mask-active { background: rgba(30, 111, 255, .12); cursor: move; } .image-block-mask-dragging::before { content: ''; width: 10000px; height: 10000px; position: absolute; top: -5000px; left: -5000px; cursor: move; } } }
到此這篇關(guān)于React實現(xiàn)圖片縮放的示例代碼的文章就介紹到這了,更多相關(guān)React圖片縮放內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React?中使用?RxJS?優(yōu)化數(shù)據(jù)流的處理方案
這篇文章主要為大家介紹了React?中使用?RxJS?優(yōu)化數(shù)據(jù)流的處理方案示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02詳解React開發(fā)中使用require.ensure()按需加載ES6組件
本篇文章主要介紹了詳解React開發(fā)中使用require.ensure()按需加載ES6組件,非常具有實用價值,需要的朋友可以參考下2017-05-05React 項目中動態(tài)設(shè)置環(huán)境變量
本文主要介紹了React 項目中動態(tài)設(shè)置環(huán)境變量,本文將介紹兩種常用的方法,使用 dotenv 庫和通過命令行參數(shù)傳遞環(huán)境變量,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04基于React.js實現(xiàn)兔兔牌九宮格翻牌抽獎組件
這篇文章主要為大家詳細(xì)介紹了如何基于React.js實現(xiàn)兔兔牌九宮格翻牌抽獎組件,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-01-01react?native?reanimated實現(xiàn)動畫示例詳解
這篇文章主要為大家介紹了react?native?reanimated實現(xiàn)動畫示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03