React实现图片缩放的示例代码
前言
用 React 实现图片缩放的功能。当拖动图片上四个角会沿着 x 轴放大和缩小图片。其中图片缩放的核心功能抽成 hook 来使用,需要把 hook 提供的 ref 设置到需要改变大小的节点的,当宽度改变时会改变 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; // 根据 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; } // 设置新的宽度 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?.(); // 移除监听 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
因为图片缩放是在某个特定区域内进行的,所以会有图片最大宽高检测的逻辑,保证图片放大到特定宽度后不再放大。还增加了 delete 键的监听。 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; // 根据 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) => { // 根据 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 = {}; // 根据 direction 设置 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( () => // 设置 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) => { // 点击的区域不在图片上 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; } } }
到此这篇关于React实现图片缩放的示例代码的文章就介绍到这了,更多相关React图片缩放内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
详解React Native 采用Fetch方式发送跨域POST请求
这篇文章主要介绍了详解React Native 采用Fetch方式发送跨域POST请求,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧2017-11-11react使用antd-design中select不能及时刷新问题及解决
这篇文章主要介绍了react使用antd-design中select不能及时刷新问题及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2024-03-03在react中使用highlight.js将页面上的代码高亮的方法
本文通过 highlight.js 库实现对文章正文 HTML 中的代码元素自动添加语法高亮,具有一定的参考价值,感兴趣的可以了解一下2022-01-01react中使用better-scroll滚动插件的实现示例
滚动在很多地方都可以使用,本文主要介绍了react中使用better-scroll滚动插件的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2023-07-07
最新评论