Skip to content

Commit 46b1e7c

Browse files
authored
feat: added mobile touch event Interaction for Images (#286)
* feat: add touch zoom * feat: optimize touch center point scaling * feat: optimize touch image adhesion to edges and center point calculation * test: add previewTouch * chore: correct variable name * chore: remove useless code logic
1 parent f6b728d commit 46b1e7c

6 files changed

Lines changed: 550 additions & 132 deletions

File tree

src/Operations.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const Operations: React.FC<OperationsProps> = props => {
119119
icon: zoomOut,
120120
onClick: onZoomOut,
121121
type: 'zoomOut',
122-
disabled: scale === minScale,
122+
disabled: scale <= minScale,
123123
},
124124
{
125125
icon: zoomIn,

src/Preview.tsx

Lines changed: 28 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import type { DialogProps as IDialogPropTypes } from 'rc-dialog';
33
import Dialog from 'rc-dialog';
44
import addEventListener from 'rc-util/lib/Dom/addEventListener';
55
import KeyCode from 'rc-util/lib/KeyCode';
6-
import { warning } from 'rc-util/lib/warning';
76
import React, { useContext, useEffect, useRef, useState } from 'react';
87
import { PreviewGroupContext } from './context';
9-
import getFixScaleEleTransPosition from './getFixScaleEleTransPosition';
108
import type { TransformAction, TransformType } from './hooks/useImageTransform';
119
import useImageTransform from './hooks/useImageTransform';
10+
import useMouseEvent from './hooks/useMouseEvent';
11+
import useTouchEvent from './hooks/useTouchEvent';
1212
import useStatus from './hooks/useStatus';
1313
import Operations from './Operations';
14-
import { BASE_SCALE_RATIO, WHEEL_MAX_SCALE_RATIO } from './previewConfig';
14+
import { BASE_SCALE_RATIO } from './previewConfig';
1515

1616
export type ToolbarRenderInfoType = {
1717
icons: {
@@ -126,24 +126,35 @@ const Preview: React.FC<PreviewProps> = props => {
126126
} = props;
127127

128128
const imgRef = useRef<HTMLImageElement>();
129-
const downPositionRef = useRef({
130-
deltaX: 0,
131-
deltaY: 0,
132-
transformX: 0,
133-
transformY: 0,
134-
});
135-
const [isMoving, setMoving] = useState(false);
136129
const groupContext = useContext(PreviewGroupContext);
137130
const showLeftOrRightSwitches = groupContext && count > 1;
138131
const showOperationsProgress = groupContext && count >= 1;
132+
const [enableTransition, setEnableTransition] = useState(true);
139133
const { transform, resetTransform, updateTransform, dispatchZoomChange } = useImageTransform(
140134
imgRef,
141135
minScale,
142136
maxScale,
143137
onTransform,
144138
);
145-
const [enableTransition, setEnableTransition] = useState(true);
146-
const { rotate, scale, x, y } = transform;
139+
const { isMoving, onMouseDown, onWheel } = useMouseEvent(
140+
imgRef,
141+
movable,
142+
visible,
143+
scaleStep,
144+
transform,
145+
updateTransform,
146+
dispatchZoomChange,
147+
);
148+
const { isTouching, onTouchStart, onTouchMove, onTouchEnd } = useTouchEvent(
149+
imgRef,
150+
movable,
151+
visible,
152+
minScale,
153+
transform,
154+
updateTransform,
155+
dispatchZoomChange,
156+
);
157+
const { rotate, scale } = transform;
147158

148159
const wrapClassName = classnames({
149160
[`${prefixCls}-moving`]: isMoving,
@@ -203,75 +214,6 @@ const Preview: React.FC<PreviewProps> = props => {
203214
}
204215
};
205216

206-
const onMouseUp: React.MouseEventHandler<HTMLBodyElement> = () => {
207-
if (visible && isMoving) {
208-
setMoving(false);
209-
/** No need to restore the position when the picture is not moved, So as not to interfere with the click */
210-
const { transformX, transformY } = downPositionRef.current;
211-
const hasChangedPosition = x !== transformX && y !== transformY;
212-
if (!hasChangedPosition) {
213-
return;
214-
}
215-
216-
const width = imgRef.current.offsetWidth * scale;
217-
const height = imgRef.current.offsetHeight * scale;
218-
// eslint-disable-next-line @typescript-eslint/no-shadow
219-
const { left, top } = imgRef.current.getBoundingClientRect();
220-
const isRotate = rotate % 180 !== 0;
221-
222-
const fixState = getFixScaleEleTransPosition(
223-
isRotate ? height : width,
224-
isRotate ? width : height,
225-
left,
226-
top,
227-
);
228-
229-
if (fixState) {
230-
updateTransform({ ...fixState }, 'dragRebound');
231-
}
232-
}
233-
};
234-
235-
const onMouseDown: React.MouseEventHandler<HTMLDivElement> = event => {
236-
// Only allow main button
237-
if (!movable || event.button !== 0) return;
238-
event.preventDefault();
239-
event.stopPropagation();
240-
downPositionRef.current = {
241-
deltaX: event.pageX - transform.x,
242-
deltaY: event.pageY - transform.y,
243-
transformX: transform.x,
244-
transformY: transform.y,
245-
};
246-
setMoving(true);
247-
};
248-
249-
const onMouseMove: React.MouseEventHandler<HTMLBodyElement> = event => {
250-
if (visible && isMoving) {
251-
updateTransform(
252-
{
253-
x: event.pageX - downPositionRef.current.deltaX,
254-
y: event.pageY - downPositionRef.current.deltaY,
255-
},
256-
'move',
257-
);
258-
}
259-
};
260-
261-
const onWheel = (event: React.WheelEvent<HTMLImageElement>) => {
262-
if (!visible || event.deltaY == 0) return;
263-
// Scale ratio depends on the deltaY size
264-
const scaleRatio = Math.abs(event.deltaY / 100);
265-
// Limit the maximum scale ratio
266-
const mergedScaleRatio = Math.min(scaleRatio, WHEEL_MAX_SCALE_RATIO);
267-
// Scale the ratio each time
268-
let ratio = BASE_SCALE_RATIO + mergedScaleRatio * scaleStep;
269-
if (event.deltaY > 0) {
270-
ratio = BASE_SCALE_RATIO / ratio;
271-
}
272-
dispatchZoomChange(ratio, 'wheel', event.clientX, event.clientY);
273-
};
274-
275217
const onKeyDown = (event: KeyboardEvent) => {
276218
if (!visible || !showLeftOrRightSwitches) return;
277219

@@ -297,39 +239,6 @@ const Preview: React.FC<PreviewProps> = props => {
297239
}
298240
};
299241

300-
useEffect(() => {
301-
let onTopMouseUpListener;
302-
let onTopMouseMoveListener;
303-
let onMouseUpListener;
304-
let onMouseMoveListener;
305-
306-
if (movable) {
307-
onMouseUpListener = addEventListener(window, 'mouseup', onMouseUp, false);
308-
onMouseMoveListener = addEventListener(window, 'mousemove', onMouseMove, false);
309-
310-
try {
311-
// Resolve if in iframe lost event
312-
/* istanbul ignore next */
313-
if (window.top !== window.self) {
314-
onTopMouseUpListener = addEventListener(window.top, 'mouseup', onMouseUp, false);
315-
onTopMouseMoveListener = addEventListener(window.top, 'mousemove', onMouseMove, false);
316-
}
317-
} catch (error) {
318-
/* istanbul ignore next */
319-
warning(false, `[rc-image] ${error}`);
320-
}
321-
}
322-
323-
return () => {
324-
onMouseUpListener?.remove();
325-
onMouseMoveListener?.remove();
326-
/* istanbul ignore next */
327-
onTopMouseUpListener?.remove();
328-
/* istanbul ignore next */
329-
onTopMouseMoveListener?.remove();
330-
};
331-
}, [visible, isMoving, x, y, rotate, movable]);
332-
333242
useEffect(() => {
334243
const onKeyDownListener = addEventListener(window, 'keydown', onKeyDown, false);
335244

@@ -350,13 +259,17 @@ const Preview: React.FC<PreviewProps> = props => {
350259
transform: `translate3d(${transform.x}px, ${transform.y}px, 0) scale3d(${
351260
transform.flipX ? '-' : ''
352261
}${scale}, ${transform.flipY ? '-' : ''}${scale}, 1) rotate(${rotate}deg)`,
353-
transitionDuration: !enableTransition && '0s',
262+
transitionDuration: (!enableTransition || isTouching) && '0s',
354263
}}
355264
fallback={fallback}
356265
src={src}
357266
onWheel={onWheel}
358267
onMouseDown={onMouseDown}
359268
onDoubleClick={onDoubleClick}
269+
onTouchStart={onTouchStart}
270+
onTouchMove={onTouchMove}
271+
onTouchEnd={onTouchEnd}
272+
onTouchCancel={onTouchEnd}
360273
/>
361274
);
362275

src/hooks/useImageTransform.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,21 @@ export type TransformAction =
2525
| 'wheel'
2626
| 'doubleClick'
2727
| 'move'
28-
| 'dragRebound';
28+
| 'dragRebound'
29+
| 'touchZoom';
30+
31+
export type UpdateTransformFunc = (
32+
newTransform: Partial<TransformType>,
33+
action: TransformAction,
34+
) => void;
35+
36+
export type DispatchZoomChangeFunc = (
37+
ratio: number,
38+
action: TransformAction,
39+
centerX?: number,
40+
centerY?: number,
41+
isTouch?: boolean,
42+
) => void;
2943

3044
const initialTransform = {
3145
x: 0,
@@ -54,7 +68,7 @@ export default function useImageTransform(
5468
};
5569

5670
/** Direct update transform */
57-
const updateTransform = (newTransform: Partial<TransformType>, action: TransformAction) => {
71+
const updateTransform: UpdateTransformFunc = (newTransform, action) => {
5872
if (frame.current === null) {
5973
queue.current = [];
6074
frame.current = raf(() => {
@@ -76,36 +90,32 @@ export default function useImageTransform(
7690
});
7791
};
7892

79-
/** Scale according to the position of clientX and clientY */
80-
const dispatchZoomChange = (
81-
ratio: number,
82-
action: TransformAction,
83-
clientX?: number,
84-
clientY?: number,
85-
) => {
93+
/** Scale according to the position of centerX and centerY */
94+
const dispatchZoomChange: DispatchZoomChangeFunc = (ratio, action, centerX?, centerY?, isTouch?) => {
8695
const { width, height, offsetWidth, offsetHeight, offsetLeft, offsetTop } = imgRef.current;
8796

8897
let newRatio = ratio;
8998
let newScale = transform.scale * ratio;
9099
if (newScale > maxScale) {
91-
newRatio = maxScale / transform.scale;
92100
newScale = maxScale;
101+
newRatio = maxScale / transform.scale;
93102
} else if (newScale < minScale) {
94-
newRatio = minScale / transform.scale;
95-
newScale = minScale;
103+
// For mobile interactions, allow scaling down to the minimum scale.
104+
newScale = isTouch ? newScale : minScale;
105+
newRatio = newScale / transform.scale;
96106
}
97107

98108
/** Default center point scaling */
99-
const mergedClientX = clientX ?? innerWidth / 2;
100-
const mergedClientY = clientY ?? innerHeight / 2;
109+
const mergedCenterX = centerX ?? innerWidth / 2;
110+
const mergedCenterY = centerY ?? innerHeight / 2;
101111

102112
const diffRatio = newRatio - 1;
103113
/** Deviation calculated from image size */
104114
const diffImgX = diffRatio * width * 0.5;
105115
const diffImgY = diffRatio * height * 0.5;
106116
/** The difference between the click position and the edge of the document */
107-
const diffOffsetLeft = diffRatio * (mergedClientX - transform.x - offsetLeft);
108-
const diffOffsetTop = diffRatio * (mergedClientY - transform.y - offsetTop);
117+
const diffOffsetLeft = diffRatio * (mergedCenterX - transform.x - offsetLeft);
118+
const diffOffsetTop = diffRatio * (mergedCenterY - transform.y - offsetTop);
109119
/** Final positioning */
110120
let newX = transform.x - (diffOffsetLeft - diffImgX);
111121
let newY = transform.y - (diffOffsetTop - diffImgY);

0 commit comments

Comments
 (0)