React 实现图片缩放
前言
用 React 实现图片缩放的功能。当拖动图片上四个角会沿着 x 轴放大和缩小图片。其中图片缩放的核心功能抽成 hook 来使用,需要把 hook 提供的 ref 设置到需要改变大小的节点的,当宽度改变时会改变 width 的 state 。
流程图
组件使用 hook
hook 往 ref 节点四个角上挂载拖拽元素和增加鼠标监听事件
鼠标拖拽缩放节点,触发监听事件通知外部宽度变化
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;
}
}
}
转载自:https://juejin.cn/post/7287256531498090551